okami.mom/albedo

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

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

main
6.9 KiB173 linesraw
1"""Finalized traces survive buffer release, nested evaluation, and late edits."""
2
3import json
4import unittest
5import urllib.parse
6
7from harness import Albedo, Provider, python, text
8
9
10class TracesTests(unittest.TestCase):
11    def setUp(self):
12        self.code = ""
13        self.provider = Provider(
14            lambda request: (
15                python(self.code)
16                if request["messages"][-1].get("role") == "user"
17                else text("done")
18            )
19        )
20        self.addCleanup(self.provider.close)
21        self.app = Albedo(self.provider)
22        self.app.__enter__()
23        self.addCleanup(self.app.__exit__, None, None, None)
24        self.session = self.app.session()
25
26    def run_cell(self, code, status="ok"):
27        self.code = code
28        self.app.prompt(self.session, "execute this cell").close()
29        self.app.idle(self.session)
30        entry = [
31            entry
32            for entry in self.app.history(self.session)["items"]
33            if entry["kind"] == "tool_result" and entry["tool"]["name"] == "python"
34        ][-1]
35
36        def referenced_json(field):
37            [reference] = [
38                part["reference"]
39                for part in entry["content"]
40                if part["kind"] == "reference" and part["reference"]["field"] == field
41            ]
42            pieces, offset, token = [], 0, None
43            for _ in range(100):
44                path = reference["url"]
45                if token is not None:
46                    path += "?" + urllib.parse.urlencode({"next": token})
47                with self.app.api(path) as response:
48                    page = json.load(response)
49                for chunk in page["parts"]:
50                    if chunk["field"] != field:
51                        continue
52                    self.assertEqual(chunk["offset_bytes"], offset)
53                    self.assertEqual(chunk["encoding"], "utf8")
54                    pieces.append(chunk["text"])
55                    offset += len(chunk["text"].encode())
56                token = page["next"]
57                if token is None:
58                    break
59            else:
60                self.fail(f"{field} content pagination did not terminate")
61            self.assertEqual(offset, reference["bytes"])
62            return json.loads("".join(pieces))
63
64        results = [
65            json.loads(part["value"])
66            for part in entry["content"]
67            if part["kind"] == "json" and part["field"] == "result"
68        ]
69        result = results[0] if results else json.loads(referenced_json("result"))
70        traces = [part["trace"] for part in entry["content"] if part["kind"] == "trace"]
71        trace = traces[0] if traces else referenced_json("trace")
72        self.assertEqual(result["status"], status, result)
73        return result, trace
74
75    def read_trace(self, id):
76        result, _ = self.run_cell(
77            f"import json\nprint(json.dumps(await cells.trace({id!r})))"
78        )
79        return json.loads(result["output"])
80
81    def test_top_level_and_nested_traces_preserve_changes_for_all_outcomes(self):
82        for status, ending in (
83            ("ok", ""),
84            ("error", "raise ValueError('failed')"),
85            ("interrupted", "raise __import__('asyncio').CancelledError()"),
86        ):
87            with self.subTest(status=status):
88                (self.app.workspace / "note.txt").write_text("before\n")
89                (self.app.workspace / "binary.dat").unlink(missing_ok=True)
90                source = (
91                    """
92import os
93open('temporary.txt', 'w').write('after\\n')
94os.replace('temporary.txt', 'note.txt')
95open('note.txt').read()
96open('binary.dat', 'wb').write(b'\\0binary')
97"""
98                    + ending
99                )
100                original, trace = self.run_cell(source, status)
101                self.assertEqual(trace, self.read_trace(original["cell_id"]))
102                self.assert_changes(trace)
103                # The exact saved source runs as a separate nested cell.
104                (self.app.workspace / "note.txt").write_text("before\n")
105                (self.app.workspace / "binary.dat").unlink()
106                parent, parent_trace = self.run_cell(
107                    f"try:\n    await cells.run({original['cell_id']!r}, allow_partial=True)\n"
108                    "except BaseException:\n    pass\nprint(cells.last_id)"
109                )
110                child = parent["output"].strip().splitlines()[-1]
111                nested = self.read_trace(child)
112                self.assert_changes(nested)
113                self.run_cell(
114                    f"assert (await cells.info({child!r}))['status'] == {status!r}"
115                )
116                self.assertEqual(parent_trace["changes"], [])
117
118    def assert_changes(self, trace):
119        changes = {change["path"].split("/")[-1]: change for change in trace["changes"]}
120        self.assertNotIn("temporary.txt", changes)
121        self.assertIn("-before", changes["note.txt"]["diff"])
122        self.assertIn("+after", changes["note.txt"]["diff"])
123        self.assertEqual(changes["binary.dat"]["kind"], "unavailable")
124        self.assertTrue(
125            any(
126                activity["kind"] == "read" and activity["target"].endswith("note.txt")
127                for activity in trace["activities"]
128            )
129        )
130
131    def test_many_edit_heavy_cells_release_buffers_and_keep_old_traces(self):
132        oldest = None
133        oldest_trace = None
134        for index in range(18):
135            result, trace = self.run_cell(
136                f"for index in range(20):\n    open(f'edit-{{index}}.txt', 'w').write({str(index)!r})\n"
137                "open('edit-0.txt').read()"
138            )
139            self.assertTrue(trace["truncated"])
140            self.assertTrue(trace["changes"])
141            if oldest is None:
142                oldest, oldest_trace = result["cell_id"], trace
143        result, _ = self.run_cell("""
144import __main__ as kernel
145completed = [capture for capture in kernel.ARCHIVES.values() if capture.trace.sealed]
146assert completed
147assert all(not capture.trace.before and not capture.trace.activities for capture in completed)
148print('released')
149""")
150        self.assertIn("released", result["output"])
151        self.assertEqual(self.read_trace(oldest), oldest_trace)
152
153    def test_late_background_edits_execute_without_changing_finalized_trace(self):
154        result, trace = self.run_cell("""
155import asyncio
156import __main__ as kernel
157late_gate = asyncio.Event()
158async def late_edit():
159    await late_gate.wait()
160    open('late.txt', 'w').write('late\\n')
161    open('late.txt').read()
162late_task = asyncio.create_task(late_edit())
163open('early.txt', 'w').write('early\\n')
164""")
165        self.run_cell(f"""
166late_gate.set()
167await late_task
168capture = kernel.ARCHIVES[{result["cell_id"]!r}]
169assert capture.trace.sealed
170assert not capture.trace.before and not capture.trace.activities
171""")
172        self.assertEqual((self.app.workspace / "late.txt").read_text(), "late\n")
173        self.assertEqual(self.read_trace(result["cell_id"]), trace)