relay-codex 0.0.0-stage → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,314 @@
1
+ """Offline tests: no real accounts, Keychain access, or model calls."""
2
+
3
+ import asyncio
4
+ import json
5
+ from pathlib import Path
6
+ import sys
7
+ import unittest
8
+
9
+ from router import (
10
+ Controller,
11
+ RPC,
12
+ RemoteError,
13
+ RouterError,
14
+ account_home,
15
+ limited,
16
+ transport_guard,
17
+ )
18
+
19
+
20
+ class FakeAccount:
21
+ def __init__(self, label, blocked=False, error=False):
22
+ self.label = label
23
+ self.blocked = blocked
24
+ self.error = error
25
+ self.checks = 0
26
+
27
+ async def usage(self, force=False):
28
+ self.checks += 1
29
+ if self.error:
30
+ raise RouterError("unavailable")
31
+ return {"ordinaryUsageAllowed": not self.blocked}
32
+
33
+ async def credentials(self, refresh=False):
34
+ return {"accessToken": "FAKE_NOT_A_CREDENTIAL", "chatgptAccountId": self.label}
35
+
36
+
37
+ class FakeBackend:
38
+ def __init__(self):
39
+ self.calls = []
40
+ self.sent = []
41
+
42
+ async def call(self, method, params=None, **kwargs):
43
+ self.calls.append((method, params))
44
+ if method == "account/read":
45
+ return {"account": {"type": "chatgpt"}}
46
+ return {"ok": True}
47
+
48
+ async def send(self, message):
49
+ self.sent.append(message)
50
+
51
+
52
+ class FakeClient:
53
+ def __init__(self):
54
+ self.messages = []
55
+
56
+ async def send(self, raw):
57
+ self.messages.append(json.loads(raw))
58
+
59
+
60
+ def completed(
61
+ code="usageLimitExceeded", status="failed", thread="thread-1", turn="turn-1"
62
+ ):
63
+ return {
64
+ "method": "turn/completed",
65
+ "params": {
66
+ "threadId": thread,
67
+ "turn": {"id": turn, "status": status, "error": {"codexErrorInfo": code}},
68
+ },
69
+ }
70
+
71
+
72
+ class ControllerTests(unittest.IsolatedAsyncioTestCase):
73
+ async def asyncSetUp(self):
74
+ self.backend = FakeBackend()
75
+ self.accounts = [FakeAccount("one"), FakeAccount("two"), FakeAccount("three")]
76
+ self.reports = []
77
+ self.c = Controller(
78
+ self.accounts, self.backend, self.reports.append, interval=0.02
79
+ )
80
+ self.c.client = FakeClient()
81
+ self.c.last_turn["thread-1"] = {
82
+ "threadId": "thread-1",
83
+ "model": "chosen-model",
84
+ "input": [{"text": "UNIQUE_ORIGINAL_USER_REQUEST"}],
85
+ }
86
+
87
+ async def asyncTearDown(self):
88
+ if self.c.recovery:
89
+ self.c.recovery.cancel()
90
+ await asyncio.gather(self.c.recovery, return_exceptions=True)
91
+
92
+ async def test_quota_switch_and_continue_exact_thread_without_replay(self):
93
+ await self.c.event(completed())
94
+ await asyncio.wait_for(self.c.recovery, 1)
95
+ self.assertEqual(self.c.index, 1)
96
+ self.assertEqual(
97
+ [name for name, _ in self.backend.calls],
98
+ ["account/login/start", "account/read", "turn/start"],
99
+ )
100
+ params = self.backend.calls[-1][1]
101
+ self.assertEqual(params["threadId"], "thread-1")
102
+ self.assertNotIn("UNIQUE_ORIGINAL_USER_REQUEST", params["input"][0]["text"])
103
+ self.assertNotIn("approvalPolicy", params) # Retain existing thread policy.
104
+ self.assertNotIn(
105
+ "model", params
106
+ ) # Retain the sticky model; no silent downgrade.
107
+
108
+ async def test_duplicates_do_not_create_two_turns(self):
109
+ message = completed()
110
+ await self.c.event(message)
111
+ await self.c.event(message)
112
+ await self.c.recovery
113
+ await self.c.event(message)
114
+ self.assertEqual(sum(name == "turn/start" for name, _ in self.backend.calls), 1)
115
+
116
+ async def test_policy_errors_and_success_never_rotate(self):
117
+ for code in [
118
+ "cyberPolicy",
119
+ "misalignmentPolicyViolation",
120
+ "contextWindowExceeded",
121
+ "badRequest",
122
+ ]:
123
+ await self.c.event(completed(code=code, turn=code))
124
+ await self.c.event(completed(status="completed"))
125
+ await self.c.event(completed(status="interrupted"))
126
+ self.assertIsNone(self.c.recovery)
127
+ self.assertEqual(self.backend.calls, [])
128
+
129
+ async def test_untracked_thread_never_resubmitted(self):
130
+ await self.c.event(completed(thread="subagent"))
131
+ self.assertIsNone(self.c.recovery)
132
+
133
+ async def test_skips_blocked_and_failed_accounts(self):
134
+ self.accounts[1].blocked = True
135
+ await self.c.event(completed())
136
+ await self.c.recovery
137
+ self.assertEqual(self.c.index, 2)
138
+
139
+ async def test_pending_approval_prevents_switch(self):
140
+ self.c.server_requests.add(991)
141
+ await self.c.event(completed())
142
+ await asyncio.sleep(0.03)
143
+ self.assertEqual(self.backend.calls, [])
144
+ await self.c.forward({"id": 991, "result": {"decision": "accept"}}, {})
145
+ self.assertEqual(self.backend.sent[-1]["id"], 991)
146
+ self.c.recovery.cancel()
147
+
148
+ async def test_running_subtask_prevents_switch(self):
149
+ self.c.active_threads.add("still-running")
150
+ await self.c.event(completed())
151
+ await asyncio.sleep(0.03)
152
+ self.assertEqual(self.backend.calls, [])
153
+
154
+ async def test_resolved_server_request_does_not_leave_stale_approval_wait(self):
155
+ self.c.server_requests.add(991)
156
+ await self.c.event(
157
+ {
158
+ "method": "serverRequest/resolved",
159
+ "params": {"requestId": 991, "threadId": "thread-1"},
160
+ }
161
+ )
162
+ self.assertFalse(self.c.server_requests)
163
+ await self.c.event(completed())
164
+ await self.c.recovery
165
+ self.assertEqual(self.c.index, 1)
166
+
167
+ async def test_user_interrupt_cancels_wait(self):
168
+ self.accounts[1].blocked = self.accounts[2].blocked = True
169
+ await self.c.event(completed())
170
+ await asyncio.sleep(0.005)
171
+ await self.c.forward(
172
+ {"id": 7, "method": "turn/interrupt", "params": {"threadId": "thread-1"}},
173
+ {},
174
+ )
175
+ await asyncio.sleep(0.005)
176
+ self.assertTrue(self.c.recovery.cancelled())
177
+ self.assertFalse(
178
+ any(name == "account/login/start" for name, _ in self.backend.calls)
179
+ )
180
+
181
+ async def test_no_busy_loop_when_all_accounts_exhausted(self):
182
+ for account in self.accounts:
183
+ account.blocked = True
184
+ await self.c.event(completed())
185
+ await asyncio.sleep(0.055)
186
+ self.assertLessEqual(sum(account.checks for account in self.accounts), 9)
187
+ self.assertEqual(self.backend.calls, [])
188
+
189
+ async def test_refresh_never_returns_different_account(self):
190
+ await self.c.event(
191
+ {
192
+ "id": 88,
193
+ "method": "account/chatgptAuthTokens/refresh",
194
+ "params": {"previousAccountId": "different"},
195
+ }
196
+ )
197
+ self.assertIn("error", self.backend.sent[-1])
198
+ self.assertFalse(self.c.client.messages)
199
+
200
+ async def test_refresh_credentials_not_forwarded_to_frontend(self):
201
+ await self.c.event(
202
+ {
203
+ "id": 88,
204
+ "method": "account/chatgptAuthTokens/refresh",
205
+ "params": {"previousAccountId": "one"},
206
+ }
207
+ )
208
+ self.assertIn("result", self.backend.sent[-1])
209
+ self.assertFalse(self.c.client.messages)
210
+
211
+ async def test_pool_auth_cannot_be_replaced_by_frontend_logout(self):
212
+ await self.c.forward({"id": 1, "method": "account/logout"}, {})
213
+ self.assertEqual(self.backend.calls, [])
214
+ self.assertIn("error", self.c.client.messages[-1])
215
+
216
+
217
+ class PolicyTests(unittest.TestCase):
218
+ def test_percentage_alone_is_not_a_block(self):
219
+ self.assertFalse(limited({"rateLimits": {"primary": {"usedPercent": 100}}}))
220
+ self.assertTrue(limited({"ordinaryUsageAllowed": False}))
221
+ self.assertTrue(
222
+ limited({"rateLimits": {"rateLimitReachedType": "rate_limit_reached"}})
223
+ )
224
+
225
+ def test_other_model_bucket_is_not_a_global_block(self):
226
+ value = {
227
+ "rateLimitsByLimitId": {
228
+ "other": {
229
+ "normalModelSlug": "other-model",
230
+ "rateLimitReachedType": "rate_limit_reached",
231
+ }
232
+ }
233
+ }
234
+ self.assertFalse(limited(value, "chosen-model"))
235
+ self.assertTrue(limited(value, "other-model"))
236
+
237
+ def test_label_cannot_escape_account_root(self):
238
+ for label in ["../bad", "/tmp/bad", "a/b", "", ".", "a\n"]:
239
+ with self.assertRaises(RouterError):
240
+ account_home(Path("/private/test"), label)
241
+
242
+
243
+ class RPCTests(unittest.IsolatedAsyncioTestCase):
244
+ async def test_multiplexing_errors_and_disconnect(self):
245
+ program = """
246
+ import json,sys
247
+ for raw in sys.stdin:
248
+ d=json.loads(raw)
249
+ if d['method']=='quit': break
250
+ if d['method']=='bad':
251
+ reply={'id':d['id'],'error':{'code':-32602,'message':'specific client error'}}
252
+ else: reply={'id':d['id'],'result':{'echo':d['params']}}
253
+ print(json.dumps(reply),flush=True)
254
+ """
255
+ proc = await asyncio.create_subprocess_exec(
256
+ sys.executable,
257
+ "-u",
258
+ "-c",
259
+ program,
260
+ stdin=asyncio.subprocess.PIPE,
261
+ stdout=asyncio.subprocess.PIPE,
262
+ )
263
+ rpc = RPC(proc)
264
+ try:
265
+ a, b = await asyncio.gather(
266
+ rpc.call("echo", {"a": 1}), rpc.call("echo", {"b": 2})
267
+ )
268
+ self.assertEqual(a, {"echo": {"a": 1}})
269
+ self.assertEqual(b, {"echo": {"b": 2}})
270
+ with self.assertRaises(RemoteError) as ctx:
271
+ await rpc.call("bad", {})
272
+ self.assertEqual(ctx.exception.payload["code"], -32602)
273
+ with self.assertRaises(RouterError):
274
+ await rpc.call("quit", {}, timeout=1)
275
+ finally:
276
+ await rpc.close()
277
+
278
+
279
+ class TransportTests(unittest.IsolatedAsyncioTestCase):
280
+ async def test_loopback_transport_requires_bearer_and_rejects_browser_origins(self):
281
+ from websockets.asyncio.server import serve
282
+ from websockets.asyncio.client import connect
283
+ from websockets.exceptions import InvalidStatus
284
+
285
+ async def echo(ws):
286
+ async for message in ws:
287
+ await ws.send(message)
288
+
289
+ async with serve(
290
+ echo, "127.0.0.1", 0, process_request=transport_guard("test-token")
291
+ ) as server:
292
+ url = f"ws://127.0.0.1:{server.sockets[0].getsockname()[1]}"
293
+ for headers in [
294
+ {},
295
+ {"Authorization": "Bearer wrong"},
296
+ {
297
+ "Authorization": "Bearer test-token",
298
+ "Origin": "https://example.invalid",
299
+ },
300
+ ]:
301
+ with self.assertRaises(InvalidStatus):
302
+ async with connect(url, additional_headers=headers, proxy=None):
303
+ pass
304
+ async with connect(
305
+ url,
306
+ additional_headers={"Authorization": "Bearer test-token"},
307
+ proxy=None,
308
+ ) as ws:
309
+ await ws.send("test")
310
+ self.assertEqual(await ws.recv(), "test")
311
+
312
+
313
+ if __name__ == "__main__":
314
+ unittest.main()