relay-codex 0.7.2 → 0.7.4
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.
- package/docs/publishing.md +2 -2
- package/docs/troubleshooting.md +18 -0
- package/package.json +1 -1
- package/python/broker.py +5 -0
- package/python/commands.py +0 -2
- package/python/execution.py +1 -1
- package/python/pool.py +7 -0
- package/python/router.py +299 -26
- package/python/service.py +14 -4
- package/python/terminal.py +28 -0
- package/tests/fake_native.py +54 -0
- package/tests/test_activity.py +318 -0
- package/tests/test_audit.py +1 -0
- package/tests/test_recovery_boundary.py +422 -0
package/python/service.py
CHANGED
|
@@ -246,6 +246,9 @@ async def attach(
|
|
|
246
246
|
await send_control(state, ident, "detach")
|
|
247
247
|
value = json.loads(meta.read_text())
|
|
248
248
|
env["RELAY_TRANSPORT_TOKEN"] = value["token"]
|
|
249
|
+
from terminal import TerminalState
|
|
250
|
+
|
|
251
|
+
terminal_state = TerminalState()
|
|
249
252
|
proc = await asyncio.create_subprocess_exec(
|
|
250
253
|
binary,
|
|
251
254
|
"--remote",
|
|
@@ -295,6 +298,7 @@ async def attach(
|
|
|
295
298
|
proc.kill()
|
|
296
299
|
await proc.wait()
|
|
297
300
|
await asyncio.gather(waiter, return_exceptions=True)
|
|
301
|
+
terminal_state.restore()
|
|
298
302
|
if replacement:
|
|
299
303
|
print("Reconnecting to the recovered task…", flush=True)
|
|
300
304
|
value = await connection(previous=value)
|
|
@@ -372,7 +376,12 @@ def restart(state, prefix):
|
|
|
372
376
|
spec = json.loads(path.read_text())
|
|
373
377
|
if not Path(spec["cwd"]).is_dir():
|
|
374
378
|
raise RouterError("The project folder no longer exists")
|
|
375
|
-
spec.update(
|
|
379
|
+
spec.update(
|
|
380
|
+
state="starting",
|
|
381
|
+
background_launch=True,
|
|
382
|
+
foreground_owner=None,
|
|
383
|
+
relay_runtime=str(Path(__file__).parent.resolve()),
|
|
384
|
+
)
|
|
376
385
|
spec.pop("last_error", None)
|
|
377
386
|
atomic_json(path, spec)
|
|
378
387
|
process = spawn(
|
|
@@ -530,7 +539,7 @@ async def _serve(state, ident, binary):
|
|
|
530
539
|
controller.retry_interval = controller.preferences["retry_interval"]
|
|
531
540
|
controller.retry_attempts = controller.preferences["retry_attempts"]
|
|
532
541
|
controller.interval = controller.preferences["unknown_reset_interval"]
|
|
533
|
-
backend.handler = controller.event
|
|
542
|
+
backend.handler = lambda message: controller.event(message, origin=backend)
|
|
534
543
|
heartbeat = asyncio.create_task(controller.maintain())
|
|
535
544
|
connection = root / (ident + ".connection.json")
|
|
536
545
|
try:
|
|
@@ -590,7 +599,7 @@ async def _serve(state, ident, binary):
|
|
|
590
599
|
controller.backend_factory = factory
|
|
591
600
|
thread_id = old_thread or spec.get("resume")
|
|
592
601
|
if thread_id:
|
|
593
|
-
params = {"threadId": thread_id, **overrides}
|
|
602
|
+
params = {"threadId": thread_id, "excludeTurns": True, **overrides}
|
|
594
603
|
if old:
|
|
595
604
|
controller.merge_settings(thread_id, persisted.get("settings", {}))
|
|
596
605
|
params.update(
|
|
@@ -713,7 +722,7 @@ async def _serve(state, ident, binary):
|
|
|
713
722
|
if not persisted.get("accepted")
|
|
714
723
|
else None,
|
|
715
724
|
)
|
|
716
|
-
if state_name == "completed":
|
|
725
|
+
if state_name == "completed" and not persisted.get("rejected"):
|
|
717
726
|
controller.persist(
|
|
718
727
|
thread_id,
|
|
719
728
|
state="completed",
|
|
@@ -725,6 +734,7 @@ async def _serve(state, ident, binary):
|
|
|
725
734
|
)
|
|
726
735
|
elif (
|
|
727
736
|
persisted.get("review_required")
|
|
737
|
+
or persisted.get("rejected")
|
|
728
738
|
or persisted.get("active_items")
|
|
729
739
|
or not persisted.get("accepted")
|
|
730
740
|
):
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Restore terminal modes even when the native frontend cannot clean up."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import termios
|
|
7
|
+
|
|
8
|
+
RESET_MODES = b"\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l\x1b[?2004l\x1b[?25h\x1b[0m"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TerminalState:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.input_fd = None
|
|
14
|
+
self.output_fd = None
|
|
15
|
+
self.attributes = None
|
|
16
|
+
with contextlib.suppress(OSError, ValueError, AttributeError, termios.error):
|
|
17
|
+
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
18
|
+
self.input_fd = sys.stdin.fileno()
|
|
19
|
+
self.output_fd = sys.stdout.fileno()
|
|
20
|
+
self.attributes = termios.tcgetattr(self.input_fd)
|
|
21
|
+
|
|
22
|
+
def restore(self):
|
|
23
|
+
if self.attributes is None:
|
|
24
|
+
return
|
|
25
|
+
with contextlib.suppress(OSError, termios.error):
|
|
26
|
+
os.write(self.output_fd, RESET_MODES)
|
|
27
|
+
termios.tcsetattr(self.input_fd, termios.TCSANOW, self.attributes)
|
|
28
|
+
self.attributes = None
|
package/tests/fake_native.py
CHANGED
|
@@ -55,6 +55,9 @@ def finish(thread, turn, text, mode):
|
|
|
55
55
|
value = data["threads"][thread]
|
|
56
56
|
value["status"] = {"type": "idle"}
|
|
57
57
|
value["turns"][-1].update(status=status, error=error)
|
|
58
|
+
for item in value["turns"][-1]["items"]:
|
|
59
|
+
if item.get("id", "").startswith("lost-completion-"):
|
|
60
|
+
item["status"] = "completed"
|
|
58
61
|
return value["turns"][-1].copy()
|
|
59
62
|
|
|
60
63
|
value = change(apply)
|
|
@@ -125,6 +128,12 @@ for raw in sys.stdin:
|
|
|
125
128
|
result = {"account": {"type": "chatgpt", "planType": "plus"}}
|
|
126
129
|
elif method == "account/login/start":
|
|
127
130
|
result = {"type": params["type"]}
|
|
131
|
+
send(
|
|
132
|
+
{
|
|
133
|
+
"method": "account/updated",
|
|
134
|
+
"params": {"authMode": "chatgptAuthTokens", "planType": "plus"},
|
|
135
|
+
}
|
|
136
|
+
)
|
|
128
137
|
elif method == "account/rateLimits/read":
|
|
129
138
|
reset_count = change(lambda d: d.get("reset_credit_count", 1))
|
|
130
139
|
weekly = change(
|
|
@@ -208,8 +217,16 @@ for raw in sys.stdin:
|
|
|
208
217
|
]
|
|
209
218
|
)
|
|
210
219
|
}
|
|
220
|
+
for value in result["data"]:
|
|
221
|
+
value["itemsView"] = params.get("itemsView", "summary")
|
|
211
222
|
elif method == "thread/loaded/list":
|
|
212
223
|
result = {"data": change(lambda d: list(d["threads"]))}
|
|
224
|
+
elif method == "thread/backgroundTerminals/list":
|
|
225
|
+
result = {
|
|
226
|
+
"data": change(
|
|
227
|
+
lambda d: d.get("backgrounds", {}).get(params["threadId"], [])
|
|
228
|
+
)
|
|
229
|
+
}
|
|
213
230
|
elif method in {"turn/start", "review/start"}:
|
|
214
231
|
thread = params["threadId"]
|
|
215
232
|
turn = str(uuid.uuid4())
|
|
@@ -243,6 +260,43 @@ for raw in sys.stdin:
|
|
|
243
260
|
"params": {"threadId": thread, "turn": value},
|
|
244
261
|
}
|
|
245
262
|
)
|
|
263
|
+
if count <= fault.get("lost_completion_turns", 0):
|
|
264
|
+
tool = {
|
|
265
|
+
"id": "lost-completion-" + turn,
|
|
266
|
+
"type": "commandExecution",
|
|
267
|
+
"status": "inProgress",
|
|
268
|
+
}
|
|
269
|
+
change(
|
|
270
|
+
lambda d: d["threads"][thread]["turns"][-1]["items"].append(tool)
|
|
271
|
+
)
|
|
272
|
+
send(
|
|
273
|
+
{
|
|
274
|
+
"method": "item/started",
|
|
275
|
+
"params": {"threadId": thread, "item": tool},
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
if count <= fault.get("background_activity_turns", 0):
|
|
279
|
+
item_id = "background-" + turn
|
|
280
|
+
change(
|
|
281
|
+
lambda d: (
|
|
282
|
+
d.setdefault("backgrounds", {})
|
|
283
|
+
.setdefault(thread, [])
|
|
284
|
+
.append({"itemId": item_id, "processId": "test-background-job"})
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
send(
|
|
288
|
+
{
|
|
289
|
+
"method": "item/started",
|
|
290
|
+
"params": {
|
|
291
|
+
"threadId": thread,
|
|
292
|
+
"item": {
|
|
293
|
+
"id": item_id,
|
|
294
|
+
"type": "commandExecution",
|
|
295
|
+
"status": "inProgress",
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
)
|
|
246
300
|
if fault.get("external"):
|
|
247
301
|
send(
|
|
248
302
|
{
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""A lost item completion must not strand recovery or clear unknown work."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import pty
|
|
8
|
+
import termios
|
|
9
|
+
import tempfile
|
|
10
|
+
import tty
|
|
11
|
+
import unittest
|
|
12
|
+
from unittest.mock import patch
|
|
13
|
+
|
|
14
|
+
from router import Controller, RPC, RouterError
|
|
15
|
+
from test_router import FakeAccount, FakeBackend, FakeClient, completed
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ActivityTests(unittest.IsolatedAsyncioTestCase):
|
|
19
|
+
def test_forced_frontend_exit_restores_terminal_and_disables_mouse_reporting(self):
|
|
20
|
+
from terminal import TerminalState, RESET_MODES
|
|
21
|
+
|
|
22
|
+
master, slave = pty.openpty()
|
|
23
|
+
|
|
24
|
+
class Stream:
|
|
25
|
+
def isatty(self):
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
def fileno(self):
|
|
29
|
+
return slave
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
original = termios.tcgetattr(slave)
|
|
33
|
+
with patch("sys.stdin", Stream()), patch("sys.stdout", Stream()):
|
|
34
|
+
state = TerminalState()
|
|
35
|
+
tty.setraw(slave)
|
|
36
|
+
state.restore()
|
|
37
|
+
state.restore()
|
|
38
|
+
restored = termios.tcgetattr(slave)
|
|
39
|
+
# macOS may set the transient PENDIN flag when canonical mode returns.
|
|
40
|
+
restored[3] &= ~getattr(termios, "PENDIN", 0)
|
|
41
|
+
original[3] &= ~getattr(termios, "PENDIN", 0)
|
|
42
|
+
self.assertEqual(restored, original)
|
|
43
|
+
self.assertEqual(os.read(master, 4096), RESET_MODES)
|
|
44
|
+
finally:
|
|
45
|
+
os.close(master)
|
|
46
|
+
os.close(slave)
|
|
47
|
+
|
|
48
|
+
def test_noninteractive_output_receives_no_terminal_escape_codes(self):
|
|
49
|
+
import io
|
|
50
|
+
from terminal import TerminalState
|
|
51
|
+
|
|
52
|
+
output = io.StringIO()
|
|
53
|
+
with patch("sys.stdin", io.StringIO()), patch("sys.stdout", output):
|
|
54
|
+
TerminalState().restore()
|
|
55
|
+
self.assertEqual(output.getvalue(), "")
|
|
56
|
+
|
|
57
|
+
async def test_restart_dispatch_uses_the_shared_file_lock(self):
|
|
58
|
+
import commands
|
|
59
|
+
from router import parse_arguments
|
|
60
|
+
|
|
61
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
62
|
+
with patch("service.restart", return_value="saved-session") as restart:
|
|
63
|
+
result = await commands.dispatch(
|
|
64
|
+
parse_arguments(["restart", "saved-session"]), Path(folder), None
|
|
65
|
+
)
|
|
66
|
+
self.assertEqual(result, 0)
|
|
67
|
+
restart.assert_called_once()
|
|
68
|
+
|
|
69
|
+
async def test_lost_completion_over_real_jsonl_recovers_without_backend_restart(
|
|
70
|
+
self,
|
|
71
|
+
):
|
|
72
|
+
from test_services import fake_executable, wait_for
|
|
73
|
+
from settings import atomic_json
|
|
74
|
+
|
|
75
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
76
|
+
root = Path(folder)
|
|
77
|
+
atomic_json(
|
|
78
|
+
root / "fault.json", {"quota_turns": 1, "lost_completion_turns": 1}
|
|
79
|
+
)
|
|
80
|
+
rpc = await RPC.start(fake_executable(root), root)
|
|
81
|
+
c = Controller(
|
|
82
|
+
[FakeAccount("one"), FakeAccount("two")], rpc, report=lambda _: None
|
|
83
|
+
)
|
|
84
|
+
c.client = FakeClient()
|
|
85
|
+
rpc.handler = c.event
|
|
86
|
+
try:
|
|
87
|
+
initial_pid = rpc.proc.pid
|
|
88
|
+
await rpc.initialize()
|
|
89
|
+
await c.forward(
|
|
90
|
+
{"id": "thread", "method": "thread/start", "params": {}}, {}
|
|
91
|
+
)
|
|
92
|
+
thread = next(iter(c.last_turn))
|
|
93
|
+
await c.forward(
|
|
94
|
+
{
|
|
95
|
+
"id": "turn",
|
|
96
|
+
"method": "turn/start",
|
|
97
|
+
"params": {
|
|
98
|
+
"threadId": thread,
|
|
99
|
+
"input": [
|
|
100
|
+
{
|
|
101
|
+
"type": "text",
|
|
102
|
+
"text": "fake work",
|
|
103
|
+
"text_elements": [],
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{},
|
|
109
|
+
)
|
|
110
|
+
history = root / "fake-history.json"
|
|
111
|
+
|
|
112
|
+
def recovered():
|
|
113
|
+
turns = json.loads(history.read_text())["threads"][thread]["turns"]
|
|
114
|
+
return len(turns) == 2 and turns[-1]["status"] == "completed"
|
|
115
|
+
|
|
116
|
+
await wait_for(recovered)
|
|
117
|
+
self.assertEqual(c.account.label, "two")
|
|
118
|
+
self.assertFalse(c.active_items)
|
|
119
|
+
self.assertEqual(rpc.proc.pid, initial_pid)
|
|
120
|
+
self.assertIsNone(rpc.proc.returncode)
|
|
121
|
+
finally:
|
|
122
|
+
if c.recovery:
|
|
123
|
+
await asyncio.gather(c.recovery, return_exceptions=True)
|
|
124
|
+
await rpc.close()
|
|
125
|
+
|
|
126
|
+
def controller(self):
|
|
127
|
+
c = Controller(
|
|
128
|
+
[FakeAccount("one"), FakeAccount("two")],
|
|
129
|
+
FakeBackend(),
|
|
130
|
+
report=lambda _: None,
|
|
131
|
+
)
|
|
132
|
+
c.client = FakeClient()
|
|
133
|
+
c.last_turn["thread-1"] = {"threadId": "thread-1"}
|
|
134
|
+
c.preferences["watchdog_interval"] = 0.01
|
|
135
|
+
return c
|
|
136
|
+
|
|
137
|
+
async def started(self, c, ident="tool", thread="thread-1"):
|
|
138
|
+
await c.event(
|
|
139
|
+
{
|
|
140
|
+
"method": "item/started",
|
|
141
|
+
"params": {
|
|
142
|
+
"threadId": thread,
|
|
143
|
+
"item": {
|
|
144
|
+
"id": ident,
|
|
145
|
+
"type": "commandExecution",
|
|
146
|
+
"status": "inProgress",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def history(self, c, status="completed", view="full", ident="tool"):
|
|
153
|
+
original = c.backend.call
|
|
154
|
+
|
|
155
|
+
async def call(method, params=None, **kw):
|
|
156
|
+
if method == "thread/turns/list" and params.get("itemsView") == "full":
|
|
157
|
+
return {
|
|
158
|
+
"data": [
|
|
159
|
+
{
|
|
160
|
+
"itemsView": view,
|
|
161
|
+
"items": [
|
|
162
|
+
{
|
|
163
|
+
"id": ident,
|
|
164
|
+
"type": "commandExecution",
|
|
165
|
+
"status": status,
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
return await original(method, params, **kw)
|
|
172
|
+
|
|
173
|
+
c.backend.call = call
|
|
174
|
+
|
|
175
|
+
async def test_quota_after_lost_completion_switches_and_continues_same_thread(self):
|
|
176
|
+
c = self.controller()
|
|
177
|
+
await self.started(c)
|
|
178
|
+
self.history(c)
|
|
179
|
+
await c.event(completed())
|
|
180
|
+
await asyncio.wait_for(c.recovery, 1)
|
|
181
|
+
self.assertFalse(c.active_items)
|
|
182
|
+
self.assertEqual(c.account.label, "two")
|
|
183
|
+
turns = [p for m, p in c.backend.calls if m == "turn/start"]
|
|
184
|
+
self.assertEqual(len(turns), 1)
|
|
185
|
+
self.assertEqual(turns[0]["threadId"], "thread-1")
|
|
186
|
+
|
|
187
|
+
async def test_running_missing_and_summary_items_remain_blocking(self):
|
|
188
|
+
for status, view, ident in (
|
|
189
|
+
("inProgress", "full", "tool"),
|
|
190
|
+
("completed", "summary", "tool"),
|
|
191
|
+
("completed", "full", "different"),
|
|
192
|
+
(None, "full", "tool"),
|
|
193
|
+
):
|
|
194
|
+
c = self.controller()
|
|
195
|
+
await self.started(c)
|
|
196
|
+
self.history(c, status, view, ident)
|
|
197
|
+
await c.reconcile_active_items()
|
|
198
|
+
self.assertEqual(c.active_items, {"tool"})
|
|
199
|
+
|
|
200
|
+
async def test_finished_tool_does_not_clear_pending_approval_or_child_thread(self):
|
|
201
|
+
c = self.controller()
|
|
202
|
+
await self.started(c)
|
|
203
|
+
c.server_requests.add(7)
|
|
204
|
+
c.active_threads.add("child")
|
|
205
|
+
self.history(c)
|
|
206
|
+
await c.reconcile_active_items()
|
|
207
|
+
self.assertFalse(c.active_items)
|
|
208
|
+
self.assertEqual(c.server_requests, {7})
|
|
209
|
+
self.assertEqual(c.active_threads, {"child"})
|
|
210
|
+
self.assertFalse(any(m == "account/login/start" for m, _ in c.backend.calls))
|
|
211
|
+
|
|
212
|
+
async def test_new_start_during_history_read_invalidates_the_read(self):
|
|
213
|
+
c = self.controller()
|
|
214
|
+
await self.started(c)
|
|
215
|
+
|
|
216
|
+
async def call(method, params=None, **kw):
|
|
217
|
+
await self.started(c, "new")
|
|
218
|
+
return {
|
|
219
|
+
"data": [
|
|
220
|
+
{
|
|
221
|
+
"itemsView": "full",
|
|
222
|
+
"items": [
|
|
223
|
+
{
|
|
224
|
+
"id": "tool",
|
|
225
|
+
"type": "commandExecution",
|
|
226
|
+
"status": "completed",
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
c.backend.call = call
|
|
234
|
+
await c.reconcile_active_items()
|
|
235
|
+
self.assertEqual(c.active_items, {"tool", "new"})
|
|
236
|
+
|
|
237
|
+
async def test_full_history_pages_until_matching_item_and_stops_on_repeated_cursor(
|
|
238
|
+
self,
|
|
239
|
+
):
|
|
240
|
+
c = self.controller()
|
|
241
|
+
await self.started(c)
|
|
242
|
+
calls = []
|
|
243
|
+
|
|
244
|
+
async def call(method, params=None, **kw):
|
|
245
|
+
calls.append(params)
|
|
246
|
+
if not params.get("cursor"):
|
|
247
|
+
return {"data": [], "nextCursor": "older"}
|
|
248
|
+
return {
|
|
249
|
+
"data": [
|
|
250
|
+
{
|
|
251
|
+
"itemsView": "full",
|
|
252
|
+
"items": [
|
|
253
|
+
{
|
|
254
|
+
"id": "tool",
|
|
255
|
+
"type": "commandExecution",
|
|
256
|
+
"status": "failed",
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
}
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
c.backend.call = call
|
|
264
|
+
await c.reconcile_active_items()
|
|
265
|
+
self.assertFalse(c.active_items)
|
|
266
|
+
self.assertEqual(len(calls), 2)
|
|
267
|
+
await self.started(c)
|
|
268
|
+
calls.clear()
|
|
269
|
+
|
|
270
|
+
async def repeated(method, params=None, **kw):
|
|
271
|
+
calls.append(params)
|
|
272
|
+
return {"data": [], "nextCursor": "same"}
|
|
273
|
+
|
|
274
|
+
c.backend.call = repeated
|
|
275
|
+
await c.reconcile_active_items()
|
|
276
|
+
self.assertEqual(len(calls), 2)
|
|
277
|
+
self.assertEqual(c.active_items, {"tool"})
|
|
278
|
+
|
|
279
|
+
async def test_failed_read_and_unowned_item_never_clear_activity(self):
|
|
280
|
+
c = self.controller()
|
|
281
|
+
await self.started(c)
|
|
282
|
+
c.active_items.add("unknown-owner")
|
|
283
|
+
|
|
284
|
+
async def failed(*args, **kwargs):
|
|
285
|
+
raise RouterError("unavailable")
|
|
286
|
+
|
|
287
|
+
c.backend.call = failed
|
|
288
|
+
await c.reconcile_active_items()
|
|
289
|
+
self.assertEqual(c.active_items, {"tool", "unknown-owner"})
|
|
290
|
+
|
|
291
|
+
async def test_completion_and_terminal_start_do_not_leave_stale_ownership(self):
|
|
292
|
+
c = self.controller()
|
|
293
|
+
await self.started(c)
|
|
294
|
+
await c.event(
|
|
295
|
+
{
|
|
296
|
+
"method": "item/completed",
|
|
297
|
+
"params": {"threadId": "thread-1", "item": {"id": "tool"}},
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
self.assertFalse(c.active_item_threads)
|
|
301
|
+
await c.event(
|
|
302
|
+
{
|
|
303
|
+
"method": "item/started",
|
|
304
|
+
"params": {
|
|
305
|
+
"threadId": "thread-1",
|
|
306
|
+
"item": {
|
|
307
|
+
"id": "already-done",
|
|
308
|
+
"type": "commandExecution",
|
|
309
|
+
"status": "completed",
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
self.assertFalse(c.active_items)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if __name__ == "__main__":
|
|
318
|
+
unittest.main()
|
package/tests/test_audit.py
CHANGED
|
@@ -246,6 +246,7 @@ class WorkerRegressions(unittest.IsolatedAsyncioTestCase):
|
|
|
246
246
|
c.task = asyncio.create_task(service.serve(c.state, c.ident, c.binary))
|
|
247
247
|
await wait_for(c.meta.exists)
|
|
248
248
|
self.assertEqual(observed[0]["approvalsReviewer"], "auto_review")
|
|
249
|
+
self.assertTrue(observed[0]["excludeTurns"])
|
|
249
250
|
self.assertIn("/private/tmp/audit-extra", observed[0]["runtimeWorkspaceRoots"])
|
|
250
251
|
pool.close()
|
|
251
252
|
|