okami.mom/albedo

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

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

main
8.0 KiB191 linesraw
1"""An ordinary HTTP client can recover identified chat without CLI or private APIs."""
2
3import http.client
4import json
5import threading
6import unittest
7import urllib.parse
8
9from harness import Albedo, Provider, operation_id, text
10
11
12class HTTPConsumerTest(unittest.TestCase):
13    def read_json(self, response):
14        self.assertEqual(
15            (response.getheader("Content-Type") or "").split(";", 1)[0],
16            "application/json",
17        )
18        payload = response.read(1048577)
19        self.assertLessEqual(len(payload), 1048576)
20        return json.loads(payload)
21
22    def test_identified_chat_reconnects_and_reconciles_durable_history(self):
23        requested = threading.Event()
24        release = threading.Event()
25
26        def reply(_):
27            requested.set()
28            if not release.wait(20):
29                raise AssertionError("consumer did not release provider")
30            return text("ordinary HTTP answer")
31
32        provider = Provider(reply)
33        self.addCleanup(provider.close)
34        self.addCleanup(release.set)
35        with Albedo(provider) as app:
36            address = urllib.parse.urlsplit(app.base)
37            headers = {
38                "Authorization": "Bearer " + app.connection["token"],
39                "Content-Type": "application/json",
40            }
41            connection = http.client.HTTPConnection(
42                address.hostname, address.port, timeout=20
43            )
44            self.addCleanup(connection.close)
45            session_id = operation_id()
46            intent = {
47                "kind": "new",
48                "workspace": str(app.workspace),
49                "provider_profile": app.profile,
50            }
51            resource = f"/sessions/{session_id}"
52            connection.request(
53                "PUT", resource, json.dumps(intent), {**headers, "If-None-Match": "*"}
54            )
55            response = connection.getresponse()
56            if response.status != 201:
57                self.fail(
58                    f"session creation returned {response.status}: {response.read()!r}"
59                )
60            self.assertEqual(response.getheader("Location"), resource)
61            created = self.read_json(response)
62            self.assertEqual(created["id"], session_id)
63            self.assertEqual(
64                created["creation"]["submitted"]["workspace"], intent["workspace"]
65            )
66
67            connection.request("GET", resource, headers=headers)
68            snapshot = self.read_json(connection.getresponse())
69            cursor = snapshot["cursor"]
70            input_id = operation_id()
71            input_resource = f"{resource}/inputs/{input_id}"
72            message = {"kind": "message", "text": "ordinary HTTP question"}
73            connection.request("PUT", input_resource, json.dumps(message), headers)
74            accepted_response = connection.getresponse()
75            self.assertEqual(accepted_response.status, 202)
76            accepted = self.read_json(accepted_response)
77            self.assertEqual(accepted["id"], input_id)
78            self.assertEqual(accepted["admission"], "accepted")
79            self.assertTrue(requested.wait(10), "input did not reach provider")
80
81            # Resolve a lost acknowledgement through the resource itself.
82            connection.request("GET", input_resource, headers=headers)
83            self.assertEqual(self.read_json(connection.getresponse())["id"], input_id)
84            connection.request("PUT", input_resource, json.dumps(message), headers)
85            duplicate_response = connection.getresponse()
86            self.assertEqual(duplicate_response.status, 202)
87            self.assertEqual(self.read_json(duplicate_response)["id"], input_id)
88
89            query = urllib.parse.urlencode(
90                {
91                    "after_generation": cursor["generation"],
92                    "after_seq": cursor["sequence"],
93                }
94            )
95            stream = http.client.HTTPConnection(
96                address.hostname, address.port, timeout=20
97            )
98            self.addCleanup(stream.close)
99            stream.request(
100                "GET",
101                resource + "?" + query,
102                headers={**headers, "Accept": "text/event-stream"},
103            )
104            response = stream.getresponse()
105            self.assertEqual(response.status, 200)
106            self.assertEqual(
107                (response.getheader("Content-Type") or "").split(";", 1)[0],
108                "text/event-stream",
109            )
110            release.set()
111            consumed = cursor["sequence"]
112            finished = False
113            for line in response:
114                if not line.startswith(b"data: "):
115                    continue
116                self.assertLessEqual(len(line[6:].rstrip(b"\r\n")), 1048576)
117                batch = json.loads(line[6:])
118                self.assertEqual(batch["generation"], cursor["generation"])
119                sequences = [event["sequence"] for event in batch["events"]]
120                self.assertEqual(
121                    sequences, list(range(consumed + 1, consumed + 1 + len(sequences)))
122                )
123                self.assertEqual(
124                    batch["cursor"], sequences[-1] if sequences else consumed
125                )
126                consumed = batch["cursor"]
127                if any(event["type"] == "turn_completed" for event in batch["events"]):
128                    finished = True
129                    break
130            self.assertTrue(finished, "watch closed without turn outcome")
131            response.close()
132            stream.close()
133
134            # Reconnect with the saved pair, never an SSE transport ID.
135            reconnect = http.client.HTTPConnection(
136                address.hostname, address.port, timeout=20
137            )
138            self.addCleanup(reconnect.close)
139            query = urllib.parse.urlencode(
140                {"after_generation": cursor["generation"], "after_seq": consumed}
141            )
142            reconnect.request(
143                "GET",
144                resource + "?" + query,
145                headers={**headers, "Accept": "text/event-stream"},
146            )
147            resumed = reconnect.getresponse()
148            for line in resumed:
149                if line.startswith(b"data: "):
150                    self.assertLessEqual(len(line[6:].rstrip(b"\r\n")), 1048576)
151                    batch = json.loads(line[6:])
152                    self.assertEqual(batch["generation"], cursor["generation"])
153                    self.assertFalse(
154                        any(event["type"] == "reset" for event in batch["events"])
155                    )
156                    self.assertTrue(
157                        all(event["sequence"] > consumed for event in batch["events"])
158                    )
159                    break
160            else:
161                self.fail("reconnect closed without a batch")
162            resumed.close()
163            reconnect.close()
164
165            connection.request("GET", resource + "/history", headers=headers)
166            history = self.read_json(connection.getresponse())
167            users = [entry for entry in history["items"] if entry["kind"] == "user"]
168            self.assertEqual([entry["input_id"] for entry in users], [input_id])
169            self.assertEqual(
170                [
171                    part["text"]
172                    for entry in users
173                    for part in entry["content"]
174                    if part["kind"] == "text"
175                ],
176                [message["text"]],
177            )
178            self.assertIn(
179                "ordinary HTTP answer",
180                [
181                    part["text"]
182                    for entry in history["items"]
183                    for part in entry["content"]
184                    if part["kind"] == "text"
185                ],
186            )
187            connection.request("GET", input_resource, headers=headers)
188            outcome = self.read_json(connection.getresponse())
189            self.assertEqual(outcome["delivery"], "committed")
190            self.assertEqual(outcome["turn"]["state"], "completed")
191            self.assertEqual(len(provider.requests), 1)