ouroboros/tests/test_cancel_queue_integration.py
Ouroboros c38ccede4b P7-fix1a: say the server stopped on the card the restart fence produces
At boot the queue is empty, so every RUNNING row the snapshot restore fenced
reaches cancellation custody through the finalize-on-miss lane, whose fixed
sentence claimed the task "was neither queued nor running at supervisor
teardown". That asserts the opposite of the fact that minted the fence, it
contradicts the boot line, and it strips the cause from the expiry of a quiz
lost to the restart (the cancel text is that cause's only carrier).

The miss lane keeps its default sentence for every other intent and, when the
settled intent carries reason server_shutdown from source snapshot_restore,
writes "Task cancelled: the server stopped while this task was still running."
The "Task cancelled" prefix the existing pins rely on is preserved. The restore
test now asserts the exact sentence for a root and its subagent child, and the
quiz test asserts the same text carries the expiry cause.
2026-09-12 17:14:46 +03:00

980 lines
41 KiB
Python

"""The queue side of a cancel intent: pending drop, snapshot restore, fail, steer.
Split out of ``tests/test_cancel_intents_phase_a.py`` by theme: the readers that consult
the intent projection before acting, the decisions they stamp, the write failures that
leave an intent open, the deference to a live claim owner, and the steering that is
refused while a cancel is active.
The v7next tip adds the upstream dispatch-authority family: assignment and the timeout
reaper block (retain, never clone) when the cancel authority cannot be read, and the
cancel-authority hold never releases a terminal row to dispatch.
"""
from __future__ import annotations
import json
import types
import pytest
from ouroboros import cancel_intents as ci
from ouroboros.task_results import (
STATUS_CANCEL_REQUESTED,
STATUS_CANCELLED,
STATUS_COMPLETED,
STATUS_RUNNING,
load_task_result,
write_task_result,
)
from tests._cancel_intents_shared import qenv as _qenv
# The fixture is requested by name as a test parameter, so it is re-bound through a
# module attribute: a direct import of a name that reappears as a parameter is an F811
# redefinition under the CI ruff gate.
qenv = _qenv
def test_drop_cancelled_pending_consults_the_intent_projection(qenv, monkeypatch):
from supervisor import workers
emitted: list = []
monkeypatch.setattr(workers, "_emit_task_done_terminal",
lambda task, tid, status, **kw: emitted.append((tid, status, kw)) or True)
monkeypatch.setattr(workers, "PENDING", qenv.q.PENDING, raising=False)
monkeypatch.setattr(workers, "DRIVE_ROOT", qenv.drive, raising=False)
qenv.q.PENDING[:] = [
{"id": "keepme", "chat_id": 1},
{"id": "dropme", "chat_id": 1},
]
write_task_result(qenv.drive, "dropme", "scheduled")
ci.request_cancel(qenv.drive, "dropme", reason="parent stopped the plan")
workers._drop_cancelled_pending()
assert [t["id"] for t in qenv.q.PENDING] == ["keepme"]
stored = load_task_result(qenv.drive, "dropme")
assert stored["status"] == STATUS_CANCELLED
assert "cost_accounting_status" in stored # reconstructed, not omitted
assert ci.active_intent(qenv.drive, "dropme") is None
assert emitted and emitted[0][0] == "dropme" and emitted[0][1] == "cancelled"
def test_assignment_blocks_when_cancel_intent_projection_is_unreadable(
tmp_path, monkeypatch,
):
"""A corrupt intent projection cannot be treated as an empty projection."""
from supervisor import queue, state, workers
delivered: list[dict] = []
worker = types.SimpleNamespace(
wid=1,
busy_task_id=None,
reaping=False,
in_q=types.SimpleNamespace(put=lambda task: delivered.append(dict(task))),
)
task = {
"id": "blocked-by-intent-corruption",
"type": "task",
"chat_id": 1,
"depth": 0,
"budget_drive_root": str(tmp_path),
}
pending = [task]
monkeypatch.setattr(workers, "DRIVE_ROOT", tmp_path)
monkeypatch.setattr(workers, "PENDING", pending)
monkeypatch.setattr(workers, "RUNNING", {})
monkeypatch.setattr(workers, "WORKERS", {1: worker})
monkeypatch.setattr(workers, "load_state", lambda: {})
monkeypatch.setattr(
state,
"budget_remaining",
lambda *_args, **_kwargs: pytest.fail("budget must not run after authority failure"),
)
snapshots: list[str] = []
monkeypatch.setattr(
queue,
"persist_queue_snapshot",
lambda reason="": snapshots.append(reason) or True,
)
queue.BUDGET_ROOT_FENCES.clear()
write_task_result(tmp_path, task["id"], "scheduled")
projection = tmp_path / "state" / "cancel_intents.json"
projection.parent.mkdir(parents=True, exist_ok=True)
corrupt_bytes = b'{"intents": [broken'
projection.write_bytes(corrupt_bytes)
workers.assign_tasks()
assert pending == [task]
assert delivered == []
assert worker.busy_task_id is None
assert projection.read_bytes() == corrupt_bytes
assert snapshots == ["cancellation_authority_indeterminate"]
def test_assignment_retains_pending_when_claim_authority_raises(qenv, monkeypatch):
"""Only an explicit claim refusal may yield custody to another owner."""
from supervisor import state, workers
task_id = "claim-authority-failure"
pending = [{"id": task_id, "type": "task", "chat_id": 1, "depth": 0}]
delivered: list[dict] = []
worker = types.SimpleNamespace(
wid=1,
busy_task_id=None,
reaping=False,
in_q=types.SimpleNamespace(put=lambda task: delivered.append(dict(task))),
)
monkeypatch.setattr(workers, "DRIVE_ROOT", qenv.drive)
monkeypatch.setattr(workers, "PENDING", pending)
monkeypatch.setattr(workers, "RUNNING", {})
monkeypatch.setattr(workers, "WORKERS", {1: worker})
monkeypatch.setattr(workers, "load_state", lambda: {})
monkeypatch.setattr(
state,
"budget_remaining",
lambda *_args, **_kwargs: pytest.fail("budget must not run after claim failure"),
)
monkeypatch.setattr(
ci,
"claim_intent",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
ci.CancelIntentProjectionCorrupt("projection changed during claim")
),
)
write_task_result(qenv.drive, task_id, "scheduled")
ci.request_cancel(qenv.drive, task_id, reason="stop")
workers.assign_tasks()
assert [row["id"] for row in pending] == [task_id]
assert delivered == []
assert worker.busy_task_id is None
assert load_task_result(qenv.drive, task_id)["status"] == "scheduled"
assert ci.active_intent(qenv.drive, task_id)["state"] == ci.INTENT_REQUESTED
def test_timeout_reaper_does_not_clone_over_unreadable_cancel_authority(
tmp_path, monkeypatch,
):
"""Every physical retry must prove that the old id has no cancel intent."""
from supervisor import queue, workers
now = 10_000.0
task_id = "timeout-over-corrupt-intent-store"
task = {"id": task_id, "type": "task", "chat_id": 0}
meta = {
"task": task,
"started_at": now - 1000.0,
"last_heartbeat_at": now - 1000.0,
"last_progress_at": now - 1000.0,
"attempt": 1,
"worker_id": -1,
}
running = {task_id: meta}
queue.init_queue_refs([], running, {"value": 0})
monkeypatch.setattr(queue, "DRIVE_ROOT", tmp_path)
monkeypatch.setattr(queue, "FINALIZATION_GRACE_SEC", 0.0)
monkeypatch.setattr(queue, "get_task_idle_timeout_sec", lambda: 60.0)
monkeypatch.setattr(queue, "get_per_call_timeout_ceiling_sec", lambda: 0.0)
monkeypatch.setattr(queue, "get_task_abs_ceiling_sec", lambda: 10_000_000.0)
monkeypatch.setattr(queue, "_ensure_reaper_started", lambda: None)
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda reason="": True)
jobs: list[dict] = []
monkeypatch.setattr(queue, "_reap_queue", types.SimpleNamespace(put=jobs.append))
monkeypatch.setattr(workers, "WORKERS", {})
projection = tmp_path / "state" / "cancel_intents.json"
projection.parent.mkdir(parents=True)
projection.write_bytes(b'{"intents": [broken')
queue._enforce_task_timeouts_locked(workers, now, 0, {})
assert len(jobs) == 1
assert jobs[0]["task_id"] == task_id
assert jobs[0]["will_retry"] is False
assert jobs[0]["retry_task_id"] == ""
assert task_id not in running
def test_snapshot_restore_refuses_a_task_with_active_intent(qenv, monkeypatch):
from ouroboros.utils import utc_now_iso
ci.request_cancel(qenv.drive, "restoreme")
snapshot = {
"ts": utc_now_iso(),
"pending": [{"task": {"id": "restoreme", "chat_id": 1, "type": "chat"}}],
"running": [],
"acceptance_fences": [],
"budget_root_fences": [],
}
state_dir = qenv.drive / "state"
state_dir.mkdir(parents=True, exist_ok=True)
(state_dir / "queue_snapshot.json").write_text(json.dumps(snapshot), encoding="utf-8")
monkeypatch.setattr(qenv.q, "QUEUE_SNAPSHOT_PATH", state_dir / "queue_snapshot.json",
raising=False)
restored = qenv.q.restore_pending_from_snapshot()
assert restored == 0
assert qenv.q.PENDING == []
def test_snapshot_restore_blocks_when_cancel_intent_projection_is_unreadable(
tmp_path, monkeypatch,
):
"""Restart recovery must not resurrect a row over a corrupt intent store."""
from ouroboros.utils import utc_now_iso
from supervisor import queue
pending: list[dict] = []
running: dict = {}
counter = {"value": 0}
queue.init_queue_refs(pending, running, counter)
monkeypatch.setattr(queue, "DRIVE_ROOT", tmp_path)
snapshot_path = tmp_path / "state" / "queue_snapshot.json"
monkeypatch.setattr(queue, "QUEUE_SNAPSHOT_PATH", snapshot_path)
queue.ACCEPTANCE_FENCES.clear()
queue.BUDGET_ROOT_FENCES.clear()
queue.ADMISSION_RESERVATIONS.clear()
task_id = "restore-over-corrupt-intent-store"
write_task_result(tmp_path, task_id, "scheduled")
projection = tmp_path / "state" / "cancel_intents.json"
projection.parent.mkdir(parents=True, exist_ok=True)
corrupt_bytes = b'{"intents": [broken'
projection.write_bytes(corrupt_bytes)
task = {
"id": task_id,
"type": "task",
"chat_id": 1,
"depth": 0,
"budget_drive_root": str(tmp_path),
}
snapshot_path.parent.mkdir(parents=True, exist_ok=True)
snapshot_path.write_text(
json.dumps({
"ts": utc_now_iso(),
"pending": [{"id": task_id, "queue_seq": 1, "task": task}],
"running": [],
"acceptance_fences": [],
"budget_root_fences": [],
}),
encoding="utf-8",
)
assert queue.restore_pending_from_snapshot() == 1
assert [row["id"] for row in pending] == [task_id]
assert "_terminalization_retry" not in pending[0]
assert isinstance(pending[0].get("_cancel_intent_authority_hold"), dict)
assert load_task_result(tmp_path, task_id)["status"] == "scheduled"
assert projection.read_bytes() == corrupt_bytes
persisted = json.loads(snapshot_path.read_text(encoding="utf-8"))
persisted_task = persisted["pending"][0]["task"]
assert isinstance(persisted_task.get("_cancel_intent_authority_hold"), dict)
assert not isinstance(persisted_task.get("_terminalization_retry"), dict)
# Repairing the projection releases this authority hold; it must not turn
# the ordinary scheduled row into a synthetic terminal failure.
from supervisor import state, workers
delivered: list[dict] = []
worker = types.SimpleNamespace(
wid=1,
busy_task_id=None,
reaping=False,
in_q=types.SimpleNamespace(put=lambda row: delivered.append(dict(row))),
)
monkeypatch.setattr(workers, "DRIVE_ROOT", tmp_path)
monkeypatch.setattr(workers, "PENDING", pending)
monkeypatch.setattr(workers, "RUNNING", running)
monkeypatch.setattr(workers, "WORKERS", {1: worker})
monkeypatch.setattr(workers, "load_state", lambda: {})
monkeypatch.setattr(workers, "repo_writer_task_allowed", lambda _task: True)
monkeypatch.setattr(state, "budget_remaining", lambda *_args, **_kwargs: 100.0)
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda reason="": True)
queue.BUDGET_ROOT_FENCES.clear()
projection.unlink()
workers.assign_tasks()
assert pending == []
assert [row["id"] for row in delivered] == [task_id]
assert "_terminalization_retry" not in delivered[0]
assert "_cancel_intent_authority_hold" not in delivered[0]
assert load_task_result(tmp_path, task_id)["status"] == "scheduled"
def test_cancel_authority_hold_never_releases_a_terminal_row_to_dispatch(
tmp_path, monkeypatch,
):
"""A repaired hold distinguishes ordinary resume from terminal cleanup."""
from supervisor import queue, state, workers
task_id = "terminal-under-authority-hold"
task = {
"id": task_id,
"type": "task",
"chat_id": 1,
"depth": 0,
"_cancel_intent_authority_hold": {
"reason": "Cancel-intent authority is unreadable; dispatch is blocked.",
"held_at": "2026-08-28T00:00:00+00:00",
},
}
pending = [task]
running: dict = {}
delivered: list[dict] = []
worker = types.SimpleNamespace(
wid=1,
busy_task_id=None,
reaping=False,
in_q=types.SimpleNamespace(put=lambda row: delivered.append(dict(row))),
)
monkeypatch.setattr(workers, "DRIVE_ROOT", tmp_path)
monkeypatch.setattr(workers, "PENDING", pending)
monkeypatch.setattr(workers, "RUNNING", running)
monkeypatch.setattr(workers, "WORKERS", {1: worker})
monkeypatch.setattr(workers, "load_state", lambda: {})
monkeypatch.setattr(workers, "repo_writer_task_allowed", lambda _task: True)
monkeypatch.setattr(state, "budget_remaining", lambda *_args, **_kwargs: 100.0)
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda reason="": True)
monkeypatch.setattr(
workers,
"_emit_task_done_terminal",
lambda *_args, **_kwargs: pytest.fail("authority hold does not own terminal events"),
)
queue.BUDGET_ROOT_FENCES.clear()
write_task_result(tmp_path, task_id, STATUS_CANCELLED, result="cancelled")
workers.assign_tasks()
assert pending == []
assert delivered == []
assert worker.busy_task_id is None
assert load_task_result(tmp_path, task_id)["status"] == STATUS_CANCELLED
def test_preserve_pending_shutdown_keeps_cancel_authority_hold_nonterminal(
qenv, monkeypatch,
):
"""A restart hold is queue custody, never a synthetic failed outcome."""
from supervisor import queue, workers
task_id = "held-through-planned-restart"
held = {
"id": task_id,
"type": "task",
"chat_id": 0,
"depth": 0,
"_cancel_intent_authority_hold": {
"reason": "Cancel-intent authority is unreadable; dispatch is blocked.",
"held_at": "2026-08-28T00:00:00+00:00",
},
}
qenv.q.PENDING[:] = [held]
monkeypatch.setattr(workers, "PENDING", qenv.q.PENDING, raising=False)
monkeypatch.setattr(workers, "RUNNING", {}, raising=False)
monkeypatch.setattr(workers, "WORKERS", {}, raising=False)
monkeypatch.setattr(workers, "DRIVE_ROOT", qenv.drive, raising=False)
monkeypatch.setattr(workers, "_WORKER_POOL_DISABLED_REASON", "")
monkeypatch.setattr(queue, "persist_queue_snapshot", lambda reason="": True)
write_task_result(qenv.drive, task_id, "scheduled")
assert workers.kill_workers(
preserve_pending=True,
reconcile_delegate_custody=False,
) is True
assert qenv.q.PENDING == [held]
assert "_terminalization_retry" not in qenv.q.PENDING[0]
assert load_task_result(qenv.drive, task_id)["status"] == "scheduled"
def test_steering_is_refused_while_a_cancel_intent_is_active(tmp_path, monkeypatch):
"""A1.8: no NEW steering writes into a task whose cancellation is pending."""
import supervisor.events as events_mod
from ouroboros.owner_mailbox import drain_owner_messages
from supervisor.events import _handle_steer_task
ci.request_cancel(tmp_path, "steerme", reason="tearing down")
receipts: list = []
monkeypatch.setattr(
events_mod, "_emit_routing_receipt",
lambda ctx, evt, **kw: receipts.append(kw) or {},
)
sent: list = []
ctx = types.SimpleNamespace(
DRIVE_ROOT=tmp_path,
RUNNING={"steerme": {"task": {"id": "steerme", "chat_id": 1}}},
PENDING=[],
get_chat_agent=lambda: None,
send_with_budget=lambda *a, **k: sent.append(a),
persist_queue_snapshot=lambda **_kw: True,
)
_handle_steer_task(
{"target_task_id": "steerme", "message": "new orders", "chat_id": 1}, ctx,
)
assert receipts and receipts[0]["status"] == "rejected"
assert receipts[0]["reason"] == "cancel_pending"
assert drain_owner_messages(tmp_path, "steerme") == []
assert sent and "cancellation is pending" in sent[0][1]
def test_drop_cancelled_pending_stamps_the_decision_and_honors_the_stored_status(
qenv, monkeypatch,
):
"""A-F4: the pre-assignment drop follows custody's rules."""
from supervisor import workers
emitted: list = []
monkeypatch.setattr(workers, "_emit_task_done_terminal",
lambda task, tid, status, **kw: emitted.append((tid, status)) or True)
monkeypatch.setattr(workers, "PENDING", qenv.q.PENDING, raising=False)
monkeypatch.setattr(workers, "DRIVE_ROOT", qenv.drive, raising=False)
qenv.q.PENDING[:] = [
{"id": "drop-decided", "chat_id": 1},
{"id": "drop-completed", "chat_id": 1},
]
write_task_result(qenv.drive, "drop-decided", "scheduled")
ci.request_cancel(qenv.drive, "drop-decided", reason="parent stopped the plan",
requested_by="parent7")
# This one finished on its own between the intent and the drop.
write_task_result(qenv.drive, "drop-completed", "scheduled")
ci.request_cancel(qenv.drive, "drop-completed")
write_task_result(qenv.drive, "drop-completed", STATUS_COMPLETED, result="won the race")
workers._drop_cancelled_pending()
decided = load_task_result(qenv.drive, "drop-decided")
assert decided["status"] == STATUS_CANCELLED
assert decided["parent_decision"] == "cancelled"
assert decided["parent_decision_reason"] == "parent stopped the plan"
# Completion wins: the stored status is what the card resolves to.
assert load_task_result(qenv.drive, "drop-completed")["status"] == STATUS_COMPLETED
assert ("drop-completed", STATUS_COMPLETED) in emitted
assert ("drop-decided", STATUS_CANCELLED) in emitted
def test_drop_cancelled_pending_retains_custody_until_task_done_is_published(
qenv, monkeypatch,
):
"""A durable cancellation without task_done remains non-dispatchable."""
from supervisor import workers
emitted: list = []
publish_results = iter([False, True])
snapshots: list[str] = []
def publish(_task, task_id, status, **_kwargs):
emitted.append((task_id, status))
return next(publish_results)
monkeypatch.setattr(workers, "_emit_task_done_terminal", publish)
monkeypatch.setattr(workers, "PENDING", qenv.q.PENDING, raising=False)
monkeypatch.setattr(workers, "DRIVE_ROOT", qenv.drive, raising=False)
monkeypatch.setattr(
qenv.q,
"persist_queue_snapshot",
lambda reason="": snapshots.append(reason) or True,
)
qenv.q.PENDING[:] = [{"id": "drop-event-gap", "chat_id": 1, "depth": 0}]
write_task_result(qenv.drive, "drop-event-gap", "scheduled")
ci.request_cancel(
qenv.drive,
"drop-event-gap",
reason="parent stopped the plan",
requested_by="parent7",
)
workers._drop_cancelled_pending()
assert [row["id"] for row in qenv.q.PENDING] == ["drop-event-gap"]
retry = qenv.q.PENDING[0]["_terminalization_retry"]
assert retry["status"] == STATUS_CANCELLED
assert retry["trigger"] == "pending_cancel_event"
assert retry["reconcile_delegate_custody"] is False
assert snapshots == ["pending_terminal_event_retry"]
stored = load_task_result(qenv.drive, "drop-event-gap")
assert stored["status"] == STATUS_CANCELLED
assert stored["parent_decision"] == "cancelled"
assert ci.active_intent(qenv.drive, "drop-event-gap") is None
assert workers._retry_terminalization_pending() == (["drop-event-gap"], [])
assert qenv.q.PENDING == []
assert emitted == [
("drop-event-gap", STATUS_CANCELLED),
("drop-event-gap", STATUS_CANCELLED),
]
@pytest.mark.parametrize("settle_failure", ["returns_none", "raises"])
def test_drop_cancelled_pending_releases_a_failed_intent_claim(
qenv, monkeypatch, settle_failure,
):
"""A failed intent settle cannot strand a claim while event custody retries."""
from supervisor import workers
emitted: list[tuple[str, str]] = []
publish_results = iter([False, True])
def publish(_task, task_id, status, **_kwargs):
emitted.append((task_id, status))
return next(publish_results)
monkeypatch.setattr(workers, "_emit_task_done_terminal", publish)
monkeypatch.setattr(workers, "PENDING", qenv.q.PENDING, raising=False)
monkeypatch.setattr(workers, "DRIVE_ROOT", qenv.drive, raising=False)
monkeypatch.setattr(
qenv.q,
"persist_queue_snapshot",
lambda reason="": True,
)
def failed_settle(*_args, **_kwargs):
if settle_failure == "raises":
raise OSError("intent projection temporarily unavailable")
return None
released: list[dict] = []
real_release = ci.release_claim
def release_claim(root, task_id, **kwargs):
released.append({"task_id": task_id, **kwargs})
return real_release(root, task_id, **kwargs)
monkeypatch.setattr(ci, "settle_intent", failed_settle)
monkeypatch.setattr(ci, "release_claim", release_claim)
task_id = "drop-settle-gap"
qenv.q.PENDING[:] = [{"id": task_id, "chat_id": 1, "depth": 0}]
write_task_result(qenv.drive, task_id, "scheduled")
ci.request_cancel(qenv.drive, task_id, reason="parent stopped the plan")
workers._drop_cancelled_pending()
assert released and released[0]["task_id"] == task_id
intent = ci.active_intent(qenv.drive, task_id)
assert intent is not None
assert intent["state"] == ci.INTENT_REQUESTED
assert "claim_owner" not in intent
assert intent["last_error"] == "pending-drop intent settlement failed"
retry = qenv.q.PENDING[0]["_terminalization_retry"]
assert retry["status"] == STATUS_CANCELLED
assert retry["trigger"] == "pending_cancel_event"
assert retry["reconcile_delegate_custody"] is False
# The durable result/event retry is independent of the re-opened intent;
# the watchdog can settle that intent on its next custody pass.
assert workers._retry_terminalization_pending() == ([task_id], [])
assert qenv.q.PENDING == []
assert emitted == [(task_id, STATUS_CANCELLED), (task_id, STATUS_CANCELLED)]
intent = ci.active_intent(qenv.drive, task_id)
assert intent is not None and intent["state"] == ci.INTENT_REQUESTED
def test_drop_cancelled_pending_does_not_assume_settled_when_settle_helper_missing(
qenv, monkeypatch,
):
"""A missing settle helper must retain the active claim for a later retry."""
from supervisor import workers
task_id = "drop-missing-settle"
emitted: list[tuple[str, str]] = []
monkeypatch.setattr(workers, "PENDING", qenv.q.PENDING, raising=False)
monkeypatch.setattr(workers, "DRIVE_ROOT", qenv.drive, raising=False)
monkeypatch.setattr(
workers,
"_emit_task_done_terminal",
lambda _task, tid, status, **_kwargs: emitted.append((tid, status)) or True,
)
monkeypatch.setattr(qenv.q, "persist_queue_snapshot", lambda reason="": True)
qenv.q.PENDING[:] = [{"id": task_id, "chat_id": 1, "depth": 0}]
write_task_result(qenv.drive, task_id, "scheduled")
ci.request_cancel(qenv.drive, task_id, reason="parent stopped the plan")
real_settle = ci.settle_intent
real_release = ci.release_claim
monkeypatch.setattr(ci, "settle_intent", None)
monkeypatch.setattr(ci, "release_claim", lambda *_args, **_kwargs: False)
workers._drop_cancelled_pending()
assert [row["id"] for row in qenv.q.PENDING] == [task_id]
retry = qenv.q.PENDING[0]["_terminalization_retry"]
assert retry["trigger"] == "pending_cancel_intent"
assert retry["event_published"] is True
intent = ci.active_intent(qenv.drive, task_id)
assert intent is not None and intent["state"] == ci.INTENT_CLAIMED
assert emitted == [(task_id, STATUS_CANCELLED)]
# Restore the real helpers: the retained claim and marker can now finish
# without emitting a second terminal event.
monkeypatch.setattr(ci, "settle_intent", real_settle)
monkeypatch.setattr(ci, "release_claim", real_release)
assert workers._retry_terminalization_pending() == ([task_id], [])
assert qenv.q.PENDING == []
intent = ci.active_intent(qenv.drive, task_id)
assert intent is not None and intent["state"] == ci.INTENT_REQUESTED
assert emitted == [(task_id, STATUS_CANCELLED)]
def test_drop_cancelled_pending_leaves_the_intent_open_when_the_write_fails(
qenv, monkeypatch,
):
"""A-F4: never publish a cancellation that is not on disk."""
from supervisor import workers
emitted: list = []
monkeypatch.setattr(workers, "_emit_task_done_terminal",
lambda task, tid, status, **kw: emitted.append((tid, status)) or True)
monkeypatch.setattr(workers, "PENDING", qenv.q.PENDING, raising=False)
monkeypatch.setattr(workers, "DRIVE_ROOT", qenv.drive, raising=False)
monkeypatch.setattr(
"ouroboros.task_results.write_task_result",
lambda *_a, **_kw: (_ for _ in ()).throw(OSError("disk full")),
)
qenv.q.PENDING[:] = [{"id": "drop-nowrite", "chat_id": 1}]
write_task_result(qenv.drive, "drop-nowrite", "scheduled")
ci.request_cancel(qenv.drive, "drop-nowrite")
workers._drop_cancelled_pending()
assert qenv.q.PENDING == [], "it must not be assigned to a worker"
assert emitted == [], "no task_done for a cancellation that never persisted"
assert ci.active_intent(qenv.drive, "drop-nowrite") is not None
def test_steering_refusal_covers_the_legacy_latch_too(tmp_path, monkeypatch):
"""A-F19: a pre-migration wedged task must not accept new owner messages."""
import supervisor.events as events_mod
from ouroboros.owner_mailbox import drain_owner_messages
from supervisor.events import _handle_steer_task
write_task_result(tmp_path, "legacy-steer", STATUS_CANCEL_REQUESTED, result="wedged")
receipts: list = []
monkeypatch.setattr(events_mod, "_emit_routing_receipt",
lambda ctx, evt, **kw: receipts.append(kw) or {})
sent: list = []
ctx = types.SimpleNamespace(
DRIVE_ROOT=tmp_path,
RUNNING={"legacy-steer": {"task": {"id": "legacy-steer", "chat_id": 1}}},
PENDING=[],
get_chat_agent=lambda: None,
send_with_budget=lambda *a, **k: sent.append(a),
persist_queue_snapshot=lambda **_kw: True,
)
_handle_steer_task(
{"target_task_id": "legacy-steer", "message": "new orders", "chat_id": 1}, ctx,
)
assert receipts and receipts[0]["reason"] == "cancel_pending"
assert drain_owner_messages(tmp_path, "legacy-steer") == []
def test_drop_cancelled_pending_yields_to_a_live_claim_owner(qenv, monkeypatch):
"""AR2-2: the pre-assignment drop CLAIMS before it settles. A live custody's
claim wins — assignment retains the authoritative pending row and aborts,
then the claim owner captures/removes that same row."""
from supervisor import workers
emitted: list = []
monkeypatch.setattr(workers, "_emit_task_done_terminal",
lambda task, tid, status, **kw: emitted.append((tid, status)) or True)
monkeypatch.setattr(workers, "PENDING", qenv.q.PENDING, raising=False)
monkeypatch.setattr(workers, "DRIVE_ROOT", qenv.drive, raising=False)
qenv.q.PENDING[:] = [{"id": "drop-owned", "chat_id": 1}]
write_task_result(qenv.drive, "drop-owned", "scheduled")
ci.request_cancel(qenv.drive, "drop-owned")
ci.claim_intent(qenv.drive, "drop-owned", owner="cancel_task_custody") # live owner
assert workers._drop_cancelled_pending() is False
assert [row["id"] for row in qenv.q.PENDING] == ["drop-owned"]
assert emitted == [], "the claim owner emits, not the drop"
assert load_task_result(qenv.drive, "drop-owned")["status"] == "scheduled"
intent = ci.active_intent(qenv.drive, "drop-owned")
assert intent["state"] == ci.INTENT_CLAIMED
assert intent["claim_owner"] == "cancel_task_custody"
assert ci.release_claim(qenv.drive, "drop-owned", error="test owner resumes")
assert qenv.tl.cancel_task_custody("drop-owned") == qenv.tl.CANCEL_CANCELLED
assert qenv.q.PENDING == []
def test_drop_cancelled_pending_defers_when_intent_vanishes_before_settle(
qenv, monkeypatch,
):
"""A changed claim aborts the whole assignment pass, not just the drop."""
from supervisor import queue, state, workers
task_id = "drop-intent-race"
pending = [{"id": task_id, "type": "task", "chat_id": 1, "depth": 0}]
delivered: list[dict] = []
worker = types.SimpleNamespace(
wid=1,
busy_task_id=None,
reaping=False,
in_q=types.SimpleNamespace(put=lambda task: delivered.append(dict(task))),
)
monkeypatch.setattr(workers, "PENDING", pending, raising=False)
monkeypatch.setattr(workers, "RUNNING", {}, raising=False)
monkeypatch.setattr(workers, "WORKERS", {1: worker}, raising=False)
monkeypatch.setattr(workers, "DRIVE_ROOT", qenv.drive, raising=False)
monkeypatch.setattr(workers, "load_state", lambda: {})
monkeypatch.setattr(
state,
"budget_remaining",
lambda *_args, **_kwargs: pytest.fail(
"budget must not run after cancellation custody changes"
),
)
snapshots: list[str] = []
monkeypatch.setattr(
queue,
"persist_queue_snapshot",
lambda reason="": snapshots.append(reason) or True,
)
queue.BUDGET_ROOT_FENCES.clear()
write_task_result(qenv.drive, task_id, "scheduled")
first = ci.request_cancel(qenv.drive, task_id, reason="old request")
real_settle = ci.settle_intent
def vanished_claim(root, tid, *, owner):
assert owner == "pending_drop"
real_settle(
root, tid, outcome="cancelled",
expected_generation=first["generation"], request_id=first["request_id"],
)
ci.request_cancel(root, tid, reason="new request", source="race")
return None
monkeypatch.setattr(ci, "claim_intent", vanished_claim)
workers.assign_tasks()
assert [row["id"] for row in pending] == [task_id]
assert delivered == []
assert worker.busy_task_id is None
assert load_task_result(qenv.drive, task_id)["status"] == "scheduled"
replacement = ci.active_intent(qenv.drive, task_id)
assert replacement is not None
assert replacement["state"] == ci.INTENT_REQUESTED
assert replacement["reason"] == "new request"
assert snapshots == ["cancellation_authority_indeterminate"]
def test_snapshot_restore_consults_the_intent_projection_under_the_queue_lock(
qenv, monkeypatch,
):
"""AR2-10 (§8-A1): the projection read at restore holds the queue lock, so
the "no active intent" view and the enqueue are one serialized step."""
from ouroboros.utils import utc_now_iso
consults: list = []
def _spy(root, tid, *, strict=False):
consults.append(qenv.q._queue_lock._is_owned())
return True # refusal path: no enqueue side effects in this harness
monkeypatch.setattr("ouroboros.cancel_intents.has_active_intent", _spy)
snapshot = {
"ts": utc_now_iso(),
"pending": [{"task": {"id": "locked-restore", "chat_id": 1, "type": "chat"}}],
"running": [],
"acceptance_fences": [],
"budget_root_fences": [],
}
state_dir = qenv.drive / "state"
state_dir.mkdir(parents=True, exist_ok=True)
(state_dir / "queue_snapshot.json").write_text(json.dumps(snapshot), encoding="utf-8")
monkeypatch.setattr(qenv.q, "QUEUE_SNAPSHOT_PATH", state_dir / "queue_snapshot.json",
raising=False)
assert qenv.q.restore_pending_from_snapshot() == 0
assert qenv.q.PENDING == []
assert consults == [True], "the intent consult must hold the queue lock"
def test_steer_refusal_removes_the_just_staged_attachments(tmp_path, monkeypatch):
"""GR2-9: a steering message refused by the transactional cancel re-check
must not leave its just-staged input files in the dying task's store."""
import supervisor.events as events_mod
import supervisor.queue as queue_mod
from supervisor.events import _handle_steer_task
monkeypatch.setattr(queue_mod, "DRIVE_ROOT", tmp_path)
write_task_result(tmp_path, "steer-stage", STATUS_RUNNING, result="working")
source = tmp_path / "owner-input.txt"
source.write_text("owner attachment", encoding="utf-8")
# The up-front check passes (no cancel yet); the cancel ingress lands in the
# window before the transactional re-check — exactly the staged-then-refused
# shape the fix removes.
checks = {"n": 0}
real_pending = ci.cancel_pending
def _racing_cancel_pending(root, tid):
checks["n"] += 1
if checks["n"] == 2 and tid == "steer-stage":
ci.request_cancel(tmp_path, "steer-stage", reason="race")
return real_pending(root, tid)
monkeypatch.setattr("ouroboros.cancel_intents.cancel_pending", _racing_cancel_pending)
receipts: list = []
monkeypatch.setattr(events_mod, "_emit_routing_receipt",
lambda ctx, evt, **kw: receipts.append(kw) or {})
ctx = types.SimpleNamespace(
DRIVE_ROOT=tmp_path,
RUNNING={"steer-stage": {"task": {"id": "steer-stage", "chat_id": 1}}},
PENDING=[],
get_chat_agent=lambda: None,
send_with_budget=lambda *a, **k: None,
persist_queue_snapshot=lambda **_kw: True,
)
_handle_steer_task(
{"target_task_id": "steer-stage", "message": "new orders", "chat_id": 1,
"attachment_uploads": [{"path": str(source), "label": "input"}]},
ctx,
)
assert receipts and receipts[-1]["reason"] == "cancel_pending"
from ouroboros.artifacts import task_artifact_dir_path
attach_dir = task_artifact_dir_path(tmp_path, "steer-stage") / "attachments"
staged = list(attach_dir.glob("*")) if attach_dir.exists() else []
assert staged == [], f"staged inputs must be removed on refusal: {staged}"
from ouroboros.owner_mailbox import drain_owner_messages
assert drain_owner_messages(tmp_path, "steer-stage") == []
def _restart_snapshot(qenv, monkeypatch, *, running: list, pending: list = (), ts: str = ""):
"""Write a queue snapshot the way a shutdown left it and point restore at it."""
from ouroboros.utils import utc_now_iso
state_dir = qenv.drive / "state"
state_dir.mkdir(parents=True, exist_ok=True)
path = state_dir / "queue_snapshot.json"
path.write_text(json.dumps({
"ts": ts or utc_now_iso(),
"pending": [{"task": task} for task in pending],
"running": running,
"acceptance_fences": [],
"budget_root_fences": [],
}), encoding="utf-8")
monkeypatch.setattr(qenv.q, "QUEUE_SNAPSHOT_PATH", path, raising=False)
return path
SERVER_STOPPED_CANCEL = "Task cancelled: the server stopped while this task was still running."
def test_snapshot_restore_fences_a_surviving_running_row_for_custody(qenv, monkeypatch):
"""Q11=A: work the window killed ends as Cancelled — through the ONE settle
owner. Restore only mints the durable intent; the terminal write, the kill and
the reconcile stay with cancellation custody a watchdog window later, and the
card states the same fact the boot line states."""
import time
from supervisor import task_lifecycle
write_task_result(qenv.drive, "interrupted-root", STATUS_RUNNING, chat_id=1)
write_task_result(qenv.drive, "interrupted-child", STATUS_RUNNING, chat_id=1,
parent_task_id="interrupted-root", root_task_id="interrupted-root",
delegation_role="subagent")
_restart_snapshot(qenv, monkeypatch, running=[
{"id": "interrupted-root", "task": {"id": "interrupted-root", "chat_id": 1}},
{"id": "interrupted-child", "task": {"id": "interrupted-child", "chat_id": 1,
"delegation_role": "subagent",
"parent_task_id": "interrupted-root"}},
])
fenced: list = []
assert qenv.q.restore_pending_from_snapshot(terminalized=fenced) == 0
assert fenced == ["interrupted-root", "interrupted-child"]
# Restore is NOT a terminal writer: the row is untouched, the intent is durable.
assert load_task_result(qenv.drive, "interrupted-root")["status"] == STATUS_RUNNING
intent = ci.active_intent(qenv.drive, "interrupted-root")
assert intent and intent["reason"] == "server_shutdown"
assert intent["source"] == "snapshot_restore"
rows = [json.loads(line) for line in
(qenv.drive / "logs" / "supervisor.jsonl").read_text(encoding="utf-8").splitlines()]
restore_rows = [row for row in rows if row["type"] == "queue_restored_from_snapshot"]
assert restore_rows[-1]["terminalized_running"] == ["interrupted-root", "interrupted-child"]
assert restore_rows[-1]["restored_pending"] == 0
# A second boot before custody ran re-reads the same rows and mints nothing new.
second: list = []
assert qenv.q.restore_pending_from_snapshot(terminalized=second) == 0
assert second == []
assert ci.active_intent(qenv.drive, "interrupted-root")["request_id"] == intent["request_id"]
# The existing watchdog half terminalizes them, and its text names the cause
# the fence carried: a card that says "was neither queued nor running" would
# contradict the very fact that minted the intent.
outcomes = task_lifecycle.sweep_cancel_intents(now=time.time() + 60)
assert outcomes == {"interrupted-root": "cancelled", "interrupted-child": "cancelled"}
for task_id in ("interrupted-root", "interrupted-child"):
settled = load_task_result(qenv.drive, task_id)
assert settled["status"] == STATUS_CANCELLED
assert settled["result"] == SERVER_STOPPED_CANCEL
assert ci.active_intent(qenv.drive, task_id) is None
def test_snapshot_restore_leaves_owned_and_terminal_running_rows_alone(qenv, monkeypatch):
"""The fence is for rows nothing else owns: an active intent belongs to
cancellation custody, and a task that finished stays finished."""
write_task_result(qenv.drive, "already-owned", STATUS_RUNNING, chat_id=1)
owned = ci.request_cancel(qenv.drive, "already-owned", reason="owner pressed Stop")
write_task_result(qenv.drive, "finished", STATUS_COMPLETED, chat_id=1, result="done")
write_task_result(qenv.drive, "revived", "scheduled", chat_id=1)
_restart_snapshot(
qenv, monkeypatch,
running=[
{"id": "already-owned", "task": {"id": "already-owned", "chat_id": 1}},
{"id": "finished", "task": {"id": "finished", "chat_id": 1}},
{"id": "", "task": {}},
],
pending=[{"id": "revived", "chat_id": 1, "type": "chat"}],
)
fenced: list = []
assert qenv.q.restore_pending_from_snapshot(terminalized=fenced) == 1
assert fenced == []
assert [task["id"] for task in qenv.q.PENDING] == ["revived"]
assert ci.active_intent(qenv.drive, "already-owned")["request_id"] == owned["request_id"]
assert ci.active_intent(qenv.drive, "already-owned")["reason"] == "owner pressed Stop"
assert ci.active_intent(qenv.drive, "finished") is None
assert load_task_result(qenv.drive, "finished")["status"] == STATUS_COMPLETED
def test_snapshot_restore_fence_expires_the_open_quiz_and_closes_the_owner_wait(qenv, monkeypatch):
"""A restart that kills a task with an open question reuses the EXISTING
expiry state: no new 'lost to a restart' quiz state is invented."""
import time
from ouroboros import owner_quiz
from supervisor import queue_transitions, task_lifecycle, workers
events: list = []
monkeypatch.setattr(workers, "get_event_q",
lambda: types.SimpleNamespace(put=events.append), raising=False)
task_id = "asked-and-interrupted"
write_task_result(qenv.drive, task_id, STATUS_RUNNING, chat_id=1)
owner_quiz.record_asked(
qenv.drive, task_id, quiz_id="q1", question="Which folder?",
options=["A", "B"], wait_for_answer=True,
)
write_task_result(
qenv.drive, task_id, STATUS_RUNNING,
owner_wait={"state": "waiting", "quiz_id": "q1"},
)
_restart_snapshot(qenv, monkeypatch, running=[
{"id": task_id, "owner_wait": {"state": "waiting"}, "task": {"id": task_id, "chat_id": 1}},
])
fenced: list = []
assert qenv.q.restore_pending_from_snapshot(terminalized=fenced) == 0
assert fenced == [task_id]
assert owner_quiz.quiz_states(qenv.drive, task_id)["q1"]["state"] == "open"
assert task_lifecycle.sweep_cancel_intents(now=time.time() + 60)[task_id] == "cancelled"
stored = load_task_result(qenv.drive, task_id)
assert stored["status"] == STATUS_CANCELLED
# The lost quiz carries its cause in the cancel text, which is why no new
# "lost to a restart" quiz state exists.
assert stored["result"] == SERVER_STOPPED_CANCEL
# Custody publishes the terminal event; the supervisor's task-done seam
# (events_task_done -> reconcile_terminal_task_projections) is what closes
# the per-task owner-control projections, exactly as for any other terminal.
done = [event for event in events if event["type"] == "task_done"]
assert len(done) == 1 and done[0]["status"] == STATUS_CANCELLED
queue_transitions.reconcile_terminal_task_projections(qenv.drive, task_id)
assert owner_quiz.quiz_states(qenv.drive, task_id)["q1"]["state"] == "expired_terminal"
assert load_task_result(qenv.drive, task_id)["owner_wait"]["state"] == "expired_terminal"