okami.mom/albedo

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

Aly Raffaufrefactor(test): share inspection and stream supportcba5ff8

main
6.5 KiB187 linesraw
1"""Real provider and session fixtures for execution, persistence, and recovery."""
2
3import json
4import time
5import unittest
6import urllib.error
7import urllib.parse
8
9from harness import Albedo, Provider, operation_id, python, text
10
11
12def contents(item):
13    content = item.get("content", "")
14    if isinstance(content, str):
15        return content
16    return " ".join(part.get("text", "") for part in content if isinstance(part, dict))
17
18
19def script(request):
20    inputs = request.get("messages", request.get("input", []))
21    user = next(
22        (contents(item) for item in reversed(inputs) if item.get("role") == "user"), ""
23    )
24    tools = sum(
25        item.get("role") == "tool" or item.get("type") == "function_call_output"
26        for item in inputs
27    )
28    if "continue from branch" in user:
29        return text("finished")
30    if "no first token" in user:
31        return text("never streamed", hang=True)
32    if "over 100 turns" in user:
33        return text("finished") if tools >= 105 else python("1")
34    if "workspace probe" in user:
35        code = (
36            "import os\nfrom pathlib import Path\n"
37            "assert 'workspace_marker' not in globals()\n"
38            "workspace_marker = os.getcwd()\nPath('cwd-probe').write_text(os.getcwd())"
39        )
40        done = (
41            inputs[-1].get("role") == "tool"
42            or inputs[-1].get("type") == "function_call_output"
43        )
44        return text("finished") if done else python(code)
45    code = (
46        "import asyncio, os\nfrom pathlib import Path\n"
47        "Path('example.txt').write_text('hello\\n')\n"
48        "saved = Path('example.txt').read_text()\n"
49        "assert 'ALBEDO_API_KEY' not in os.environ\n"
50        "assert 'ALBEDO_TOKEN' not in os.environ\n"
51        + (
52            "Path('recovery-started').write_text('ready')\n"
53            if "hang then recover" in user
54            else ""
55        )
56        + f"await asyncio.sleep({30 if 'hang' in user else 2 if 'first task' in user else 0.4})\nlen(saved)"
57    )
58    return (
59        text("finished")
60        if tools
61        else python(code, reasoning="reasoning about the task")
62    )
63
64
65class IntegrationScenario(unittest.TestCase):
66    def setUp(self):
67        self.provider = Provider(script)
68        self.addCleanup(self.provider.close)
69
70    def app_for(self, protocol, *, prepare=None, providers=None):
71        app = Albedo(
72            self.provider, protocol=protocol, prepare=prepare, providers=providers
73        )
74        app.__enter__()
75        self.addCleanup(app.__exit__, None, None, None)
76        return app
77
78    def read(self, app, path):
79        with app.api(path) as response:
80            return json.load(response)
81
82    def change(self, app, session, fields):
83        with app.api(f"/sessions/{session}?view=configuration") as response:
84            configuration = json.load(response)
85            etag = response.headers["ETag"]
86        if "workspace" in fields:
87            fields = {**fields, "family_revision": configuration["family_revision"]}
88        with app.api(
89            f"/sessions/{session}?view=configuration",
90            fields,
91            method="PATCH",
92            headers={"If-Match": etag},
93        ) as response:
94            return json.load(response)
95
96    def history(self, app, session):
97        page = app.history(session)
98        entries = page["items"]
99        while page["older"]:
100            page = self.read(
101                app,
102                f"/sessions/{session}/history?next={urllib.parse.quote(page['older'], safe='')}",
103            )
104            entries = page["items"] + entries
105        return entries
106
107    def entry_text(self, entry):
108        return "".join(
109            part["text"] for part in entry["content"] if part["kind"] == "text"
110        )
111
112    def send(self, app, session, message):
113        app.prompt(session, message).close()
114        app.idle(session, timeout=90)
115
116    def settings(self, app, protocol, active="alpha", **models):
117        other = "responses" if protocol == "chat_completions" else "chat_completions"
118        configured = {
119            "alpha": ("alpha-2", models.get("alpha", "fixture-alpha"), protocol),
120            "beta": ("beta-1", models.get("beta", "fixture-beta"), protocol),
121            "gamma": ("gamma-1", models.get("gamma", "fixture-gamma"), other),
122        }
123        providers = {
124            name: {
125                "baseUrl": self.provider.url + f"/{name}/v1",
126                "apiKey": key,
127                "model": model,
128                "protocol": binding,
129            }
130            for name, (key, model, binding) in configured.items()
131        }
132        (app.home / "config.json").write_text(
133            json.dumps({"active": active, "providers": providers})
134        )
135
136    def default_session(self, app):
137        identity = operation_id()
138        with app.api(
139            f"/sessions/{identity}",
140            {"kind": "new", "workspace": str(app.workspace)},
141            method="PUT",
142            headers={"If-None-Match": "*"},
143        ) as response:
144            self.assertEqual(json.load(response)["id"], identity)
145        return identity
146
147    def records(self, start):
148        return self.provider.requests[start:]
149
150    def wait_for_request(self, start):
151        deadline = time.monotonic() + 15
152        while len(self.provider.requests) <= start and time.monotonic() < deadline:
153            time.sleep(0.05)
154        self.assertGreater(len(self.provider.requests), start)
155
156    def assert_projected(self, record, protocol, users):
157        items = record["request"][
158            "messages" if protocol == "chat_completions" else "input"
159        ]
160        serialized = json.dumps(items)
161        for user in users:
162            self.assertIn(user, serialized)
163        self.assertIn("finished", serialized)
164        if protocol == "chat_completions":
165            calls = [
166                call["id"]
167                for item in items
168                if item.get("role") == "assistant"
169                for call in item.get("tool_calls", [])
170            ]
171            outputs = [
172                item["tool_call_id"] for item in items if item.get("role") == "tool"
173            ]
174        else:
175            calls = [
176                item["call_id"] for item in items if item.get("type") == "function_call"
177            ]
178            outputs = [
179                item["call_id"]
180                for item in items
181                if item.get("type") == "function_call_output"
182            ]
183        self.assertTrue(any(call.startswith("fixture-call-") for call in calls))
184        self.assertTrue(set(calls) & set(outputs))
185
186    def restart(self, app):
187        app.restart(crash=True)