okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffauffeat(api)!: migrate daemon and clients to protocol 361c43ca

main
9.4 KiB204 linesraw
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):
12    def setUp(self):
13        self.code = ""
14        self.provider = Provider(self.reply)
15        self.addCleanup(self.provider.close)
16        self.app = Albedo(self.provider, protocol="responses")
17        self.app.__enter__()
18        self.addCleanup(self.app.__exit__, None, None, None)
19        self.external = self.app.root / f"packages-{self.app.workspace.name}"
20        self.external.mkdir()
21        self.addCleanup(shutil.rmtree, self.external)
22        self.installed = self.app.workspace / ".agents/skills"
23        self.installed.mkdir(parents=True)
24        self.session = self.app.session()
25        self.route = f"/sessions/{self.session}/catalog"
26
27    def reply(self, request):
28        if request["input"][-1].get("type") == "function_call_output":
29            return text("done")
30        return python(self.code) if self.code else text("ok")
31
32    def request(self, path, body=None):
33        with self.app.api(path, body) as response:
34            return json.load(response)
35
36    def skill(self, directory, name, body="INSTRUCTIONS"):
37        directory.mkdir(parents=True, exist_ok=True)
38        source = directory / "SKILL.md"
39        source.write_text(f"---\nname: {name}\ndescription: {name}\n---\n{body}\n")
40        return source
41
42    def reload(self):
43        self.request(
44            f"/sessions/{self.session}/reload",
45            {"target": "session"},
46        )
47
48    def probe(self, code):
49        self.code = code + "\nprint('SKILL_PROBE_OK')"
50        before = len(self.provider.requests)
51        self.app.prompt(self.session, "probe installed skills").close()
52        self.app.idle(self.session)
53        requests = [item["request"] for item in self.provider.requests[before:]]
54        output = next(
55            item["output"]
56            for item in reversed(requests[-1]["input"])
57            if item.get("type") == "function_call_output"
58        )
59        self.assertIn("SKILL_PROBE_OK", output, output)
60        return requests[0]
61
62    def test_external_directory_links_discover_activate_and_read_resources(self):
63        for name, relative in [("absolute", False), ("relative", True)]:
64            source = self.skill(
65                self.external / f"hash-{name}-package", name, f"BODY_{name}"
66            )
67            link = self.installed / name
68            target = (
69                os.path.relpath(source.parent, link.parent)
70                if relative
71                else source.parent
72            )
73            link.symlink_to(target, target_is_directory=True)
74            (source.parent / "reference.txt").write_text(f"REFERENCE_{name}")
75        catalog = self.request(self.route)
76        for name in ("absolute", "relative"):
77            row = next(
78                row
79                for row in catalog["discovery"]["candidates"]
80                if row["title"] == name
81            )
82            self.assertEqual(row["source"], str(self.installed / name / "SKILL.md"))
83            self.assertEqual(
84                row["resolved_source"],
85                str((self.installed / name / "SKILL.md").resolve()),
86            )
87        self.reload()
88        request = 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"
92            f"    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        )
98        for name in ("absolute", "relative"):
99            self.assertIn(
100                str(self.installed / name / "SKILL.md"), request["instructions"]
101            )
102
103    def test_linked_instructions_keep_installed_siblings_and_follow_resource_links(
104        self,
105    ):
106        source = self.skill(self.external / "hash-instructions", "file-linked")
107        renamed = source.with_name("package-instructions.md")
108        source.rename(renamed)
109        source = renamed
110        directory = self.installed / "file-linked"
111        directory.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")
115        resources = self.external / "hash-resources"
116        resources.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(
122            os.path.relpath(resources / "reference.txt", directory)
123        )
124        self.reload()
125        self.probe(
126            "activation = await commands.invoke('/file-linked')\n"
127            f"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
141        replacement = self.external / "replacement-resource.txt"
142        replacement.write_text("UPDATED_RESOURCE")
143        (directory / "asset.txt").unlink()
144        (directory / "asset.txt").symlink_to(replacement)
145        self.probe(
146            "assert (await skills.read('file-linked', resource='asset.txt')).content == 'UPDATED_RESOURCE'"
147        )
148
149    def test_changed_selections_require_reload(self):
150        for change in ("directory", "instructions", "replacement"):
151            with self.subTest(change=change):
152                name = f"pinned-{change}"
153                first = self.skill(self.external / f"first-{change}", name)
154                installed = self.installed / name
155                if change == "instructions":
156                    installed.mkdir()
157                    link = installed / "SKILL.md"
158                    link.symlink_to(first)
159                else:
160                    link = installed
161                    link.symlink_to(first.parent, target_is_directory=True)
162                (installed / "resource.txt").write_text("FIRST_RESOURCE")
163                self.reload()
164                initial = self.request(self.route)
165
166                if change == "replacement":
167                    replacement = first.with_name("replacement.md")
168                    replacement.write_bytes(first.read_bytes())
169                    replacement.replace(first)
170                    expected_resource = "FIRST_RESOURCE"
171                    current = first
172                else:
173                    current = self.skill(self.external / f"second-{change}", name)
174                    link.unlink()
175                    if change == "directory":
176                        link.symlink_to(current.parent, target_is_directory=True)
177                        (current.parent / "resource.txt").write_text("SECOND_RESOURCE")
178                        expected_resource = "SECOND_RESOURCE"
179                    else:
180                        link.symlink_to(current)
181                        expected_resource = "FIRST_RESOURCE"
182                self.assertNotEqual(
183                    initial["discovery"]["revision"],
184                    self.request(self.route)["discovery"]["revision"],
185                )
186                self.probe(
187                    f"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                )
194                self.reload()
195                self.probe(
196                    f"assert 'INSTRUCTIONS' in (await commands.invoke('/' + {name!r}))['instructions']\n"
197                    f"assert (await skills.read({name!r}, resource='resource.txt')).content == {expected_resource!r}"
198                )
199                current.write_text(
200                    current.read_text().replace("INSTRUCTIONS", "NEW_BODY")
201                )
202                self.probe(
203                    f"assert 'NEW_BODY' in (await commands.invoke('/' + {name!r}))['instructions']"
204                )