okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1"""Installed skills follow package symlinks and keep resource bases across reloads.""" 2 3import json 4import os 5import shutil 6import unittest 7 8from harness import Albedo ,Provider ,python ,text 9 10 11class SkillSymlinkTests (unittest .TestCase ): 12def setUp (self ): 13self .code = "" 14self .provider = Provider (self .reply ) 15self .addCleanup (self .provider .close ) 16self .app = Albedo (self .provider ,protocol = "responses" ) 17self .app .__enter__ () 18self .addCleanup (self .app .__exit__ ,None ,None ,None ) 19self .external = self .app .root / f"packages- { self . app . workspace . name } " 20self .external .mkdir () 21self .addCleanup (shutil .rmtree ,self .external ) 22self .installed = self .app .workspace / ".agents/skills" 23self .installed .mkdir (parents = True ) 24self .session = self .app .session () 25self .route = f"/sessions/ { self . session } /catalog" 26 27def reply (self ,request ): 28if request ["input" ][- 1 ].get ("type" )== "function_call_output" : 29return text ("done" ) 30return python (self .code )if self .code else text ("ok" ) 31 32def request (self ,path ,body = None ): 33with self .app .api (path ,body )as response : 34return json .load (response ) 35 36def skill (self ,directory ,name ,body = "INSTRUCTIONS" ): 37directory .mkdir (parents = True ,exist_ok = True ) 38source = directory / "SKILL.md" 39source .write_text (f"---\nname: { name } \ndescription: { name } \n---\n { body } \n" ) 40return source 41 42def reload (self ): 43self .request ( 44f"/sessions/ { self . session } /reload" , 45 {"target" :"session" }, 46 ) 47 48def probe (self ,code ): 49self .code = code + "\nprint('SKILL_PROBE_OK')" 50before = len (self .provider .requests ) 51self .app .prompt (self .session ,"probe installed skills" ).close () 52self .app .idle (self .session ) 53requests = [item ["request" ]for item in self .provider .requests [before :]] 54output = next ( 55item ["output" ] 56for item in reversed (requests [- 1 ]["input" ]) 57if item .get ("type" )== "function_call_output" 58 ) 59self .assertIn ("SKILL_PROBE_OK" ,output ,output ) 60return requests [0 ] 61 62def test_external_directory_links_discover_activate_and_read_resources (self ): 63for name ,relative in [("absolute" ,False ), ("relative" ,True )]: 64source = self .skill ( 65self .external / f"hash- { name } -package" ,name ,f"BODY_ { name } " 66 ) 67link = self .installed / name 68target = ( 69os .path .relpath (source .parent ,link .parent ) 70if relative 71else source .parent 72 ) 73link .symlink_to (target ,target_is_directory = True ) 74 (source .parent / "reference.txt" ).write_text (f"REFERENCE_ { name } " ) 75catalog = self .request (self .route ) 76for name in ("absolute" ,"relative" ): 77row = next ( 78row 79for row in catalog ["discovery" ]["candidates" ] 80if row ["title" ]== name 81 ) 82self .assertEqual (row ["source" ],str (self .installed / name / "SKILL.md" )) 83self .assertEqual ( 84row ["resolved_source" ], 85str ((self .installed / name / "SKILL.md" ).resolve ()), 86 ) 87self .reload () 88request = self .probe ( 89"for name in ['absolute', 'relative']:\n" 90" activation = await commands.invoke('/' + name)\n" 91" assert 'BODY_' + name in activation['instructions'], activation\n" 92f" assert activation['source'] == { str ( self . installed )!r } + '/' + name + '/SKILL.md', activation\n" 93" resources = await skills.resources(name)\n" 94" assert 'reference.txt' in resources.resources, resources\n" 95" page = await skills.read(name, resource='reference.txt')\n" 96" assert page.content == 'REFERENCE_' + name, page" 97 ) 98for name in ("absolute" ,"relative" ): 99self .assertIn ( 100str (self .installed / name / "SKILL.md" ),request ["instructions" ] 101 ) 102 103def test_linked_instructions_keep_installed_siblings_and_follow_resource_links ( 104self , 105 ): 106source = self .skill (self .external / "hash-instructions" ,"file-linked" ) 107renamed = source .with_name ("package-instructions.md" ) 108source .rename (renamed ) 109source = renamed 110directory = self .installed / "file-linked" 111directory .mkdir () 112 (directory / "SKILL.md" ).symlink_to (source ) 113 (directory / "local.txt" ).write_text ("INSTALLED_SIBLING" ) 114 (source .parent / "local.txt" ).write_text ("WRONG_TARGET_SIBLING" ) 115resources = self .external / "hash-resources" 116resources .mkdir () 117 (resources / "reference.txt" ).write_text ("LINKED_RESOURCE" ) 118 (resources / "cycle" ).symlink_to (resources ,target_is_directory = True ) 119 (directory / "references" ).symlink_to (resources ,target_is_directory = True ) 120 (directory / "other-references" ).symlink_to (resources ,target_is_directory = True ) 121 (directory / "asset.txt" ).symlink_to ( 122os .path .relpath (resources / "reference.txt" ,directory ) 123 ) 124self .reload () 125self .probe ( 126"activation = await commands.invoke('/file-linked')\n" 127f"assert activation['source'] == { str ( directory / 'SKILL.md' )!r } , activation\n" 128"resources = await skills.resources('file-linked')\n" 129"assert set(resources.resources) == {'SKILL.md', 'local.txt', 'asset.txt', 'references/reference.txt', 'other-references/reference.txt'}, resources\n" 130"assert (await skills.read('file-linked', resource='local.txt')).content == 'INSTALLED_SIBLING'\n" 131"assert (await skills.read('file-linked', resource='references/reference.txt')).content == 'LINKED_RESOURCE'\n" 132"assert (await skills.read('file-linked', resource='asset.txt')).content == 'LINKED_RESOURCE'\n" 133"for path in ['../SKILL.md', '/etc/passwd']:\n" 134" try:\n" 135" await skills.read('file-linked', resource=path)\n" 136" assert False, path\n" 137" except SkillsError:\n" 138" pass" 139 ) 140 141replacement = self .external / "replacement-resource.txt" 142replacement .write_text ("UPDATED_RESOURCE" ) 143 (directory / "asset.txt" ).unlink () 144 (directory / "asset.txt" ).symlink_to (replacement ) 145self .probe ( 146"assert (await skills.read('file-linked', resource='asset.txt')).content == 'UPDATED_RESOURCE'" 147 ) 148 149def test_changed_selections_require_reload (self ): 150for change in ("directory" ,"instructions" ,"replacement" ): 151with self .subTest (change = change ): 152name = f"pinned- { change } " 153first = self .skill (self .external / f"first- { change } " ,name ) 154installed = self .installed / name 155if change == "instructions" : 156installed .mkdir () 157link = installed / "SKILL.md" 158link .symlink_to (first ) 159else : 160link = installed 161link .symlink_to (first .parent ,target_is_directory = True ) 162 (installed / "resource.txt" ).write_text ("FIRST_RESOURCE" ) 163self .reload () 164initial = self .request (self .route ) 165 166if change == "replacement" : 167replacement = first .with_name ("replacement.md" ) 168replacement .write_bytes (first .read_bytes ()) 169replacement .replace (first ) 170expected_resource = "FIRST_RESOURCE" 171current = first 172else : 173current = self .skill (self .external / f"second- { change } " ,name ) 174link .unlink () 175if change == "directory" : 176link .symlink_to (current .parent ,target_is_directory = True ) 177 (current .parent / "resource.txt" ).write_text ("SECOND_RESOURCE" ) 178expected_resource = "SECOND_RESOURCE" 179else : 180link .symlink_to (current ) 181expected_resource = "FIRST_RESOURCE" 182self .assertNotEqual ( 183initial ["discovery" ]["revision" ], 184self .request (self .route )["discovery" ]["revision" ], 185 ) 186self .probe ( 187f"for operation in [lambda: commands.invoke('/' + { name !r } ), lambda: skills.resources( { name !r } ), lambda: skills.read( { name !r } )]:\n" 188" try:\n" 189" await operation()\n" 190" assert False, 'changed selection must require reload'\n" 191" except (CommandsError, SkillsError) as error:\n" 192" assert 'reload' in str(error).lower(), error" 193 ) 194self .reload () 195self .probe ( 196f"assert 'INSTRUCTIONS' in (await commands.invoke('/' + { name !r } ))['instructions']\n" 197f"assert (await skills.read( { name !r } , resource='resource.txt')).content == { expected_resource !r } " 198 ) 199current .write_text ( 200current .read_text ().replace ("INSTRUCTIONS" ,"NEW_BODY" ) 201 ) 202self .probe ( 203f"assert 'NEW_BODY' in (await commands.invoke('/' + { name !r } ))['instructions']" 204 )