okami.mom/albedo

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

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

main
8.5 KiB215 linesraw
1"""Linked workspaces: one project in several places.
2
3A workspace linked with another reads that one's memory and work items as
4well as its own, while everything it writes is filed under itself. A member
5whose folder is gone keeps what it had until the user removes it, and
6unlinking deletes nothing.
7"""
8
9import json
10import re
11import shutil
12import unittest
13import urllib.error
14import urllib.parse
15
16from harness import Albedo, Provider, python, text
17
18
19class LinkTests(unittest.TestCase):
20    def setUp(self):
21        self.cells = []
22
23        def script(request):
24            if request["input"][-1].get("role") == "user" and self.cells:
25                return python(self.cells.pop(0))
26            return text("ok")
27
28        self.provider = Provider(script)
29        self.addCleanup(self.provider.close)
30        self.app = Albedo(self.provider, protocol="responses")
31        self.app.__enter__()
32        self.addCleanup(self.app.__exit__, None, None, None)
33
34    def cell(self, session, code):
35        self.cells.append(code)
36        self.app.prompt(session, "run it").close()
37        self.app.idle(session)
38        results = [
39            json.loads(part["value"])
40            for entry in self.app.history(session)["items"]
41            if entry["kind"] == "tool_result" and entry["tool"]["name"] == "python"
42            for part in entry["content"]
43            if part["kind"] == "json" and part["field"] == "result"
44        ]
45        self.assertEqual(results[-1]["status"], "ok", results[-1])
46        return results[-1]
47
48    def group(self, workspace):
49        query = urllib.parse.urlencode({"workspace": str(workspace)})
50        with self.app.api(f"/extensions/links/groups?{query}") as response:
51            return json.load(response)
52
53    def merge(self, workspace, other):
54        group = self.group(workspace)["configuration_resource"]
55        other_group = self.group(other)["configuration_resource"]
56        with self.app.api(
57            group["url"],
58            {"other_workspace": str(other), "other_etag": other_group["etag"]},
59            headers={"If-Match": group["etag"]},
60        ) as response:
61            return json.load(response)
62
63    def unlink(self, workspace, member):
64        group = self.group(workspace)["configuration_resource"]
65        with self.app.api(
66            group["url"] + "&" + urllib.parse.urlencode({"member": str(member)}),
67            method="DELETE",
68            headers={"If-Match": group["etag"]},
69        ) as response:
70            return json.load(response)
71
72    def memory_file(self, workspace):
73        slug = re.sub(r"[^A-Za-z0-9]", "-", str(workspace))
74        return self.app.home / "memories" / slug / "memory.md"
75
76    def test_linked_workspaces_read_each_other_and_write_their_own(self):
77        laptop, server = self.app.root / "laptop", self.app.root / "server"
78        laptop.mkdir()
79        server.mkdir()
80        first = self.app.session(laptop)
81        self.cell(
82            first,
83            "memory.append('the cache key is the prefix hash')\n"
84            "await work.create('ship the picker')",
85        )
86
87        self.app.session(server)
88        added = self.merge(server, laptop)
89        self.assertEqual(
90            set(added["resource"]["value"]["members"]), {str(server), str(laptop)}
91        )
92        group = self.group(server)
93        self.assertEqual(
94            set(group["configuration_resource"]["value"]["members"]),
95            {str(server), str(laptop)},
96        )
97
98        # A session opened after the link starts with the linked memory and
99        # is told it is linked.
100        later = self.app.session(server)
101        read = self.cell(
102            later,
103            "memory.append('chernobog runs the gate')\n"
104            "await work.create('profile the gate')\n"
105            "print(memory.grep('cache key'))\n"
106            "print(sorted((i.title, i.workspace) for i in await work.list()))",
107        )
108        self.assertIn(
109            "[" + str(laptop) + "] memory.md:1: the cache key", read["output"]
110        )
111        self.assertIn(f"('ship the picker', '{laptop}')", read["output"])
112        self.assertIn(f"('profile the gate', '{server}')", read["output"])
113        instructions = self.provider.requests[-1]["request"]["instructions"]
114        self.assertIn("the cache key is the prefix hash", instructions)
115        self.assertIn("This workspace is linked with " + str(laptop), instructions)
116        # Writes stayed where they were made.
117        self.assertEqual(
118            self.memory_file(server).read_text(), "chernobog runs the gate\n"
119        )
120        self.assertEqual(
121            self.memory_file(laptop).read_text(), "the cache key is the prefix hash\n"
122        )
123
124        # The laptop checkout goes away: it is shown as gone and still read.
125        shutil.rmtree(laptop)
126        presence = {item["workspace"]: item for item in self.group(server)["presence"]}
127        self.assertEqual(presence[str(laptop)]["exists"], False)
128        kept = self.cell(later, "print(memory.grep('cache key'))")
129        self.assertIn("the cache key", kept["output"])
130
131        # Removing it stops the reads and deletes nothing.
132        self.unlink(server, laptop)
133        self.assertEqual(
134            self.group(server)["configuration_resource"]["value"]["members"],
135            [str(server)],
136        )
137        alone = self.cell(
138            later,
139            "print(memory.grep('cache key'))\n"
140            "print([i.title for i in await work.list()])",
141        )
142        self.assertNotIn("the cache key", alone["output"])
143        self.assertNotIn("ship the picker", alone["output"])
144        self.assertTrue(self.memory_file(laptop).exists())
145
146    def test_open_sessions_in_the_group_are_told_with_the_memory_they_now_read(self):
147        laptop, server = self.app.root / "desk", self.app.root / "rack"
148        laptop.mkdir()
149        server.mkdir()
150        on_laptop = self.app.session(laptop)
151        self.cell(on_laptop, "memory.append('the cache key is the prefix hash')")
152        here, beside = self.app.session(server), self.app.session(server)
153        self.cell(here, "1")
154        self.cell(beside, "1")
155
156        added = self.merge(server, laptop)
157        self.assertEqual(added["notification_count"], 3)
158        self.assertEqual(
159            {item["session_id"] for item in added["notifications"]},
160            {on_laptop, here, beside},
161        )
162        self.assertTrue(
163            all(
164                item["notification"]["state"] == "queued"
165                for item in added["notifications"]
166            )
167        )
168
169        def next_request(session):
170            self.app.prompt(session, "hi").close()
171            self.app.idle(session)
172            request = self.provider.requests[-1]["request"]
173            return json.dumps(request["input"]), request["instructions"]
174
175        # Sessions in this workspace opened before the link: the laptop's
176        # memory arrives in the note, not in a rebuilt prompt.
177        for session in (here, beside):
178            told, instructions = next_request(session)
179            self.assertIn("linked this workspace with " + str(laptop), told)
180            self.assertIn("the cache key is the prefix hash", told)
181            self.assertNotIn("the cache key", instructions)
182        told, _ = next_request(on_laptop)
183        self.assertIn("linked this workspace with " + str(server), told)
184        # A session created after the link starts with the linked memory.
185        unopened = self.app.session(server)
186        told, instructions = next_request(unopened)
187        self.assertNotIn("linked this workspace", told)
188        self.assertIn("the cache key is the prefix hash", instructions)
189
190        self.unlink(server, laptop)
191        told, _ = next_request(on_laptop)
192        self.assertIn("unlinked this workspace from " + str(server), told)
193        told, _ = next_request(beside)
194        self.assertIn("The user unlinked " + str(laptop), told)
195
196    def test_a_link_is_refused_for_this_workspace_and_missing_folders(self):
197        workspace = self.app.root / "alone"
198        workspace.mkdir()
199        self.app.session(workspace)
200        for details, status, code in (
201            (str(workspace), 409, "already_linked"),
202            (str(self.app.root / "nowhere"), 400, "invalid_request"),
203        ):
204            with self.assertRaises(urllib.error.HTTPError) as refused:
205                self.merge(workspace, details)
206            self.assertEqual(refused.exception.code, status)
207            self.assertEqual(json.load(refused.exception)["code"], code)
208        self.assertEqual(
209            self.group(workspace)["configuration_resource"]["value"]["members"],
210            [str(workspace)],
211        )
212
213
214if __name__ == "__main__":
215    unittest.main()