harness-sdk-python 0.7.2__tar.gz → 0.8.0__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.7.2 → harness_sdk_python-0.8.0}/PKG-INFO +1 -1
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/pyproject.toml +1 -1
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/src/harness_sdk/run_manager.py +49 -31
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/run_helpers.py +1 -1
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_ack_visibility.py +15 -1
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_dispatching.py +59 -9
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_input_required.py +30 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_meta.py +7 -3
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_run_leaf.py +39 -10
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_settle.py +4 -1
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_steer.py +2 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_stop_continue.py +1 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/.gitignore +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/README.md +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_facade.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.7.2 → harness_sdk_python-0.8.0}/tests/test_rewind_during_run.py +0 -0
|
@@ -14,10 +14,12 @@ and settle when their run ends — accepted once the run has acked its
|
|
|
14
14
|
messages, rejected otherwise; ``run/stop`` awaits a future the drain resolves
|
|
15
15
|
once the in-flight run has ended.
|
|
16
16
|
|
|
17
|
-
``
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
``entry["dispatching"]`` is the dispatch record (or None): the payload the
|
|
18
|
+
run callback receives. ``ctx.ack()`` clears it and confirms
|
|
19
|
+
``runLeafMessageId``; ``steering.take()`` may refill it — at most one unacked
|
|
20
|
+
batch ever exists. A run that ends with an unacked record reverts its items
|
|
21
|
+
by provenance: lane items to the front of their lane, direct dispatches to
|
|
22
|
+
the front of the queue, rewind replacements dropped.
|
|
21
23
|
"""
|
|
22
24
|
|
|
23
25
|
import asyncio
|
|
@@ -51,6 +53,7 @@ _TRIGGERS = (
|
|
|
51
53
|
"input-resume",
|
|
52
54
|
"error-continue",
|
|
53
55
|
"stop-continue",
|
|
56
|
+
"steer",
|
|
54
57
|
)
|
|
55
58
|
|
|
56
59
|
_DECISIONS = ("approve", "reject", "edit", "respond")
|
|
@@ -242,7 +245,7 @@ class RunManager:
|
|
|
242
245
|
"status": "ready",
|
|
243
246
|
"queue": [],
|
|
244
247
|
"steerQueue": [],
|
|
245
|
-
"dispatching":
|
|
248
|
+
"dispatching": None,
|
|
246
249
|
"error": None,
|
|
247
250
|
"runLeafMessageId": None,
|
|
248
251
|
}
|
|
@@ -435,28 +438,30 @@ class RunManager:
|
|
|
435
438
|
if self._task is not None:
|
|
436
439
|
raise RuntimeError("a run is already in flight")
|
|
437
440
|
message_meta = {m["id"]: m.get("meta") for m in messages}
|
|
438
|
-
|
|
441
|
+
items = list(messages)
|
|
439
442
|
messages = [
|
|
440
443
|
{k: v for k, v in message.items() if k != "meta"} for message in messages
|
|
441
444
|
]
|
|
442
|
-
record: dict[str, Any] = {"trigger": trigger, "messages":
|
|
445
|
+
record: dict[str, Any] = {"trigger": trigger, "messages": items}
|
|
443
446
|
if rollback_to is not _ABSENT:
|
|
444
447
|
record["rollbackTo"] = rollback_to
|
|
445
448
|
if input_outcomes:
|
|
446
|
-
record["inputOutcomes"] =
|
|
447
|
-
|
|
449
|
+
record["inputOutcomes"] = [
|
|
450
|
+
{"request": request, "response": response, "meta": meta}
|
|
451
|
+
for request, response, meta in input_outcomes
|
|
452
|
+
]
|
|
453
|
+
self._dispatch_record = dict(record)
|
|
448
454
|
self._stop_reason = None
|
|
449
455
|
self._run_acked = False
|
|
450
456
|
entry = self._ensure_entry()
|
|
451
457
|
self._leaf_confirmed = plain(entry["runLeafMessageId"])
|
|
452
|
-
entry["dispatching"] =
|
|
458
|
+
entry["dispatching"] = record
|
|
453
459
|
entry["error"] = None
|
|
454
460
|
if messages:
|
|
455
461
|
self._dispatched_ids = tuple(m["id"] for m in messages)
|
|
456
462
|
if rollback_to is not _ABSENT:
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
entry["runLeafMessageId"] = messages[-1]["id"]
|
|
463
|
+
# The true leaf is the not-yet-created message; ack records it.
|
|
464
|
+
entry["runLeafMessageId"] = None
|
|
460
465
|
entry["runId"] = uuid.uuid4().hex
|
|
461
466
|
self._set_status("running")
|
|
462
467
|
ctx = RunManager.RunContext(
|
|
@@ -523,7 +528,7 @@ class RunManager:
|
|
|
523
528
|
elif isinstance(outcome, RunManager.Error):
|
|
524
529
|
error = _reject("run-error", "run ended in error")
|
|
525
530
|
elif isinstance(outcome, RunManager.Stop):
|
|
526
|
-
error = _reject("stopped", "run stopped before the
|
|
531
|
+
error = _reject("stopped", "run stopped before the ack")
|
|
527
532
|
else:
|
|
528
533
|
error = None
|
|
529
534
|
self._settle_initiators(error)
|
|
@@ -532,8 +537,8 @@ class RunManager:
|
|
|
532
537
|
self._drain()
|
|
533
538
|
|
|
534
539
|
async def _pull_leaf(self) -> None:
|
|
535
|
-
# An end with unacked
|
|
536
|
-
if plain(self._entry()["dispatching"]):
|
|
540
|
+
# An end with an unacked batch reverts instead of recording a leaf.
|
|
541
|
+
if plain(self._entry()["dispatching"]) is not None:
|
|
537
542
|
return
|
|
538
543
|
self._entry()["runLeafMessageId"] = await self._thread.get_leaf_message_id()
|
|
539
544
|
|
|
@@ -553,16 +558,25 @@ class RunManager:
|
|
|
553
558
|
else:
|
|
554
559
|
future.set_exception(error)
|
|
555
560
|
|
|
556
|
-
def
|
|
561
|
+
def _ack(self) -> None:
|
|
562
|
+
entry = self._entry()
|
|
563
|
+
record = plain(entry["dispatching"])
|
|
564
|
+
if record is None:
|
|
565
|
+
raise RuntimeError("ack() with no unacked batch")
|
|
557
566
|
self._dispatching = []
|
|
558
|
-
|
|
567
|
+
entry["dispatching"] = None
|
|
559
568
|
self._run_acked = True
|
|
560
|
-
|
|
569
|
+
if record["messages"]:
|
|
570
|
+
entry["runLeafMessageId"] = record["messages"][-1]["id"]
|
|
571
|
+
self._leaf_confirmed = plain(entry["runLeafMessageId"])
|
|
561
572
|
|
|
562
573
|
def _revert_dispatching(self) -> None:
|
|
563
574
|
taken, self._dispatching = self._dispatching, []
|
|
564
575
|
entry = self._ensure_entry()
|
|
565
|
-
entry["dispatching"]
|
|
576
|
+
record = plain(entry["dispatching"])
|
|
577
|
+
entry["dispatching"] = None
|
|
578
|
+
if record is not None and "rollbackTo" in record:
|
|
579
|
+
entry["runLeafMessageId"] = self._leaf_confirmed
|
|
566
580
|
if not taken:
|
|
567
581
|
return
|
|
568
582
|
for lane in ("steerQueue", "queue"):
|
|
@@ -571,8 +585,6 @@ class RunManager:
|
|
|
571
585
|
entry[lane] = front + self._lane_items(lane)
|
|
572
586
|
ids = {item["id"] for _, item in taken}
|
|
573
587
|
self._dispatched_ids = tuple(id for id in self._dispatched_ids if id not in ids)
|
|
574
|
-
if plain(entry["runLeafMessageId"]) in ids:
|
|
575
|
-
entry["runLeafMessageId"] = self._leaf_confirmed
|
|
576
588
|
|
|
577
589
|
def _dispatchable(self, lane: str) -> bool:
|
|
578
590
|
if self._task is not None or self._staged_rewinds:
|
|
@@ -600,8 +612,11 @@ class RunManager:
|
|
|
600
612
|
if steer:
|
|
601
613
|
self._entry()["steerQueue"] = []
|
|
602
614
|
self._dispatching.extend(("steerQueue", item) for item in steer)
|
|
603
|
-
self._dispatching.append((
|
|
604
|
-
self._ensure_entry()["dispatching"] =
|
|
615
|
+
self._dispatching.append((e.lane, self._stamped(e.message, e.meta)))
|
|
616
|
+
self._ensure_entry()["dispatching"] = {
|
|
617
|
+
"trigger": "message-send",
|
|
618
|
+
"messages": [item for _, item in self._dispatching],
|
|
619
|
+
}
|
|
605
620
|
|
|
606
621
|
def _dispatch_staged(
|
|
607
622
|
self, *, input_outcomes: tuple[tuple[dict[str, Any], Any, Any], ...] = ()
|
|
@@ -1233,16 +1248,18 @@ class RunManager:
|
|
|
1233
1248
|
_ctx: "RunManager.RunContext"
|
|
1234
1249
|
|
|
1235
1250
|
def take(self) -> tuple[dict[str, Any], ...]:
|
|
1236
|
-
"""Drain the steer lane into
|
|
1251
|
+
"""Drain the steer lane into a fresh dispatch record; requires the current batch to be acked."""
|
|
1237
1252
|
self._ctx._ensure_active()
|
|
1238
1253
|
manager = self._ctx._manager
|
|
1239
|
-
items = manager._lane_items("steerQueue")
|
|
1240
1254
|
entry = manager._entry()
|
|
1255
|
+
if plain(entry["dispatching"]) is not None:
|
|
1256
|
+
raise RuntimeError("take before the current batch is acked")
|
|
1257
|
+
items = manager._lane_items("steerQueue")
|
|
1258
|
+
if not items:
|
|
1259
|
+
return ()
|
|
1241
1260
|
entry["steerQueue"] = []
|
|
1242
1261
|
manager._dispatching.extend(("steerQueue", item) for item in items)
|
|
1243
|
-
entry["dispatching"] =
|
|
1244
|
-
if items:
|
|
1245
|
-
entry["runLeafMessageId"] = items[-1]["id"]
|
|
1262
|
+
entry["dispatching"] = {"trigger": "steer", "messages": items}
|
|
1246
1263
|
self._ctx._message_meta.update(
|
|
1247
1264
|
{item["id"]: item.get("meta") for item in items}
|
|
1248
1265
|
)
|
|
@@ -1301,9 +1318,10 @@ class RunManager:
|
|
|
1301
1318
|
if self._manager._ctx is not self:
|
|
1302
1319
|
raise RuntimeError("this run has already settled")
|
|
1303
1320
|
|
|
1304
|
-
def
|
|
1321
|
+
def ack(self) -> None:
|
|
1322
|
+
"""Ack the current batch: clears the dispatch record and confirms the run leaf."""
|
|
1305
1323
|
self._ensure_active()
|
|
1306
|
-
self._manager.
|
|
1324
|
+
self._manager._ack()
|
|
1307
1325
|
|
|
1308
1326
|
def set_recovery_state(self, value: Any) -> None:
|
|
1309
1327
|
self._ensure_active()
|
|
@@ -24,7 +24,8 @@ def covered_state(drv, seq):
|
|
|
24
24
|
|
|
25
25
|
def dispatching_ids(state):
|
|
26
26
|
runs = state.get("runs") or [{}]
|
|
27
|
-
|
|
27
|
+
record = runs[0].get("dispatching")
|
|
28
|
+
return [item["id"] for item in record["messages"]] if record else []
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def assert_never_queued(drv, message_id):
|
|
@@ -71,6 +72,19 @@ async def test_unacked_run_end_reverts_the_direct_dispatch_to_the_queue():
|
|
|
71
72
|
assert queue_ids(drv.replica, "queue") == ["m1"]
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
async def test_unacked_run_end_reverts_the_direct_steer_send_to_the_steer_lane():
|
|
76
|
+
script = Script()
|
|
77
|
+
async with run_host(script) as (drv, host):
|
|
78
|
+
seq = drv.post("run/steer", add("s1"))
|
|
79
|
+
call = await script.next_call()
|
|
80
|
+
assert (await drv.res(seq))["type"] == "accepted"
|
|
81
|
+
assert dispatching_ids(covered_state(drv, seq)) == ["s1"]
|
|
82
|
+
call.fail(RuntimeError("boom"))
|
|
83
|
+
await drv.wait_status("error")
|
|
84
|
+
assert queue_ids(drv.replica, "steerQueue") == ["s1"]
|
|
85
|
+
assert queue_ids(drv.replica, "queue") == []
|
|
86
|
+
|
|
87
|
+
|
|
74
88
|
async def test_rewind_edit_acks_with_its_dispatch():
|
|
75
89
|
script = Script()
|
|
76
90
|
script.thread["u1"] = {"parentId": None, "role": "user", "isLeaf": True}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
"""Contract: replicated dispatch
|
|
2
|
-
the current
|
|
3
|
-
|
|
4
|
-
``
|
|
5
|
-
atomic: dispatch
|
|
6
|
-
|
|
7
|
-
returns lane items to the front of their lane."""
|
|
1
|
+
"""Contract: the replicated dispatch record. ``dispatching`` on the run entry
|
|
2
|
+
is the current unacked batch — a record ``{trigger, messages, rollbackTo?,
|
|
3
|
+
inputOutcomes?}`` or None. A message item lives in exactly one of the lanes,
|
|
4
|
+
the record's ``messages``, or the thread, and every transition is
|
|
5
|
+
single-envelope atomic: dispatch fills the record, ``ctx.ack()`` clears it as
|
|
6
|
+
the canonical message appears, a steer take refills it, and an unacked run
|
|
7
|
+
end returns lane items to the front of their lane."""
|
|
8
8
|
|
|
9
9
|
import copy
|
|
10
10
|
|
|
11
|
+
import pytest
|
|
11
12
|
from run_helpers import Script, add, msg, queue_ids, run_host
|
|
12
13
|
|
|
13
14
|
from harness_sdk import RunManager
|
|
@@ -16,16 +17,20 @@ from statewire_helpers import apply_ops
|
|
|
16
17
|
|
|
17
18
|
def dispatching_ids(replica):
|
|
18
19
|
runs = replica["runs"]
|
|
19
|
-
|
|
20
|
+
record = runs[0]["dispatching"] if runs else None
|
|
21
|
+
return [item["id"] for item in record["messages"]] if record else []
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def places(state, message_id):
|
|
23
25
|
entry = (state.get("runs") or [{}])[0]
|
|
24
26
|
found = {
|
|
25
27
|
name
|
|
26
|
-
for name in ("queue", "steerQueue"
|
|
28
|
+
for name in ("queue", "steerQueue")
|
|
27
29
|
if any(item["id"] == message_id for item in entry.get(name, []))
|
|
28
30
|
}
|
|
31
|
+
record = entry.get("dispatching")
|
|
32
|
+
if record and any(item["id"] == message_id for item in record["messages"]):
|
|
33
|
+
found.add("dispatching")
|
|
29
34
|
if any(m["id"] == message_id for m in state.get("messages", [])):
|
|
30
35
|
found.add("messages")
|
|
31
36
|
return found
|
|
@@ -53,6 +58,7 @@ async def test_idle_send_dispatches_straight_into_dispatching():
|
|
|
53
58
|
drv.post("run/enqueue", add("m1"))
|
|
54
59
|
await script.next_call()
|
|
55
60
|
await drv.wait_status("running")
|
|
61
|
+
assert drv.run["dispatching"]["trigger"] == "message-send"
|
|
56
62
|
assert dispatching_ids(drv.replica) == ["m1"]
|
|
57
63
|
assert queue_ids(drv.replica, "queue") == []
|
|
58
64
|
assert residence(drv, "m1") == ["dispatching"]
|
|
@@ -97,6 +103,7 @@ async def test_steering_take_moves_the_lane_into_dispatching():
|
|
|
97
103
|
await drv.command("run/steer", add("s1"), terminal=False)
|
|
98
104
|
call.ctx.steering.take()
|
|
99
105
|
await drv.wait(lambda s: dispatching_ids(s) == ["s1"])
|
|
106
|
+
assert drv.run["dispatching"]["trigger"] == "steer"
|
|
100
107
|
assert queue_ids(drv.replica, "steerQueue") == []
|
|
101
108
|
assert residence(drv, "s1") == ["steerQueue", "dispatching"]
|
|
102
109
|
host.live.state["messages"] = [msg("m1"), msg("s1")]
|
|
@@ -121,3 +128,46 @@ async def test_edit_replacement_rides_dispatching_until_ack():
|
|
|
121
128
|
assert residence(drv, "u2") == ["dispatching", "messages"]
|
|
122
129
|
rerun.finish(RunManager.Complete())
|
|
123
130
|
assert (await drv.res(seq))["type"] == "accepted"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def test_take_before_the_ack_raises():
|
|
134
|
+
script = Script()
|
|
135
|
+
async with run_host(script) as (drv, host):
|
|
136
|
+
drv.post("run/enqueue", add("m1"))
|
|
137
|
+
call = await script.next_call()
|
|
138
|
+
await drv.side("run/steer", add("s1"))
|
|
139
|
+
await drv.wait(lambda s: queue_ids(s, "steerQueue") == ["s1"])
|
|
140
|
+
with pytest.raises(RuntimeError, match="before the current batch is acked"):
|
|
141
|
+
call.ctx.steering.take()
|
|
142
|
+
call.ack()
|
|
143
|
+
assert [m["id"] for m in call.ctx.steering.take()] == ["s1"]
|
|
144
|
+
call.ack()
|
|
145
|
+
call.finish(RunManager.Complete())
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def test_ack_with_no_unacked_batch_raises():
|
|
149
|
+
script = Script()
|
|
150
|
+
async with run_host(script) as (drv, host):
|
|
151
|
+
drv.post("run/enqueue", add("m1"))
|
|
152
|
+
call = await script.next_call()
|
|
153
|
+
call.ack()
|
|
154
|
+
with pytest.raises(RuntimeError, match="no unacked batch"):
|
|
155
|
+
call.ack()
|
|
156
|
+
call.finish(RunManager.Complete())
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def test_second_take_after_the_reack_drains_the_next_batch():
|
|
160
|
+
script = Script()
|
|
161
|
+
async with run_host(script) as (drv, host):
|
|
162
|
+
drv.post("run/enqueue", add("m1"))
|
|
163
|
+
call = await script.next_call()
|
|
164
|
+
call.ack()
|
|
165
|
+
await drv.command("run/steer", add("s1"), terminal=False)
|
|
166
|
+
assert [m["id"] for m in call.ctx.steering.take()] == ["s1"]
|
|
167
|
+
call.ack()
|
|
168
|
+
await drv.command("run/steer", add("s2", anchor="m1"), terminal=False)
|
|
169
|
+
assert [m["id"] for m in call.ctx.steering.take()] == ["s2"]
|
|
170
|
+
await drv.wait(lambda s: dispatching_ids(s) == ["s2"])
|
|
171
|
+
assert drv.run["dispatching"]["trigger"] == "steer"
|
|
172
|
+
call.ack()
|
|
173
|
+
call.finish(RunManager.Complete())
|
|
@@ -79,6 +79,36 @@ async def test_completing_answer_resumes_with_ordered_responses():
|
|
|
79
79
|
await drv.wait_status("ready")
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
async def test_input_outcomes_ride_the_dispatch_record_until_the_ack():
|
|
83
|
+
script = Script()
|
|
84
|
+
async with run_host(script) as (drv, host):
|
|
85
|
+
await park(drv, script, [tool_call("r1"), approval("r2")])
|
|
86
|
+
await drv.command(
|
|
87
|
+
"run/input", {"requestId": "r2", "response": {"decision": "approve"}}
|
|
88
|
+
)
|
|
89
|
+
await drv.command(
|
|
90
|
+
"run/input", {"requestId": "r1", "response": {"output": "ok"}}
|
|
91
|
+
)
|
|
92
|
+
resumed = await script.next_call()
|
|
93
|
+
await drv.wait(lambda s: run_of(s).get("dispatching") is not None)
|
|
94
|
+
assert drv.run["dispatching"] == {
|
|
95
|
+
"trigger": "input-resume",
|
|
96
|
+
"messages": [],
|
|
97
|
+
"inputOutcomes": [
|
|
98
|
+
{"request": tool_call("r1"), "response": {"output": "ok"}, "meta": None},
|
|
99
|
+
{
|
|
100
|
+
"request": approval("r2"),
|
|
101
|
+
"response": {"decision": "approve"},
|
|
102
|
+
"meta": None,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
}
|
|
106
|
+
resumed.ack()
|
|
107
|
+
await drv.wait(lambda s: run_of(s).get("dispatching") is None)
|
|
108
|
+
resumed.finish(RunManager.Complete())
|
|
109
|
+
await drv.wait_status("ready")
|
|
110
|
+
|
|
111
|
+
|
|
82
112
|
async def test_completing_batch_resumes_once_in_the_same_envelope():
|
|
83
113
|
script = Script()
|
|
84
114
|
async with run_host(script) as (drv, host):
|
|
@@ -131,10 +131,14 @@ async def test_meta_dies_with_a_dequeued_entry():
|
|
|
131
131
|
async def test_unacked_run_end_reprojects_entries_with_meta():
|
|
132
132
|
script = Script()
|
|
133
133
|
async with run_host(script) as (drv, _):
|
|
134
|
-
drv.
|
|
134
|
+
drv.batch(
|
|
135
|
+
[
|
|
136
|
+
("run/enqueue", add("m1", meta={"clientId": "c1"})),
|
|
137
|
+
("run/steer", add("s1", meta={"clientId": "c2"})),
|
|
138
|
+
]
|
|
139
|
+
)
|
|
135
140
|
call = await script.next_call()
|
|
136
|
-
|
|
137
|
-
call.ctx.steering.take()
|
|
141
|
+
assert [m["id"] for m in call.ctx.messages] == ["m1", "s1"]
|
|
138
142
|
call.fail(RuntimeError("boom"))
|
|
139
143
|
await drv.wait_status("error")
|
|
140
144
|
assert drv.run["queue"][0]["meta"] == {"clientId": "c1"}
|
|
@@ -30,15 +30,18 @@ async def test_ready_state_has_no_run_entry():
|
|
|
30
30
|
assert drv.replica["runs"] == []
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
async def
|
|
33
|
+
async def test_ack_sets_the_leaf_to_the_last_batch_message():
|
|
34
34
|
script = Script()
|
|
35
35
|
async with run_host(script) as (drv, host):
|
|
36
36
|
drv.post("run/enqueue", add("m1"))
|
|
37
|
-
await script.next_call()
|
|
37
|
+
call = await script.next_call()
|
|
38
|
+
await drv.wait_status("running")
|
|
39
|
+
assert drv.run["runLeafMessageId"] is None
|
|
40
|
+
call.ack()
|
|
38
41
|
await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "m1")
|
|
39
42
|
|
|
40
43
|
|
|
41
|
-
async def
|
|
44
|
+
async def test_steer_batch_ack_sets_the_leaf_to_the_last_lane_item():
|
|
42
45
|
script = Script()
|
|
43
46
|
async with run_host(script) as (drv, host):
|
|
44
47
|
drv.post("run/enqueue", add("m1"))
|
|
@@ -49,10 +52,12 @@ async def test_steer_dispatch_sets_the_leaf_to_the_last_lane_item():
|
|
|
49
52
|
call.finish(RunManager.Complete())
|
|
50
53
|
steered = await script.next_call()
|
|
51
54
|
assert steered.ctx.messages[-1]["id"] == "s2"
|
|
55
|
+
assert drv.run["runLeafMessageId"] == "m1"
|
|
56
|
+
steered.ack()
|
|
52
57
|
await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "s2")
|
|
53
58
|
|
|
54
59
|
|
|
55
|
-
async def
|
|
60
|
+
async def test_steering_take_moves_the_leaf_at_its_ack():
|
|
56
61
|
script = Script()
|
|
57
62
|
async with run_host(script) as (drv, host):
|
|
58
63
|
drv.post("run/enqueue", add("m1"))
|
|
@@ -61,8 +66,9 @@ async def test_steering_take_sets_the_leaf():
|
|
|
61
66
|
await drv.command("run/steer", add("s1", anchor="m1"), terminal=False)
|
|
62
67
|
await drv.wait(lambda s: len(run_of(s).get("steerQueue", [])) == 1)
|
|
63
68
|
call.ctx.steering.take()
|
|
64
|
-
|
|
69
|
+
assert drv.run["runLeafMessageId"] == "m1"
|
|
65
70
|
call.ack()
|
|
71
|
+
await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "s1")
|
|
66
72
|
call.finish(RunManager.Complete())
|
|
67
73
|
|
|
68
74
|
|
|
@@ -133,30 +139,53 @@ async def test_complete_with_a_queued_item_keeps_the_leaf_flowing():
|
|
|
133
139
|
script.leaf = "a1"
|
|
134
140
|
call.finish(RunManager.Complete())
|
|
135
141
|
drain = await script.next_call()
|
|
136
|
-
await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "m2")
|
|
137
142
|
drain.ack()
|
|
143
|
+
await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "m2")
|
|
138
144
|
drain.finish(RunManager.Complete())
|
|
139
145
|
await drv.wait_status("ready")
|
|
140
146
|
assert drv.replica["runs"] == []
|
|
141
147
|
|
|
142
148
|
|
|
143
|
-
async def
|
|
149
|
+
async def test_rewind_dispatch_nulls_the_leaf_and_the_ack_records_the_replacement():
|
|
144
150
|
script = Script()
|
|
145
151
|
thread_with_turn(script)
|
|
146
152
|
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
147
153
|
drv.post("run/edit", {"sourceId": "u1", "message": msg("u2")})
|
|
148
|
-
await script.next_call()
|
|
154
|
+
call = await script.next_call()
|
|
155
|
+
await drv.wait_status("running")
|
|
156
|
+
assert drv.run["runLeafMessageId"] is None
|
|
157
|
+
call.ack()
|
|
149
158
|
await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "u2")
|
|
150
159
|
|
|
151
160
|
|
|
152
|
-
async def
|
|
161
|
+
async def test_reload_dispatch_nulls_the_leaf_until_the_settle_pull():
|
|
153
162
|
script = Script()
|
|
154
163
|
thread_with_turn(script)
|
|
155
164
|
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
156
165
|
drv.post("run/reload", {"sourceId": "a1"})
|
|
157
166
|
call = await script.next_call()
|
|
158
|
-
await drv.
|
|
167
|
+
await drv.wait_status("running")
|
|
168
|
+
assert drv.run["runLeafMessageId"] is None
|
|
159
169
|
call.ack()
|
|
170
|
+
assert drv.run["runLeafMessageId"] is None
|
|
160
171
|
script.leaf = "a2"
|
|
161
172
|
call.finish(RunManager.Stop(dispatch_queue=False))
|
|
162
173
|
await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "a2")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def test_unacked_rewind_end_restores_the_previous_leaf():
|
|
177
|
+
script = Script()
|
|
178
|
+
thread_with_turn(script)
|
|
179
|
+
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
180
|
+
drv.post("run/enqueue", add("m1", anchor="a1"))
|
|
181
|
+
call = await script.next_call()
|
|
182
|
+
call.ack()
|
|
183
|
+
script.leaf = "a2"
|
|
184
|
+
call.finish(RunManager.Stop(dispatch_queue=False))
|
|
185
|
+
await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "a2")
|
|
186
|
+
drv.post("run/edit", {"sourceId": "u1", "message": msg("u2")})
|
|
187
|
+
rerun = await script.next_call()
|
|
188
|
+
await drv.wait(lambda s: run_of(s).get("runLeafMessageId") is None)
|
|
189
|
+
rerun.fail(RuntimeError("boom"))
|
|
190
|
+
await drv.wait_status("error")
|
|
191
|
+
assert drv.run["runLeafMessageId"] == "a2"
|
|
@@ -105,6 +105,7 @@ async def test_unacked_run_end_reverts_entries_to_the_lane_front():
|
|
|
105
105
|
await drv.command("run/steer", {"messageId": "m1"}, terminal=False)
|
|
106
106
|
continued = await script.next_call()
|
|
107
107
|
assert continued.ctx.trigger == "error-continue"
|
|
108
|
+
continued.ack()
|
|
108
109
|
assert [m["id"] for m in continued.ctx.steering.take()] == ["m1"]
|
|
109
110
|
|
|
110
111
|
|
|
@@ -113,12 +114,14 @@ async def test_unacked_run_end_reverts_taken_steered_items():
|
|
|
113
114
|
async with run_host(script) as (drv, host):
|
|
114
115
|
drv.post("run/enqueue", add("m1"))
|
|
115
116
|
call = await script.next_call()
|
|
117
|
+
call.ack()
|
|
116
118
|
await drv.side("run/steer", add("s1"))
|
|
119
|
+
await drv.wait(lambda s: queue_ids(s, "steerQueue") == ["s1"])
|
|
117
120
|
call.ctx.steering.take()
|
|
118
121
|
call.fail(RuntimeError("boom"))
|
|
119
122
|
await drv.wait_status("error")
|
|
120
123
|
assert queue_ids(drv.replica, "steerQueue") == ["s1"]
|
|
121
|
-
assert queue_ids(drv.replica, "queue") == [
|
|
124
|
+
assert queue_ids(drv.replica, "queue") == []
|
|
122
125
|
|
|
123
126
|
|
|
124
127
|
async def test_ack_removes_the_entries_for_good():
|
|
@@ -55,6 +55,7 @@ async def test_steer_add_continues_in_error_and_stop(end, continue_type):
|
|
|
55
55
|
assert continued.ctx.messages == ()
|
|
56
56
|
assert queue_ids(drv.replica, "steerQueue") == ["s1"]
|
|
57
57
|
assert continued.ctx.steering.available.is_set()
|
|
58
|
+
continued.ack()
|
|
58
59
|
assert [m["id"] for m in continued.ctx.steering.take()] == ["s1"]
|
|
59
60
|
|
|
60
61
|
|
|
@@ -86,6 +87,7 @@ async def test_steer_edit_continues_in_error_with_replacement():
|
|
|
86
87
|
await drv.command("run/steer", {"message": msg("m2", "edited")}, terminal=False)
|
|
87
88
|
continued = await script.next_call()
|
|
88
89
|
assert continued.ctx.trigger == "error-continue"
|
|
90
|
+
continued.ack()
|
|
89
91
|
taken = continued.ctx.steering.take()
|
|
90
92
|
assert [m["parts"][0]["text"] for m in taken] == ["edited"]
|
|
91
93
|
|
|
@@ -234,6 +234,7 @@ async def test_continue_with_steer_lane_needs_no_capability():
|
|
|
234
234
|
assert res["type"] == "pending"
|
|
235
235
|
continued = await script.next_call()
|
|
236
236
|
assert continued.ctx.trigger == "error-continue"
|
|
237
|
+
continued.ack()
|
|
237
238
|
assert [m["id"] for m in continued.ctx.steering.take()] == ["s1"]
|
|
238
239
|
continued.ack()
|
|
239
240
|
continued.finish(RunManager.Complete())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|