harness-sdk-python 0.4.1__tar.gz → 0.4.2__tar.gz
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.
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/PKG-INFO +1 -1
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/pyproject.toml +1 -1
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/src/harness_sdk/run_manager.py +103 -32
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/run_helpers.py +5 -2
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_batches.py +2 -1
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_caller.py +8 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_edit_dispatched.py +5 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_edit_reload.py +4 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_enqueue.py +13 -1
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_facade.py +5 -3
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_input_required.py +10 -1
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_outcomes.py +11 -2
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_rewind_during_run.py +4 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_run_leaf.py +5 -0
- harness_sdk_python-0.4.2/tests/test_settle.py +218 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_steer.py +5 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_stop_continue.py +32 -5
- harness_sdk_python-0.4.1/tests/test_settle.py +0 -131
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/.gitignore +0 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/README.md +0 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.4.1 → harness_sdk_python-0.4.2}/tests/test_placement.py +0 -0
|
@@ -6,8 +6,14 @@ Implements the full state x command matrix from runs.mdx on a single
|
|
|
6
6
|
Commands stage entries into an intake list and schedule a drain; the drain
|
|
7
7
|
reduces the whole intake into the lanes in order (pure placement), then takes
|
|
8
8
|
exactly one action based on status and net effect (interrupt, dispatch, or
|
|
9
|
-
continue).
|
|
9
|
+
continue). A parked send settles at staging; run-starting commands settle at
|
|
10
|
+
the executor's ``ctx.ack_messages()``; ``run/stop`` awaits a future the drain
|
|
10
11
|
resolves once the in-flight run has ended.
|
|
12
|
+
|
|
13
|
+
Dispatched entries leave the queue projection but are retained until the
|
|
14
|
+
executor acks. A run that ends before the ack puts them back at the front of
|
|
15
|
+
their lane and settles their sends accepted; edit, reload, and continue
|
|
16
|
+
initiators settle rejected (``stopped`` or the failure).
|
|
11
17
|
"""
|
|
12
18
|
|
|
13
19
|
import asyncio
|
|
@@ -101,7 +107,7 @@ class _Stop:
|
|
|
101
107
|
|
|
102
108
|
@dataclass
|
|
103
109
|
class _Continue:
|
|
104
|
-
|
|
110
|
+
future: "asyncio.Future[Any]" = field(default_factory=_future)
|
|
105
111
|
|
|
106
112
|
|
|
107
113
|
@dataclass
|
|
@@ -126,6 +132,8 @@ class _Effects:
|
|
|
126
132
|
steer_added: bool = False
|
|
127
133
|
new_added: bool = False
|
|
128
134
|
continue_requested: bool = False
|
|
135
|
+
staged_sends: list[tuple[str, "asyncio.Future[Any]"]] = field(default_factory=list)
|
|
136
|
+
continues: list["asyncio.Future[Any]"] = field(default_factory=list)
|
|
129
137
|
|
|
130
138
|
|
|
131
139
|
class RunManager:
|
|
@@ -170,7 +178,10 @@ class RunManager:
|
|
|
170
178
|
self._staged_stops: list["asyncio.Future[Any]"] = []
|
|
171
179
|
self._stop_reason: str | None = None
|
|
172
180
|
self._staged_rewinds: list[_Rewind] = []
|
|
173
|
-
self.
|
|
181
|
+
self._dispatching: list[tuple[str, dict[str, Any]]] = []
|
|
182
|
+
self._run_acked = True
|
|
183
|
+
self._leaf_confirmed = leaf_message_id
|
|
184
|
+
self._send_futures: list["asyncio.Future[Any]"] = []
|
|
174
185
|
self._run_futures: list["asyncio.Future[Any]"] = []
|
|
175
186
|
self._input_requests: list[dict[str, Any]] = []
|
|
176
187
|
self._input_answers: dict[str, Any] = {}
|
|
@@ -203,6 +214,9 @@ class RunManager:
|
|
|
203
214
|
def _continue_type(self) -> str:
|
|
204
215
|
return "error-continue" if self._status() == "error" else "stop-continue"
|
|
205
216
|
|
|
217
|
+
def _is_dispatching(self, message_id: str) -> bool:
|
|
218
|
+
return any(item["id"] == message_id for _, item in self._dispatching)
|
|
219
|
+
|
|
206
220
|
# ─── Staging and drain ──────────────────────────────────
|
|
207
221
|
|
|
208
222
|
async def _stage(self, entry: Any) -> Any:
|
|
@@ -231,6 +245,8 @@ class RunManager:
|
|
|
231
245
|
rewind.messages,
|
|
232
246
|
rollback_to=rewind.rollback_to,
|
|
233
247
|
)
|
|
248
|
+
for message in rewind.messages:
|
|
249
|
+
self._callers.pop(message["id"], None)
|
|
234
250
|
rewind.ack()
|
|
235
251
|
if not rewind.future.done():
|
|
236
252
|
self._run_futures.append(rewind.future)
|
|
@@ -245,6 +261,15 @@ class RunManager:
|
|
|
245
261
|
if not rewind.acked:
|
|
246
262
|
rewind.ack()
|
|
247
263
|
rewind.acked = True
|
|
264
|
+
dispatching = {item["id"] for _, item in self._dispatching}
|
|
265
|
+
for message_id, future in fx.staged_sends:
|
|
266
|
+
if message_id in dispatching:
|
|
267
|
+
self._send_futures.append(future)
|
|
268
|
+
elif not future.done():
|
|
269
|
+
future.set_result(None)
|
|
270
|
+
for future in fx.continues:
|
|
271
|
+
if not future.done():
|
|
272
|
+
future.set_result(None)
|
|
248
273
|
|
|
249
274
|
def _apply(self, entry: Any, fx: _Effects) -> None:
|
|
250
275
|
if isinstance(entry, _Stop):
|
|
@@ -252,6 +277,7 @@ class RunManager:
|
|
|
252
277
|
return
|
|
253
278
|
if isinstance(entry, _Continue):
|
|
254
279
|
fx.continue_requested = True
|
|
280
|
+
fx.continues.append(entry.future)
|
|
255
281
|
return
|
|
256
282
|
try:
|
|
257
283
|
if isinstance(entry, _Send):
|
|
@@ -278,6 +304,8 @@ class RunManager:
|
|
|
278
304
|
elif status in ("error", "stopped"):
|
|
279
305
|
if fx.continue_requested or fx.steer_added:
|
|
280
306
|
self._dispatch(self._continue_type(), [])
|
|
307
|
+
self._run_futures.extend(fx.continues)
|
|
308
|
+
fx.continues.clear()
|
|
281
309
|
elif fx.new_added and pre_empty:
|
|
282
310
|
self._pop_dispatchable()
|
|
283
311
|
elif status == "input-required" and self._input_requests and len(
|
|
@@ -331,9 +359,6 @@ class RunManager:
|
|
|
331
359
|
messages = [
|
|
332
360
|
{k: v for k, v in message.items() if k != "caller"} for message in messages
|
|
333
361
|
]
|
|
334
|
-
for message in messages:
|
|
335
|
-
self._callers.pop(message["id"], None)
|
|
336
|
-
self._adopt_entities(message["id"] for message in messages)
|
|
337
362
|
record: dict[str, Any] = {"type": type, "messages": list(messages)}
|
|
338
363
|
if rollback_to is not _ABSENT:
|
|
339
364
|
record["rollbackTo"] = rollback_to
|
|
@@ -341,6 +366,8 @@ class RunManager:
|
|
|
341
366
|
record["responses"] = responses
|
|
342
367
|
self._dispatch_record = record
|
|
343
368
|
self._stop_reason = None
|
|
369
|
+
self._run_acked = False
|
|
370
|
+
self._leaf_confirmed = plain(self._state["runLeafMessageId"])
|
|
344
371
|
self._state["error"] = None
|
|
345
372
|
if messages:
|
|
346
373
|
self._dispatched_ids = tuple(m["id"] for m in messages)
|
|
@@ -377,23 +404,34 @@ class RunManager:
|
|
|
377
404
|
"start must return a RunManager outcome, got "
|
|
378
405
|
f"{type(outcome).__name__}"
|
|
379
406
|
)
|
|
407
|
+
if isinstance(
|
|
408
|
+
outcome, (RunManager.Complete, RunManager.InputRequired)
|
|
409
|
+
) and (self._dispatching or not self._run_acked):
|
|
410
|
+
raise RuntimeError(
|
|
411
|
+
"run settled without acking its messages"
|
|
412
|
+
" (call ctx.ack_messages())"
|
|
413
|
+
)
|
|
380
414
|
except Exception as exc:
|
|
381
415
|
self._settle(ctx)
|
|
382
416
|
message = str(exc) or type(exc).__name__
|
|
383
417
|
if isinstance(exc, StatewireReject):
|
|
384
418
|
self._freeze(exc.message, exc.payload)
|
|
385
|
-
self.
|
|
419
|
+
self._settle_initiators(exc)
|
|
386
420
|
else:
|
|
387
421
|
self._freeze(message)
|
|
388
|
-
self.
|
|
422
|
+
self._settle_initiators(_reject("run-error", message))
|
|
423
|
+
self._revert_dispatching()
|
|
389
424
|
self._drain()
|
|
390
425
|
return
|
|
391
426
|
self._settle(ctx)
|
|
392
|
-
|
|
393
|
-
_reject("run-error", "run ended in error")
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
427
|
+
if isinstance(outcome, RunManager.Error):
|
|
428
|
+
error = _reject("run-error", "run ended in error")
|
|
429
|
+
elif isinstance(outcome, RunManager.Stop):
|
|
430
|
+
error = _reject("stopped", "run stopped before the messages-ack")
|
|
431
|
+
else:
|
|
432
|
+
error = None
|
|
433
|
+
self._settle_initiators(error)
|
|
434
|
+
self._revert_dispatching()
|
|
397
435
|
self._outcome = outcome
|
|
398
436
|
self._drain()
|
|
399
437
|
|
|
@@ -403,7 +441,7 @@ class RunManager:
|
|
|
403
441
|
self._task = None
|
|
404
442
|
self._dispatch_record = None
|
|
405
443
|
|
|
406
|
-
def
|
|
444
|
+
def _settle_initiators(self, error: StatewireReject | None) -> None:
|
|
407
445
|
futures, self._run_futures = self._run_futures, []
|
|
408
446
|
for future in futures:
|
|
409
447
|
if future.done():
|
|
@@ -413,21 +451,48 @@ class RunManager:
|
|
|
413
451
|
else:
|
|
414
452
|
future.set_exception(error)
|
|
415
453
|
|
|
416
|
-
def
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if
|
|
420
|
-
|
|
454
|
+
def _settle_sends(self) -> None:
|
|
455
|
+
futures, self._send_futures = self._send_futures, []
|
|
456
|
+
for future in futures:
|
|
457
|
+
if not future.done():
|
|
458
|
+
future.set_result(None)
|
|
459
|
+
|
|
460
|
+
def _ack_messages(self) -> None:
|
|
461
|
+
taken, self._dispatching = self._dispatching, []
|
|
462
|
+
self._run_acked = True
|
|
463
|
+
for _, item in taken:
|
|
464
|
+
self._callers.pop(item["id"], None)
|
|
465
|
+
self._leaf_confirmed = plain(self._state["runLeafMessageId"])
|
|
466
|
+
self._settle_sends()
|
|
467
|
+
self._settle_initiators(None)
|
|
468
|
+
|
|
469
|
+
def _revert_dispatching(self) -> None:
|
|
470
|
+
self._settle_sends()
|
|
471
|
+
taken, self._dispatching = self._dispatching, []
|
|
472
|
+
if not taken:
|
|
473
|
+
return
|
|
474
|
+
for lane in ("steerQueue", "queue"):
|
|
475
|
+
front = [item for l, item in taken if l == lane]
|
|
476
|
+
if front:
|
|
477
|
+
self._state[lane] = front + self._lane_items(lane)
|
|
478
|
+
ids = {item["id"] for _, item in taken}
|
|
479
|
+
self._dispatched_ids = tuple(
|
|
480
|
+
id for id in self._dispatched_ids if id not in ids
|
|
481
|
+
)
|
|
482
|
+
if plain(self._state["runLeafMessageId"]) in ids:
|
|
483
|
+
self._state["runLeafMessageId"] = self._leaf_confirmed
|
|
421
484
|
|
|
422
485
|
def _pop_dispatchable(self) -> bool:
|
|
423
486
|
steer = self._lane_items("steerQueue")
|
|
424
487
|
if steer:
|
|
425
488
|
self._state["steerQueue"] = []
|
|
489
|
+
self._dispatching = [("steerQueue", item) for item in steer]
|
|
426
490
|
self._dispatch("message-send", steer)
|
|
427
491
|
return True
|
|
428
492
|
queue = self._lane_items("queue")
|
|
429
493
|
if queue:
|
|
430
494
|
self._state["queue"].pop(0)
|
|
495
|
+
self._dispatching = [("queue", queue[0])]
|
|
431
496
|
self._dispatch("message-send", [queue[0]])
|
|
432
497
|
return True
|
|
433
498
|
return False
|
|
@@ -619,7 +684,7 @@ class RunManager:
|
|
|
619
684
|
if e.lane == "steerQueue":
|
|
620
685
|
fx.steer_added = True
|
|
621
686
|
return None
|
|
622
|
-
if e.message_id in self._dispatched_ids:
|
|
687
|
+
if e.message_id in self._dispatched_ids or self._is_dispatching(e.message_id):
|
|
623
688
|
return self._park_dispatched_edit(e)
|
|
624
689
|
if e.meta is not None:
|
|
625
690
|
raise _reject(
|
|
@@ -630,7 +695,7 @@ class RunManager:
|
|
|
630
695
|
self._check_anchor(e)
|
|
631
696
|
with self._caller_stamp(e.message_id, e.caller):
|
|
632
697
|
self._insert_new(e.lane, e.message, e.params)
|
|
633
|
-
|
|
698
|
+
fx.staged_sends.append((e.message_id, e.future))
|
|
634
699
|
e.ack()
|
|
635
700
|
fx.new_added = True
|
|
636
701
|
if e.lane == "steerQueue":
|
|
@@ -640,7 +705,9 @@ class RunManager:
|
|
|
640
705
|
def _apply_move(self, e: _Send, fx: _Effects) -> Any:
|
|
641
706
|
current = self._lane_of(e.message_id)
|
|
642
707
|
if current is None:
|
|
643
|
-
if e.message_id in self._dispatched_ids
|
|
708
|
+
if e.message_id in self._dispatched_ids or self._is_dispatching(
|
|
709
|
+
e.message_id
|
|
710
|
+
):
|
|
644
711
|
if e.lane == "steerQueue":
|
|
645
712
|
return None
|
|
646
713
|
raise _reject(
|
|
@@ -685,11 +752,6 @@ class RunManager:
|
|
|
685
752
|
self._state[lane] = [
|
|
686
753
|
item for item in self._lane_items(lane) if item["id"] != e.message_id
|
|
687
754
|
]
|
|
688
|
-
entity = self._entity_futures.pop(e.message_id, None)
|
|
689
|
-
if entity is not None and not entity.done():
|
|
690
|
-
entity.set_exception(
|
|
691
|
-
_reject("removed", f"message {e.message_id} was removed from the queue")
|
|
692
|
-
)
|
|
693
755
|
return None
|
|
694
756
|
|
|
695
757
|
def _apply_input(self, e: _Input) -> Any:
|
|
@@ -972,7 +1034,7 @@ class RunManager:
|
|
|
972
1034
|
response = self._validated_response(request["type"], params["response"])
|
|
973
1035
|
return await self._stage(_Input(request_id, response))
|
|
974
1036
|
|
|
975
|
-
def continue_run(self) ->
|
|
1037
|
+
async def continue_run(self, *, ack: Callable[[], None]) -> Any:
|
|
976
1038
|
status = self._status()
|
|
977
1039
|
if status not in ("error", "stopped"):
|
|
978
1040
|
raise _reject("wrong-state", f"run/continue is rejected in {status}")
|
|
@@ -983,8 +1045,11 @@ class RunManager:
|
|
|
983
1045
|
"capability-missing",
|
|
984
1046
|
"bare continue requires the incomplete-continuation capability",
|
|
985
1047
|
)
|
|
986
|
-
|
|
1048
|
+
entry = _Continue()
|
|
1049
|
+
self._intake.append(entry)
|
|
1050
|
+
ack()
|
|
987
1051
|
self._schedule(self._drain)
|
|
1052
|
+
return await entry.future
|
|
988
1053
|
|
|
989
1054
|
# ─── Outcomes and context ───────────────────────────────
|
|
990
1055
|
|
|
@@ -1075,19 +1140,25 @@ class RunManager:
|
|
|
1075
1140
|
self._ensure_active()
|
|
1076
1141
|
items = self._manager._lane_items("steerQueue")
|
|
1077
1142
|
self._manager._state["steerQueue"] = []
|
|
1078
|
-
for item in items
|
|
1079
|
-
self._manager._callers.pop(item["id"], None)
|
|
1080
|
-
self._manager._adopt_entities(item["id"] for item in items)
|
|
1143
|
+
self._manager._dispatching.extend(("steerQueue", item) for item in items)
|
|
1081
1144
|
if items:
|
|
1082
1145
|
self._manager._state["runLeafMessageId"] = items[-1]["id"]
|
|
1083
1146
|
return tuple(
|
|
1084
1147
|
{k: v for k, v in item.items() if k != "caller"} for item in items
|
|
1085
1148
|
)
|
|
1086
1149
|
|
|
1150
|
+
def ack_messages(self) -> None:
|
|
1151
|
+
self._ensure_active()
|
|
1152
|
+
self._manager._ack_messages()
|
|
1153
|
+
|
|
1087
1154
|
def set_leaf_message_id(self, message_id: str) -> None:
|
|
1088
1155
|
self._ensure_active()
|
|
1089
1156
|
if not isinstance(message_id, str) or message_id == "":
|
|
1090
1157
|
raise ValueError("message_id must be a non-empty string")
|
|
1158
|
+
if self._manager._dispatching or not self._manager._run_acked:
|
|
1159
|
+
raise RuntimeError(
|
|
1160
|
+
"ack_messages must precede set_leaf_message_id"
|
|
1161
|
+
)
|
|
1091
1162
|
self._manager._state["runLeafMessageId"] = message_id
|
|
1092
1163
|
|
|
1093
1164
|
def set_recovery_state(self, value: Any) -> None:
|
|
@@ -15,6 +15,9 @@ class Call:
|
|
|
15
15
|
ctx: RunManager.StartContext
|
|
16
16
|
outcome: "asyncio.Future[Any]"
|
|
17
17
|
|
|
18
|
+
def ack(self) -> None:
|
|
19
|
+
self.ctx.ack_messages()
|
|
20
|
+
|
|
18
21
|
def finish(self, outcome: Any) -> None:
|
|
19
22
|
self.outcome.set_result(outcome)
|
|
20
23
|
|
|
@@ -103,8 +106,8 @@ def make_host(script: Script, capabilities=(), initial_runs=None, max_queued=50)
|
|
|
103
106
|
return await self.runs.stop(params, ack=ctx.ack)
|
|
104
107
|
|
|
105
108
|
@command("run/continue")
|
|
106
|
-
async def run_continue(self):
|
|
107
|
-
self.runs.continue_run()
|
|
109
|
+
async def run_continue(self, *, ctx):
|
|
110
|
+
return await self.runs.continue_run(ack=ctx.ack)
|
|
108
111
|
|
|
109
112
|
@command("run/input")
|
|
110
113
|
async def run_input(self, params):
|
|
@@ -27,6 +27,7 @@ async def test_multi_steer_batch_places_all_and_dispatches_once():
|
|
|
27
27
|
assert [m["id"] for m in call.ctx.messages] == ["s1", "s2", "s3"]
|
|
28
28
|
script.no_call()
|
|
29
29
|
assert queue_ids(drv.replica, "steerQueue") == []
|
|
30
|
+
call.ack()
|
|
30
31
|
call.finish(RunManager.Complete())
|
|
31
32
|
for offset in range(3):
|
|
32
33
|
assert (await drv.res(first + offset))["type"] == "accepted"
|
|
@@ -40,7 +41,7 @@ async def test_steer_and_stop_in_one_batch_nets_to_stop():
|
|
|
40
41
|
first = await drv.batch(
|
|
41
42
|
[("run/steer", add("s1")), ("run/stop", None)]
|
|
42
43
|
)
|
|
43
|
-
assert (await drv.res(first
|
|
44
|
+
assert (await drv.res(first))["type"] == "accepted"
|
|
44
45
|
assert (await drv.res(first + 1, terminal=False))["type"] == "pending"
|
|
45
46
|
await asyncio.wait_for(call.ctx.stop_requested.wait(), 5)
|
|
46
47
|
call.finish(RunManager.Stop(dispatch_queue=False))
|
|
@@ -13,6 +13,7 @@ async def test_immediate_dispatch_carries_caller():
|
|
|
13
13
|
call = await script.next_call()
|
|
14
14
|
assert call.ctx.caller is not None
|
|
15
15
|
assert call.ctx.caller.client_id == "c1"
|
|
16
|
+
call.ack()
|
|
16
17
|
call.finish(RunManager.Complete())
|
|
17
18
|
|
|
18
19
|
|
|
@@ -24,6 +25,7 @@ async def test_queue_entry_projects_caller_client_id():
|
|
|
24
25
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
25
26
|
await drv.wait(lambda s: len(s["queue"]) == 1)
|
|
26
27
|
assert drv.replica["queue"][0]["caller"] == {"clientId": "c1"}
|
|
28
|
+
call.ack()
|
|
27
29
|
call.finish(RunManager.Complete())
|
|
28
30
|
|
|
29
31
|
|
|
@@ -34,12 +36,14 @@ async def test_caller_context_is_read_at_dispatch_time():
|
|
|
34
36
|
call = await script.next_call()
|
|
35
37
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
36
38
|
assert (await drv.context({"user": "simon"}))["type"] == "accepted"
|
|
39
|
+
call.ack()
|
|
37
40
|
call.finish(RunManager.Complete())
|
|
38
41
|
dispatched = await script.next_call()
|
|
39
42
|
assert dispatched.ctx.caller is not None
|
|
40
43
|
assert dispatched.ctx.caller.context == {"user": "simon"}
|
|
41
44
|
assert dispatched.ctx.messages[0]["id"] == "m2"
|
|
42
45
|
assert "caller" not in dispatched.ctx.messages[0]
|
|
46
|
+
dispatched.ack()
|
|
43
47
|
dispatched.finish(RunManager.Complete())
|
|
44
48
|
|
|
45
49
|
|
|
@@ -52,6 +56,7 @@ async def test_dispatch_without_queue_entry_has_no_caller():
|
|
|
52
56
|
] == "pending"
|
|
53
57
|
call = await script.next_call()
|
|
54
58
|
assert call.ctx.caller is None
|
|
59
|
+
call.ack()
|
|
55
60
|
call.finish(RunManager.Complete())
|
|
56
61
|
|
|
57
62
|
|
|
@@ -65,6 +70,7 @@ async def test_take_steered_strips_the_caller_stamp():
|
|
|
65
70
|
assert drv.replica["steerQueue"][0]["caller"] == {"clientId": "c1"}
|
|
66
71
|
[taken] = call.ctx.take_steered()
|
|
67
72
|
assert "caller" not in taken
|
|
73
|
+
call.ack()
|
|
68
74
|
call.finish(RunManager.Complete())
|
|
69
75
|
|
|
70
76
|
|
|
@@ -85,6 +91,7 @@ async def test_max_queued_caps_each_lane():
|
|
|
85
91
|
] == "pending"
|
|
86
92
|
steer_full = await drv.command("run/steer", add("s2"), terminal=False)
|
|
87
93
|
assert steer_full["payload"] == {"reason": "queue-full"}
|
|
94
|
+
call.ack()
|
|
88
95
|
call.finish(RunManager.Complete())
|
|
89
96
|
|
|
90
97
|
|
|
@@ -97,6 +104,7 @@ async def test_lane_change_into_a_full_lane_rejects():
|
|
|
97
104
|
await drv.command("run/steer", add("s1"), terminal=False)
|
|
98
105
|
moved = await drv.command("run/steer", {"messageId": "m2"}, terminal=False)
|
|
99
106
|
assert moved["payload"] == {"reason": "queue-full"}
|
|
107
|
+
call.ack()
|
|
100
108
|
call.finish(RunManager.Complete())
|
|
101
109
|
|
|
102
110
|
|
|
@@ -13,6 +13,7 @@ async def test_edit_of_dispatched_item_stops_rewinds_reruns(command):
|
|
|
13
13
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
14
14
|
call = await script.next_call()
|
|
15
15
|
script.thread["m1"] = {"parentId": "p0", "role": "user", "isLeaf": True}
|
|
16
|
+
call.ack()
|
|
16
17
|
pending = await drv.command(
|
|
17
18
|
command, {"message": msg("m1", "edited")}, terminal=False
|
|
18
19
|
)
|
|
@@ -26,6 +27,7 @@ async def test_edit_of_dispatched_item_stops_rewinds_reruns(command):
|
|
|
26
27
|
assert rerun.ctx.has_rollback
|
|
27
28
|
assert rerun.ctx.rollback_to == "p0"
|
|
28
29
|
await drv.wait_status("running")
|
|
30
|
+
rerun.ack()
|
|
29
31
|
rerun.finish(RunManager.Stop(dispatch_queue=False))
|
|
30
32
|
settled = await drv.res(pending["seq"])
|
|
31
33
|
assert settled["type"] == "accepted"
|
|
@@ -62,6 +64,7 @@ async def test_edit_of_dispatched_item_with_queued_items_stops_and_reruns():
|
|
|
62
64
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
63
65
|
call = await script.next_call()
|
|
64
66
|
script.thread["m1"] = {"parentId": None, "role": "user", "isLeaf": True}
|
|
67
|
+
call.ack()
|
|
65
68
|
await drv.command("run/enqueue", add("m2", anchor="m1"), terminal=False)
|
|
66
69
|
pending = await drv.command(
|
|
67
70
|
"run/enqueue", {"message": msg("m1", "edited")}, terminal=False
|
|
@@ -70,6 +73,7 @@ async def test_edit_of_dispatched_item_with_queued_items_stops_and_reruns():
|
|
|
70
73
|
call.finish(RunManager.Stop(dispatch_queue=False))
|
|
71
74
|
rerun = await script.next_call()
|
|
72
75
|
assert rerun.ctx.type == "message-edit"
|
|
76
|
+
rerun.ack()
|
|
73
77
|
rerun.finish(RunManager.Stop(dispatch_queue=False))
|
|
74
78
|
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
75
79
|
assert [item["id"] for item in drv.replica["queue"]] == ["m2"]
|
|
@@ -81,6 +85,7 @@ async def test_edit_of_dispatched_item_in_error_reruns_without_stop():
|
|
|
81
85
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
82
86
|
call = await script.next_call()
|
|
83
87
|
script.thread["m1"] = {"parentId": None, "role": "user", "isLeaf": True}
|
|
88
|
+
call.ack()
|
|
84
89
|
call.fail(RuntimeError("boom"))
|
|
85
90
|
await drv.wait_status("error")
|
|
86
91
|
await drv.command("run/enqueue", {"message": msg("m1", "edited")}, terminal=False)
|
|
@@ -56,6 +56,7 @@ async def test_deep_edit_rejected_while_queue_non_empty():
|
|
|
56
56
|
await drv.command("run/enqueue", add("m1", anchor="a1"), terminal=False)
|
|
57
57
|
call = await script.next_call()
|
|
58
58
|
await drv.command("run/enqueue", add("m2", anchor="a1"), terminal=False)
|
|
59
|
+
call.ack()
|
|
59
60
|
call.fail(RuntimeError("boom"))
|
|
60
61
|
await drv.wait_status("error")
|
|
61
62
|
assert_rejected(
|
|
@@ -75,6 +76,7 @@ async def test_leaf_edit_in_error_accepted_with_queue_non_empty():
|
|
|
75
76
|
call = await script.next_call()
|
|
76
77
|
script.thread["m1"] = {"parentId": None, "role": "user", "isLeaf": True}
|
|
77
78
|
await drv.command("run/enqueue", add("m2", anchor="m1"), terminal=False)
|
|
79
|
+
call.ack()
|
|
78
80
|
call.fail(RuntimeError("boom"))
|
|
79
81
|
await drv.wait_status("error")
|
|
80
82
|
res = await drv.command("run/edit", {"sourceId": "m1", "message": msg("u2")}, terminal=False)
|
|
@@ -131,6 +133,7 @@ async def test_reload_dispatches_with_empty_messages():
|
|
|
131
133
|
assert call.ctx.type == "message-reload"
|
|
132
134
|
assert call.ctx.messages == ()
|
|
133
135
|
assert call.ctx.rollback_to == "u1"
|
|
136
|
+
call.ack()
|
|
134
137
|
call.finish(RunManager.Complete())
|
|
135
138
|
await drv.wait_status("ready")
|
|
136
139
|
|
|
@@ -175,6 +178,7 @@ async def test_reload_leaf_only_while_queue_non_empty():
|
|
|
175
178
|
script.thread["m1"] = {"parentId": "a0", "role": "user", "isLeaf": False, "onActiveBranch": True}
|
|
176
179
|
script.thread["a1"] = {"parentId": "m1", "role": "assistant", "isLeaf": True, "onActiveBranch": True}
|
|
177
180
|
await drv.command("run/enqueue", add("m2", anchor="a1"), terminal=False)
|
|
181
|
+
call.ack()
|
|
178
182
|
call.fail(RuntimeError("boom"))
|
|
179
183
|
await drv.wait_status("error")
|
|
180
184
|
assert_rejected(
|
|
@@ -15,6 +15,7 @@ async def test_ready_add_dispatches_message_send():
|
|
|
15
15
|
assert not call.ctx.has_rollback
|
|
16
16
|
await drv.wait_status("running")
|
|
17
17
|
assert "dispatch" not in drv.replica
|
|
18
|
+
call.ack()
|
|
18
19
|
call.finish(RunManager.Complete())
|
|
19
20
|
await drv.wait_status("ready")
|
|
20
21
|
|
|
@@ -27,10 +28,12 @@ async def test_running_add_becomes_queue_item():
|
|
|
27
28
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
28
29
|
assert queue_ids(drv.replica, "queue") == ["m2"]
|
|
29
30
|
script.no_call()
|
|
31
|
+
call.ack()
|
|
30
32
|
call.finish(RunManager.Complete())
|
|
31
33
|
drain = await script.next_call()
|
|
32
34
|
assert drain.ctx.type == "message-send"
|
|
33
35
|
assert [m["id"] for m in drain.ctx.messages] == ["m2"]
|
|
36
|
+
drain.ack()
|
|
34
37
|
drain.finish(RunManager.Complete())
|
|
35
38
|
await drv.wait_status("ready")
|
|
36
39
|
|
|
@@ -42,12 +45,15 @@ async def test_queue_drains_fifo_one_item_per_run():
|
|
|
42
45
|
call = await script.next_call()
|
|
43
46
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
44
47
|
await drv.command("run/enqueue", add("m3"), terminal=False)
|
|
48
|
+
call.ack()
|
|
45
49
|
call.finish(RunManager.Complete())
|
|
46
50
|
second = await script.next_call()
|
|
47
51
|
assert [m["id"] for m in second.ctx.messages] == ["m2"]
|
|
52
|
+
second.ack()
|
|
48
53
|
second.finish(RunManager.Complete())
|
|
49
54
|
third = await script.next_call()
|
|
50
55
|
assert [m["id"] for m in third.ctx.messages] == ["m3"]
|
|
56
|
+
third.ack()
|
|
51
57
|
third.finish(RunManager.Complete())
|
|
52
58
|
await drv.wait_status("ready")
|
|
53
59
|
|
|
@@ -60,13 +66,16 @@ async def test_steer_lane_drains_first_and_whole():
|
|
|
60
66
|
await drv.command("run/enqueue", add("q1"), terminal=False)
|
|
61
67
|
await drv.command("run/steer", add("s1"), terminal=False)
|
|
62
68
|
await drv.command("run/steer", add("s2"), terminal=False)
|
|
69
|
+
call.ack()
|
|
63
70
|
call.finish(RunManager.Complete())
|
|
64
71
|
steered = await script.next_call()
|
|
65
72
|
assert steered.ctx.type == "message-send"
|
|
66
73
|
assert [m["id"] for m in steered.ctx.messages] == ["s1", "s2"]
|
|
74
|
+
steered.ack()
|
|
67
75
|
steered.finish(RunManager.Complete())
|
|
68
76
|
regular = await script.next_call()
|
|
69
77
|
assert [m["id"] for m in regular.ctx.messages] == ["q1"]
|
|
78
|
+
regular.ack()
|
|
70
79
|
regular.finish(RunManager.Complete())
|
|
71
80
|
await drv.wait_status("ready")
|
|
72
81
|
|
|
@@ -75,7 +84,9 @@ async def test_error_add_with_empty_lanes_dispatches_immediately():
|
|
|
75
84
|
script = Script()
|
|
76
85
|
async with run_host(script) as (drv, host):
|
|
77
86
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
78
|
-
|
|
87
|
+
call = await script.next_call()
|
|
88
|
+
call.ack()
|
|
89
|
+
call.fail(RuntimeError("boom"))
|
|
79
90
|
await drv.wait_status("error")
|
|
80
91
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
81
92
|
call = await script.next_call()
|
|
@@ -90,6 +101,7 @@ async def test_error_add_with_queued_items_stays_queued():
|
|
|
90
101
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
91
102
|
call = await script.next_call()
|
|
92
103
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
104
|
+
call.ack()
|
|
93
105
|
call.fail(RuntimeError("boom"))
|
|
94
106
|
await drv.wait_status("error")
|
|
95
107
|
await drv.command("run/enqueue", add("m3"), terminal=False)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
"""Contract:
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"""Contract: run lifetime composed with the legacy facade. The ``/chat``
|
|
2
|
+
response stays open through the run — past the send settling at the ack —
|
|
3
|
+
and EOFs once the run task ends; a run rejection is a single error frame
|
|
4
|
+
then EOF.
|
|
4
5
|
"""
|
|
5
6
|
|
|
6
7
|
import asyncio
|
|
@@ -63,6 +64,7 @@ async def test_chat_holds_until_the_entity_run_settles_then_eofs():
|
|
|
63
64
|
assert stream.status == 200
|
|
64
65
|
call = await script.next_call()
|
|
65
66
|
assert call.ctx.type == "message-send"
|
|
67
|
+
call.ack()
|
|
66
68
|
await asyncio.sleep(0.05)
|
|
67
69
|
assert stream._task is not None and not stream._task.done()
|
|
68
70
|
call.finish(RunManager.Complete())
|
|
@@ -18,7 +18,9 @@ def approval(id: str, tool_call_id: str = "tc1") -> dict:
|
|
|
18
18
|
|
|
19
19
|
async def park(drv, script, requests, *, anchor=None):
|
|
20
20
|
await drv.command("run/enqueue", add("m1", anchor=anchor), terminal=False)
|
|
21
|
-
|
|
21
|
+
call = await script.next_call()
|
|
22
|
+
call.ack()
|
|
23
|
+
call.finish(RunManager.InputRequired(requests))
|
|
22
24
|
await drv.wait_status("input-required")
|
|
23
25
|
|
|
24
26
|
|
|
@@ -70,6 +72,7 @@ async def test_completing_answer_resumes_with_ordered_responses():
|
|
|
70
72
|
(approval("r2"), {"decision": "approve"}),
|
|
71
73
|
)
|
|
72
74
|
assert "inputRequests" not in drv.replica
|
|
75
|
+
resumed.ack()
|
|
73
76
|
resumed.finish(RunManager.Complete())
|
|
74
77
|
await drv.wait_status("ready")
|
|
75
78
|
|
|
@@ -94,6 +97,7 @@ async def test_completing_batch_resumes_once_in_the_same_envelope():
|
|
|
94
97
|
assert resumed.ctx.type == "input-resume"
|
|
95
98
|
assert [r["id"] for r, _ in resumed.ctx.responses] == ["r1", "r2"]
|
|
96
99
|
script.no_call()
|
|
100
|
+
resumed.ack()
|
|
97
101
|
resumed.finish(RunManager.Complete())
|
|
98
102
|
await drv.wait_status("ready")
|
|
99
103
|
|
|
@@ -113,6 +117,7 @@ async def test_same_batch_duplicate_first_applies_second_rejects():
|
|
|
113
117
|
resumed = await script.next_call()
|
|
114
118
|
assert resumed.ctx.responses == ((tool_call("r1"), {"output": "a"}),)
|
|
115
119
|
script.no_call()
|
|
120
|
+
resumed.ack()
|
|
116
121
|
resumed.finish(RunManager.Complete())
|
|
117
122
|
await drv.wait_status("ready")
|
|
118
123
|
|
|
@@ -134,6 +139,7 @@ async def test_input_rejections():
|
|
|
134
139
|
),
|
|
135
140
|
"wrong-state",
|
|
136
141
|
)
|
|
142
|
+
call.ack()
|
|
137
143
|
call.finish(RunManager.InputRequired([tool_call("r1"), tool_call("r2")]))
|
|
138
144
|
await drv.wait_status("input-required")
|
|
139
145
|
assert_rejected(await drv.command("run/input", "nope"), "invalid-message")
|
|
@@ -196,6 +202,7 @@ async def test_custom_request_type_passes_through():
|
|
|
196
202
|
)["type"] == "accepted"
|
|
197
203
|
resumed = await script.next_call()
|
|
198
204
|
assert resumed.ctx.responses == ((request, response),)
|
|
205
|
+
resumed.ack()
|
|
199
206
|
resumed.finish(RunManager.Complete())
|
|
200
207
|
await drv.wait_status("ready")
|
|
201
208
|
|
|
@@ -241,9 +248,11 @@ async def test_queue_while_parked_dispatches_after_resume():
|
|
|
241
248
|
)
|
|
242
249
|
resumed = await script.next_call()
|
|
243
250
|
assert resumed.ctx.type == "input-resume"
|
|
251
|
+
resumed.ack()
|
|
244
252
|
resumed.finish(RunManager.Complete())
|
|
245
253
|
drain = await script.next_call()
|
|
246
254
|
assert [m["id"] for m in drain.ctx.messages] == ["m2"]
|
|
255
|
+
drain.ack()
|
|
247
256
|
drain.finish(RunManager.Complete())
|
|
248
257
|
await drv.wait_status("ready")
|
|
249
258
|
|
|
@@ -33,6 +33,7 @@ async def test_set_recovery_state_stays_out_of_replicated_state():
|
|
|
33
33
|
call = await script.next_call()
|
|
34
34
|
call.ctx.set_recovery_state({"checkpoint": 7})
|
|
35
35
|
assert "dispatch" not in drv.replica
|
|
36
|
+
call.ack()
|
|
36
37
|
call.finish(RunManager.Complete())
|
|
37
38
|
await drv.wait_status("ready")
|
|
38
39
|
with pytest.raises(RuntimeError, match="already settled"):
|
|
@@ -45,6 +46,7 @@ async def test_uncaught_raise_freezes_and_sets_state_error():
|
|
|
45
46
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
46
47
|
call = await script.next_call()
|
|
47
48
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
49
|
+
call.ack()
|
|
48
50
|
call.fail(RuntimeError("boom"))
|
|
49
51
|
await drv.wait_status("error")
|
|
50
52
|
assert drv.replica["error"] == {"message": "boom"}
|
|
@@ -57,7 +59,9 @@ async def test_state_error_cleared_by_next_user_initiated_entry():
|
|
|
57
59
|
script = Script()
|
|
58
60
|
async with run_host(script) as (drv, host):
|
|
59
61
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
60
|
-
|
|
62
|
+
call = await script.next_call()
|
|
63
|
+
call.ack()
|
|
64
|
+
call.fail(RuntimeError("boom"))
|
|
61
65
|
await drv.wait_status("error")
|
|
62
66
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
63
67
|
await script.next_call()
|
|
@@ -81,6 +85,7 @@ async def test_error_outcome_freezes_without_state_error():
|
|
|
81
85
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
82
86
|
call = await script.next_call()
|
|
83
87
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
88
|
+
call.ack()
|
|
84
89
|
call.finish(RunManager.Error(dispatch_queue=False))
|
|
85
90
|
await drv.wait_status("error")
|
|
86
91
|
assert drv.replica["error"] is None
|
|
@@ -94,9 +99,11 @@ async def test_error_outcome_with_dispatch_queue_drains():
|
|
|
94
99
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
95
100
|
call = await script.next_call()
|
|
96
101
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
102
|
+
call.ack()
|
|
97
103
|
call.finish(RunManager.Error(dispatch_queue=True))
|
|
98
104
|
drain = await script.next_call()
|
|
99
105
|
assert [m["id"] for m in drain.ctx.messages] == ["m2"]
|
|
106
|
+
drain.ack()
|
|
100
107
|
drain.finish(RunManager.Complete())
|
|
101
108
|
await drv.wait_status("ready")
|
|
102
109
|
|
|
@@ -105,7 +112,9 @@ async def test_error_outcome_with_dispatch_queue_and_empty_lanes_freezes():
|
|
|
105
112
|
script = Script()
|
|
106
113
|
async with run_host(script) as (drv, host):
|
|
107
114
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
108
|
-
|
|
115
|
+
call = await script.next_call()
|
|
116
|
+
call.ack()
|
|
117
|
+
call.finish(RunManager.Error(dispatch_queue=True))
|
|
109
118
|
await drv.wait_status("error")
|
|
110
119
|
script.no_call()
|
|
111
120
|
|
|
@@ -11,6 +11,7 @@ async def start_run(drv, script, *, leaf=True):
|
|
|
11
11
|
call = await script.next_call()
|
|
12
12
|
script.thread["m1"] = {"parentId": None, "role": "user", "isLeaf": leaf, "onActiveBranch": True}
|
|
13
13
|
script.thread["a1"] = {"parentId": "m1", "role": "assistant", "isLeaf": leaf, "onActiveBranch": True}
|
|
14
|
+
call.ack()
|
|
14
15
|
return call
|
|
15
16
|
|
|
16
17
|
|
|
@@ -29,6 +30,7 @@ async def test_running_leaf_edit_stops_settles_then_rewinds():
|
|
|
29
30
|
assert rerun.ctx.type == "message-edit"
|
|
30
31
|
assert [m["id"] for m in rerun.ctx.messages] == ["m2"]
|
|
31
32
|
assert rerun.ctx.rollback_to is None
|
|
33
|
+
rerun.ack()
|
|
32
34
|
rerun.finish(RunManager.Stop(dispatch_queue=False))
|
|
33
35
|
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
34
36
|
|
|
@@ -46,6 +48,7 @@ async def test_running_leaf_edit_accepted_with_non_empty_queues():
|
|
|
46
48
|
call.finish(RunManager.Stop(dispatch_queue=False))
|
|
47
49
|
rerun = await script.next_call()
|
|
48
50
|
assert rerun.ctx.type == "message-edit"
|
|
51
|
+
rerun.ack()
|
|
49
52
|
rerun.finish(RunManager.Stop(dispatch_queue=False))
|
|
50
53
|
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
51
54
|
assert queue_ids(drv.replica, "queue") == ["q1"]
|
|
@@ -66,6 +69,7 @@ async def test_running_leaf_reload_stops_settles_then_rewinds():
|
|
|
66
69
|
rerun = await script.next_call()
|
|
67
70
|
assert rerun.ctx.type == "message-reload"
|
|
68
71
|
assert rerun.ctx.rollback_to == "m1"
|
|
72
|
+
rerun.ack()
|
|
69
73
|
rerun.finish(RunManager.Stop(dispatch_queue=False))
|
|
70
74
|
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
71
75
|
assert queue_ids(drv.replica, "queue") == ["q1"]
|
|
@@ -41,6 +41,7 @@ async def test_steer_dispatch_sets_the_leaf_to_the_last_lane_item():
|
|
|
41
41
|
call = await script.next_call()
|
|
42
42
|
await drv.command("run/steer", add("s1", anchor="m1"), terminal=False)
|
|
43
43
|
await drv.command("run/steer", add("s2", anchor="s1"), terminal=False)
|
|
44
|
+
call.ack()
|
|
44
45
|
call.finish(RunManager.Complete())
|
|
45
46
|
steered = await script.next_call()
|
|
46
47
|
assert steered.ctx.messages[-1]["id"] == "s2"
|
|
@@ -56,6 +57,7 @@ async def test_take_steered_sets_the_leaf():
|
|
|
56
57
|
await drv.wait(lambda s: len(s["steerQueue"]) == 1)
|
|
57
58
|
call.ctx.take_steered()
|
|
58
59
|
await drv.wait(lambda s: s["runLeafMessageId"] == "s1")
|
|
60
|
+
call.ack()
|
|
59
61
|
call.finish(RunManager.Complete())
|
|
60
62
|
|
|
61
63
|
|
|
@@ -64,6 +66,7 @@ async def test_executor_report_sets_the_leaf():
|
|
|
64
66
|
async with run_host(script) as (drv, host):
|
|
65
67
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
66
68
|
call = await script.next_call()
|
|
69
|
+
call.ack()
|
|
67
70
|
call.ctx.set_leaf_message_id("a1")
|
|
68
71
|
await drv.wait(lambda s: s["runLeafMessageId"] == "a1")
|
|
69
72
|
call.finish(RunManager.Complete())
|
|
@@ -89,6 +92,7 @@ async def test_reload_dispatch_sets_the_leaf_to_the_rollback_target():
|
|
|
89
92
|
await drv.command("run/reload", {"sourceId": "a1"}, terminal=False)
|
|
90
93
|
call = await script.next_call()
|
|
91
94
|
await drv.wait(lambda s: s["runLeafMessageId"] == "u1")
|
|
95
|
+
call.ack()
|
|
92
96
|
call.ctx.set_leaf_message_id("a2")
|
|
93
97
|
await drv.wait(lambda s: s["runLeafMessageId"] == "a2")
|
|
94
98
|
|
|
@@ -100,6 +104,7 @@ async def test_report_rejects_an_empty_id():
|
|
|
100
104
|
call = await script.next_call()
|
|
101
105
|
with pytest.raises(ValueError, match="non-empty"):
|
|
102
106
|
call.ctx.set_leaf_message_id("")
|
|
107
|
+
call.ack()
|
|
103
108
|
call.finish(RunManager.Complete())
|
|
104
109
|
|
|
105
110
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Contract: command settlement. A parked send settles accepted at staging and
|
|
2
|
+
stays projected in its lane; a run-starting command answers pending at
|
|
3
|
+
placement and settles at the executor's ``ctx.ack_messages()``. A run that
|
|
4
|
+
ends before the ack settles then: enqueue/steer accepted, their entries back
|
|
5
|
+
at the front of their lane and ``state.error`` carrying any failure;
|
|
6
|
+
edit/reload/continue rejected (``stopped`` or the failure)."""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from run_helpers import Script, add, msg, queue_ids, run_host
|
|
12
|
+
|
|
13
|
+
from statewire import StatewireReject
|
|
14
|
+
|
|
15
|
+
from harness_sdk import RunManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def test_enqueue_settles_accepted_at_the_ack():
|
|
19
|
+
script = Script()
|
|
20
|
+
async with run_host(script) as (drv, host):
|
|
21
|
+
pending = await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
22
|
+
assert pending["type"] == "pending"
|
|
23
|
+
call = await script.next_call()
|
|
24
|
+
call.ack()
|
|
25
|
+
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
26
|
+
call.finish(RunManager.Complete())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def test_parked_send_settles_accepted_at_staging():
|
|
30
|
+
script = Script()
|
|
31
|
+
async with run_host(script) as (drv, host):
|
|
32
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
33
|
+
call = await script.next_call()
|
|
34
|
+
assert (await drv.command("run/enqueue", add("m2")))["type"] == "accepted"
|
|
35
|
+
assert (await drv.command("run/steer", add("s1")))["type"] == "accepted"
|
|
36
|
+
assert queue_ids(drv.replica, "queue") == ["m2"]
|
|
37
|
+
assert queue_ids(drv.replica, "steerQueue") == ["s1"]
|
|
38
|
+
call.ack()
|
|
39
|
+
call.finish(RunManager.Complete())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def test_run_reject_lands_the_payload_on_state_error():
|
|
43
|
+
script = Script()
|
|
44
|
+
async with run_host(script) as (drv, host):
|
|
45
|
+
pending = await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
46
|
+
call = await script.next_call()
|
|
47
|
+
call.fail(
|
|
48
|
+
StatewireReject(
|
|
49
|
+
"payment required",
|
|
50
|
+
payload={"reason": "payment-required", "url": "https://pay.example"},
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
54
|
+
await drv.wait_status("error")
|
|
55
|
+
assert queue_ids(drv.replica, "queue") == ["m1"]
|
|
56
|
+
assert drv.replica["error"] == {
|
|
57
|
+
"message": "payment required",
|
|
58
|
+
"reason": "payment-required",
|
|
59
|
+
"url": "https://pay.example",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def test_unacked_run_end_settles_the_send_accepted():
|
|
64
|
+
script = Script()
|
|
65
|
+
async with run_host(script) as (drv, host):
|
|
66
|
+
for outcome in [
|
|
67
|
+
lambda call: call.fail(RuntimeError("boom")),
|
|
68
|
+
lambda call: call.finish(RunManager.Error(dispatch_queue=False)),
|
|
69
|
+
]:
|
|
70
|
+
pending = await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
71
|
+
outcome(await script.next_call())
|
|
72
|
+
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
73
|
+
await drv.wait_status("error")
|
|
74
|
+
assert queue_ids(drv.replica, "queue") == ["m1"]
|
|
75
|
+
await drv.command("run/dequeue", {"messageId": "m1"})
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def test_unacked_run_end_rejects_a_rewind_initiator():
|
|
79
|
+
for end, reason in [("crash", "run-error"), ("stop", "stopped")]:
|
|
80
|
+
script = Script()
|
|
81
|
+
script.thread["u1"] = {"parentId": None, "role": "user", "isLeaf": True}
|
|
82
|
+
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
83
|
+
pending = await drv.command(
|
|
84
|
+
"run/edit", {"sourceId": "u1", "message": msg("u2")}, terminal=False
|
|
85
|
+
)
|
|
86
|
+
rerun = await script.next_call()
|
|
87
|
+
if end == "crash":
|
|
88
|
+
rerun.fail(RuntimeError("boom"))
|
|
89
|
+
else:
|
|
90
|
+
await drv.command("run/stop", terminal=False)
|
|
91
|
+
rerun.finish(RunManager.Stop(dispatch_queue=False))
|
|
92
|
+
settled = await drv.res(pending["seq"])
|
|
93
|
+
assert settled["type"] == "rejected"
|
|
94
|
+
assert settled["payload"] == {"reason": reason}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def test_unacked_run_end_reverts_entries_to_the_lane_front():
|
|
98
|
+
script = Script()
|
|
99
|
+
async with run_host(script) as (drv, host):
|
|
100
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
101
|
+
call = await script.next_call()
|
|
102
|
+
await drv.command("run/enqueue", add("m2"))
|
|
103
|
+
call.fail(RuntimeError("boom"))
|
|
104
|
+
await drv.wait_status("error")
|
|
105
|
+
assert queue_ids(drv.replica, "queue") == ["m1", "m2"]
|
|
106
|
+
assert drv.replica["error"] == {"message": "boom"}
|
|
107
|
+
assert drv.replica["queue"][0]["caller"] == {"clientId": "c1"}
|
|
108
|
+
assert drv.replica["runLeafMessageId"] is None
|
|
109
|
+
await drv.command("run/steer", {"messageId": "m1"}, terminal=False)
|
|
110
|
+
continued = await script.next_call()
|
|
111
|
+
assert continued.ctx.type == "error-continue"
|
|
112
|
+
assert [m["id"] for m in continued.ctx.take_steered()] == ["m1"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def test_unacked_run_end_reverts_taken_steered_items():
|
|
116
|
+
script = Script()
|
|
117
|
+
async with run_host(script) as (drv, host):
|
|
118
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
119
|
+
call = await script.next_call()
|
|
120
|
+
await drv.command("run/steer", add("s1"))
|
|
121
|
+
call.ctx.take_steered()
|
|
122
|
+
call.fail(RuntimeError("boom"))
|
|
123
|
+
await drv.wait_status("error")
|
|
124
|
+
assert queue_ids(drv.replica, "steerQueue") == ["s1"]
|
|
125
|
+
assert queue_ids(drv.replica, "queue") == ["m1"]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def test_ack_removes_the_entries_for_good():
|
|
129
|
+
script = Script()
|
|
130
|
+
async with run_host(script) as (drv, host):
|
|
131
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
132
|
+
call = await script.next_call()
|
|
133
|
+
call.ack()
|
|
134
|
+
call.fail(RuntimeError("boom"))
|
|
135
|
+
await drv.wait_status("error")
|
|
136
|
+
assert queue_ids(drv.replica, "queue") == []
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def test_complete_without_ack_is_a_run_error():
|
|
140
|
+
script = Script()
|
|
141
|
+
async with run_host(script) as (drv, host):
|
|
142
|
+
pending = await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
143
|
+
(await script.next_call()).finish(RunManager.Complete())
|
|
144
|
+
await drv.wait_status("error")
|
|
145
|
+
assert "ack_messages" in drv.replica["error"]["message"]
|
|
146
|
+
assert queue_ids(drv.replica, "queue") == ["m1"]
|
|
147
|
+
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def test_set_leaf_before_ack_raises():
|
|
151
|
+
script = Script()
|
|
152
|
+
async with run_host(script) as (drv, host):
|
|
153
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
154
|
+
call = await script.next_call()
|
|
155
|
+
with pytest.raises(RuntimeError, match="ack_messages"):
|
|
156
|
+
call.ctx.set_leaf_message_id("a1")
|
|
157
|
+
call.ack()
|
|
158
|
+
call.finish(RunManager.Complete())
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def test_dequeue_removes_a_parked_entry():
|
|
162
|
+
script = Script()
|
|
163
|
+
async with run_host(script) as (drv, host):
|
|
164
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
165
|
+
call = await script.next_call()
|
|
166
|
+
await drv.command("run/enqueue", add("m2"))
|
|
167
|
+
assert (await drv.command("run/dequeue", {"messageId": "m2"}))[
|
|
168
|
+
"type"
|
|
169
|
+
] == "accepted"
|
|
170
|
+
assert queue_ids(drv.replica, "queue") == []
|
|
171
|
+
call.ack()
|
|
172
|
+
call.finish(RunManager.Complete())
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def test_stop_lands_while_a_steer_is_parked():
|
|
176
|
+
script = Script()
|
|
177
|
+
async with run_host(script) as (drv, host):
|
|
178
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
179
|
+
call = await script.next_call()
|
|
180
|
+
assert (await drv.command("run/steer", add("s1")))["type"] == "accepted"
|
|
181
|
+
stop = await drv.command("run/stop", terminal=False)
|
|
182
|
+
assert stop["type"] == "pending"
|
|
183
|
+
await asyncio.wait_for(call.ctx.stop_requested.wait(), 5)
|
|
184
|
+
call.finish(RunManager.Stop(dispatch_queue=False))
|
|
185
|
+
assert (await drv.res(stop["seq"]))["type"] == "accepted"
|
|
186
|
+
await drv.wait_status("stopped")
|
|
187
|
+
# The unconsumed steer stays parked; the unacked dispatch reverts.
|
|
188
|
+
assert queue_ids(drv.replica, "steerQueue") == ["s1"]
|
|
189
|
+
assert queue_ids(drv.replica, "queue") == ["m1"]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def test_rewind_edit_settles_at_the_reruns_ack():
|
|
193
|
+
script = Script()
|
|
194
|
+
script.thread["u1"] = {"parentId": None, "role": "user", "isLeaf": True}
|
|
195
|
+
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
196
|
+
pending = await drv.command(
|
|
197
|
+
"run/edit", {"sourceId": "u1", "message": msg("u2")}, terminal=False
|
|
198
|
+
)
|
|
199
|
+
assert pending["type"] == "pending"
|
|
200
|
+
rerun = await script.next_call()
|
|
201
|
+
assert rerun.ctx.type == "message-edit"
|
|
202
|
+
rerun.ack()
|
|
203
|
+
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
204
|
+
rerun.finish(RunManager.Complete())
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def test_rewind_edit_failure_settles_with_the_payload():
|
|
208
|
+
script = Script()
|
|
209
|
+
script.thread["u1"] = {"parentId": None, "role": "user", "isLeaf": True}
|
|
210
|
+
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
211
|
+
pending = await drv.command(
|
|
212
|
+
"run/edit", {"sourceId": "u1", "message": msg("u2")}, terminal=False
|
|
213
|
+
)
|
|
214
|
+
rerun = await script.next_call()
|
|
215
|
+
rerun.fail(StatewireReject("payment required", payload={"code": 402}))
|
|
216
|
+
settled = await drv.res(pending["seq"])
|
|
217
|
+
assert settled["type"] == "rejected"
|
|
218
|
+
assert settled["payload"] == {"code": 402}
|
|
@@ -26,6 +26,7 @@ async def test_running_steer_parks_until_take_steered():
|
|
|
26
26
|
taken = call.ctx.take_steered()
|
|
27
27
|
assert [m["id"] for m in taken] == ["s1", "s2"]
|
|
28
28
|
assert call.ctx.has_steered() is False
|
|
29
|
+
call.ack()
|
|
29
30
|
call.finish(RunManager.Complete())
|
|
30
31
|
await drv.wait_status("ready")
|
|
31
32
|
assert queue_ids(drv.replica, "steerQueue") == []
|
|
@@ -40,6 +41,7 @@ async def test_steer_add_continues_in_error_and_stop(end, continue_type):
|
|
|
40
41
|
async with run_host(script) as (drv, host):
|
|
41
42
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
42
43
|
call = await script.next_call()
|
|
44
|
+
call.ack()
|
|
43
45
|
if end == "error":
|
|
44
46
|
call.fail(RuntimeError("boom"))
|
|
45
47
|
else:
|
|
@@ -60,6 +62,7 @@ async def test_steer_move_continues_in_error():
|
|
|
60
62
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
61
63
|
call = await script.next_call()
|
|
62
64
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
65
|
+
call.ack()
|
|
63
66
|
call.fail(RuntimeError("boom"))
|
|
64
67
|
await drv.wait_status("error")
|
|
65
68
|
await drv.command("run/steer", {"messageId": "m2"}, terminal=False)
|
|
@@ -75,6 +78,7 @@ async def test_steer_edit_continues_in_error_with_replacement():
|
|
|
75
78
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
76
79
|
call = await script.next_call()
|
|
77
80
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
81
|
+
call.ack()
|
|
78
82
|
call.fail(RuntimeError("boom"))
|
|
79
83
|
await drv.wait_status("error")
|
|
80
84
|
await drv.command("run/steer", {"message": msg("m2", "edited")}, terminal=False)
|
|
@@ -101,6 +105,7 @@ async def test_stale_context_steer_access_raises():
|
|
|
101
105
|
async with run_host(script) as (drv, host):
|
|
102
106
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
103
107
|
call = await script.next_call()
|
|
108
|
+
call.ack()
|
|
104
109
|
call.finish(RunManager.Complete())
|
|
105
110
|
await drv.wait_status("ready")
|
|
106
111
|
with pytest.raises(RuntimeError, match="already settled"):
|
|
@@ -29,6 +29,7 @@ async def test_stop_outcome_can_keep_the_queue_draining():
|
|
|
29
29
|
call = await script.next_call()
|
|
30
30
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
31
31
|
await drv.command("run/stop", terminal=False)
|
|
32
|
+
call.ack()
|
|
32
33
|
call.finish(RunManager.Stop(dispatch_queue=True))
|
|
33
34
|
drain = await script.next_call()
|
|
34
35
|
assert drain.ctx.type == "message-send"
|
|
@@ -59,6 +60,7 @@ async def test_stop_reason_resets_for_the_next_run():
|
|
|
59
60
|
call = await script.next_call()
|
|
60
61
|
await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
61
62
|
await drv.command("run/stop", {"reason": "user-stop"}, terminal=False)
|
|
63
|
+
call.ack()
|
|
62
64
|
call.finish(RunManager.Stop(dispatch_queue=True))
|
|
63
65
|
drain = await script.next_call()
|
|
64
66
|
assert drain.ctx.stop_reason is None
|
|
@@ -91,9 +93,9 @@ async def test_stop_in_input_required_cancels_the_parked_run():
|
|
|
91
93
|
script = Script()
|
|
92
94
|
async with run_host(script) as (drv, host):
|
|
93
95
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
96
|
+
call = await script.next_call()
|
|
97
|
+
call.ack()
|
|
98
|
+
call.finish(RunManager.InputRequired([{"type": "free-form", "id": "r1"}]))
|
|
97
99
|
await drv.wait_status("input-required")
|
|
98
100
|
assert (await drv.command("run/stop"))["type"] == "accepted"
|
|
99
101
|
await drv.wait_status("stopped")
|
|
@@ -105,6 +107,7 @@ async def test_stop_settles_even_when_the_run_completes_normally():
|
|
|
105
107
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
106
108
|
call = await script.next_call()
|
|
107
109
|
pending = await drv.command("run/stop", terminal=False)
|
|
110
|
+
call.ack()
|
|
108
111
|
call.finish(RunManager.Complete())
|
|
109
112
|
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
110
113
|
await drv.wait_status("ready")
|
|
@@ -128,11 +131,14 @@ async def test_bare_continue_with_incomplete_continuation(end, continue_type):
|
|
|
128
131
|
await drv.command("run/stop", terminal=False)
|
|
129
132
|
call.finish(RunManager.Stop(dispatch_queue=False))
|
|
130
133
|
await drv.wait_status(end)
|
|
131
|
-
|
|
134
|
+
res = await drv.command("run/continue", terminal=False)
|
|
135
|
+
assert res["type"] == "pending"
|
|
132
136
|
continued = await script.next_call()
|
|
133
137
|
assert continued.ctx.type == continue_type
|
|
134
138
|
assert continued.ctx.messages == ()
|
|
135
139
|
assert not continued.ctx.has_rollback
|
|
140
|
+
continued.ack()
|
|
141
|
+
assert (await drv.res(res["seq"]))["type"] == "accepted"
|
|
136
142
|
|
|
137
143
|
|
|
138
144
|
async def test_bare_continue_without_capability_rejects():
|
|
@@ -152,10 +158,13 @@ async def test_continue_with_steer_lane_needs_no_capability():
|
|
|
152
158
|
await drv.command("run/steer", add("s1"), terminal=False)
|
|
153
159
|
call.fail(RuntimeError("boom"))
|
|
154
160
|
await drv.wait_status("error")
|
|
155
|
-
|
|
161
|
+
res = await drv.command("run/continue", terminal=False)
|
|
162
|
+
assert res["type"] == "pending"
|
|
156
163
|
continued = await script.next_call()
|
|
157
164
|
assert continued.ctx.type == "error-continue"
|
|
158
165
|
assert [m["id"] for m in continued.ctx.take_steered()] == ["s1"]
|
|
166
|
+
continued.ack()
|
|
167
|
+
assert (await drv.res(res["seq"]))["type"] == "accepted"
|
|
159
168
|
|
|
160
169
|
|
|
161
170
|
@pytest.mark.parametrize("state", ["ready", "running"])
|
|
@@ -166,3 +175,21 @@ async def test_continue_rejected_outside_error_stop(state):
|
|
|
166
175
|
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
167
176
|
await script.next_call()
|
|
168
177
|
assert_rejected(await drv.command("run/continue"), "wrong-state")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def test_unacked_continued_run_end_rejects_the_continue():
|
|
181
|
+
script = Script()
|
|
182
|
+
async with run_host(script, capabilities=("incomplete-continuation",)) as (
|
|
183
|
+
drv,
|
|
184
|
+
host,
|
|
185
|
+
):
|
|
186
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
187
|
+
call = await script.next_call()
|
|
188
|
+
call.ack()
|
|
189
|
+
call.fail(RuntimeError("boom"))
|
|
190
|
+
await drv.wait_status("error")
|
|
191
|
+
res = await drv.command("run/continue", terminal=False)
|
|
192
|
+
(await script.next_call()).fail(RuntimeError("boom again"))
|
|
193
|
+
settled = await drv.res(res["seq"])
|
|
194
|
+
assert settled["type"] == "rejected"
|
|
195
|
+
assert settled["payload"] == {"reason": "run-error"}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
"""Contract: entity-lifetime settle. A run-creating command answers pending at
|
|
2
|
-
placement and settles when its entity's run reaches a terminal state; discard
|
|
3
|
-
before dispatch rejects with ``removed``.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import asyncio
|
|
7
|
-
|
|
8
|
-
from run_helpers import Script, add, msg, run_host
|
|
9
|
-
|
|
10
|
-
from statewire import StatewireReject
|
|
11
|
-
|
|
12
|
-
from harness_sdk import RunManager
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
async def test_enqueue_settles_accepted_when_its_run_completes():
|
|
16
|
-
script = Script()
|
|
17
|
-
async with run_host(script) as (drv, host):
|
|
18
|
-
pending = await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
19
|
-
assert pending["type"] == "pending"
|
|
20
|
-
call = await script.next_call()
|
|
21
|
-
call.finish(RunManager.Complete())
|
|
22
|
-
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async def test_run_reject_settles_the_entity_with_the_payload():
|
|
26
|
-
script = Script()
|
|
27
|
-
async with run_host(script) as (drv, host):
|
|
28
|
-
pending = await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
29
|
-
call = await script.next_call()
|
|
30
|
-
call.fail(
|
|
31
|
-
StatewireReject(
|
|
32
|
-
"payment required",
|
|
33
|
-
payload={"reason": "payment-required", "url": "https://pay.example"},
|
|
34
|
-
)
|
|
35
|
-
)
|
|
36
|
-
settled = await drv.res(pending["seq"])
|
|
37
|
-
assert settled["type"] == "rejected"
|
|
38
|
-
assert settled["message"] == "payment required"
|
|
39
|
-
assert settled["payload"] == {
|
|
40
|
-
"reason": "payment-required",
|
|
41
|
-
"url": "https://pay.example",
|
|
42
|
-
}
|
|
43
|
-
await drv.wait_status("error")
|
|
44
|
-
assert drv.replica["error"] == {
|
|
45
|
-
"message": "payment required",
|
|
46
|
-
"reason": "payment-required",
|
|
47
|
-
"url": "https://pay.example",
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
async def test_run_crash_settles_the_entity_as_run_error():
|
|
52
|
-
script = Script()
|
|
53
|
-
async with run_host(script) as (drv, host):
|
|
54
|
-
pending = await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
55
|
-
(await script.next_call()).fail(RuntimeError("boom"))
|
|
56
|
-
settled = await drv.res(pending["seq"])
|
|
57
|
-
assert settled["type"] == "rejected"
|
|
58
|
-
assert settled["message"] == "boom"
|
|
59
|
-
assert settled["payload"] == {"reason": "run-error"}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
async def test_error_outcome_settles_the_entity_as_run_error():
|
|
63
|
-
script = Script()
|
|
64
|
-
async with run_host(script) as (drv, host):
|
|
65
|
-
pending = await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
66
|
-
(await script.next_call()).finish(RunManager.Error(dispatch_queue=False))
|
|
67
|
-
settled = await drv.res(pending["seq"])
|
|
68
|
-
assert settled["type"] == "rejected"
|
|
69
|
-
assert settled["payload"] == {"reason": "run-error"}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
async def test_steer_settles_with_the_run_that_consumed_it():
|
|
73
|
-
script = Script()
|
|
74
|
-
async with run_host(script) as (drv, host):
|
|
75
|
-
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
76
|
-
call = await script.next_call()
|
|
77
|
-
pending = await drv.command("run/steer", add("s1"), terminal=False)
|
|
78
|
-
assert pending["type"] == "pending"
|
|
79
|
-
call.ctx.take_steered()
|
|
80
|
-
call.finish(RunManager.Complete())
|
|
81
|
-
assert (await drv.res(pending["seq"]))["type"] == "accepted"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
async def test_dequeue_settles_the_queued_entity_as_removed():
|
|
85
|
-
script = Script()
|
|
86
|
-
async with run_host(script) as (drv, host):
|
|
87
|
-
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
88
|
-
call = await script.next_call()
|
|
89
|
-
pending = await drv.command("run/enqueue", add("m2"), terminal=False)
|
|
90
|
-
assert pending["type"] == "pending"
|
|
91
|
-
assert (await drv.command("run/dequeue", {"messageId": "m2"}))["type"] == "accepted"
|
|
92
|
-
settled = await drv.res(pending["seq"])
|
|
93
|
-
assert settled["type"] == "rejected"
|
|
94
|
-
assert settled["payload"] == {"reason": "removed"}
|
|
95
|
-
call.finish(RunManager.Complete())
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
async def test_stop_lands_while_a_steer_is_parked():
|
|
99
|
-
script = Script()
|
|
100
|
-
async with run_host(script) as (drv, host):
|
|
101
|
-
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
102
|
-
call = await script.next_call()
|
|
103
|
-
steer = await drv.command("run/steer", add("s1"), terminal=False)
|
|
104
|
-
assert steer["type"] == "pending"
|
|
105
|
-
stop = await drv.command("run/stop", terminal=False)
|
|
106
|
-
assert stop["type"] == "pending"
|
|
107
|
-
await asyncio.wait_for(call.ctx.stop_requested.wait(), 5)
|
|
108
|
-
call.finish(RunManager.Stop(dispatch_queue=False))
|
|
109
|
-
assert (await drv.res(stop["seq"]))["type"] == "accepted"
|
|
110
|
-
await drv.wait_status("stopped")
|
|
111
|
-
# The unconsumed steer entity stays parked in its lane.
|
|
112
|
-
assert [item["id"] for item in drv.replica["steerQueue"]] == ["s1"]
|
|
113
|
-
assert all(
|
|
114
|
-
rsp["type"] == "pending" for rsp in drv._res.get(steer["seq"], [])
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
async def test_rewind_edit_settles_when_the_rerun_ends():
|
|
119
|
-
script = Script()
|
|
120
|
-
script.thread["u1"] = {"parentId": None, "role": "user", "isLeaf": True}
|
|
121
|
-
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
122
|
-
pending = await drv.command(
|
|
123
|
-
"run/edit", {"sourceId": "u1", "message": msg("u2")}, terminal=False
|
|
124
|
-
)
|
|
125
|
-
assert pending["type"] == "pending"
|
|
126
|
-
rerun = await script.next_call()
|
|
127
|
-
assert rerun.ctx.type == "message-edit"
|
|
128
|
-
rerun.fail(StatewireReject("payment required", payload={"code": 402}))
|
|
129
|
-
settled = await drv.res(pending["seq"])
|
|
130
|
-
assert settled["type"] == "rejected"
|
|
131
|
-
assert settled["payload"] == {"code": 402}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|