test: publish the preparer's readiness only after its first control read (#1504)

tests/test_late_phase_pause_acceptance.py::test_preparing_acceptance_resume_keeps_pause_under_existing_rails[cancel]
failed twice on full-test (windows-latest) on 2026-10-04 with
`ok: True, late_phase: settled` where a refusal was expected. The child
process published preparer.json as soon as run_historical_acceptance
returned "preparing", but review_operation starts the preparation on a
background thread and returns without waiting for it, and that thread's
first control read is mandatory (_checked_at starts at minus infinity).
When the thread ran late, it read the cancellation the parent had just
recorded, refused the preparation (stop:historical, nothing dispatched),
and the later resume correctly found only settled work. The product
behaved as designed; the test raced its own child.

The child now publishes readiness only after the preparation thread's
first control read returned no stop, so the parent installs its rail
after it. No sleep is added and no assertion changes. With a 1 s delay
injected before the preparation's work and 1.5 s after the
cancellation (a scratch copy only), the unchanged test fails with the
exact Windows result and the changed one passed all six rail cases
three times; the module passes 15 of 15 without delays.

Root cause found by a gpt-6-astra scout lane; the first-read-only
assertion is a refinement so later control reads are passed through
untouched.
This commit is contained in:
Ouroboros 2026-10-05 20:32:51 +03:00
parent 13657849ce
commit 423738f399

View file

@ -120,7 +120,7 @@ def test_pause_defers_an_unsent_automatic_late_review_until_resume(late, tmp_pat
_PREPARER = '''
import json, sys, time
import json, sys, threading, time
from pathlib import Path
from types import SimpleNamespace
from ouroboros import acceptance_late, review_operation
@ -129,12 +129,24 @@ from ouroboros.llm import LLMClient
root, task_id, output = Path(sys.argv[1]), sys.argv[2], Path(sys.argv[3])
acceptance_late._historical_writer_live = lambda *_a, **_kw: True
review_operation._CONTROL_RECHECK_SEC = 3600.0
# Retaining the pointer does not mean the worker has finished its first control
# read. Publish readiness only after that read, before the parent installs a rail.
ready = threading.Event()
control = review_operation.ReviewOperation.control
def checked_control(self):
result = control(self)
if not ready.is_set():
assert result is None, result
ready.set()
return result
review_operation.ReviewOperation.control = checked_control
LLMClient.chat = lambda *_a, **_kw: (_ for _ in ()).throw(AssertionError("unexpected paid send"))
ctx = SimpleNamespace(task_id=task_id, task_attempt=1, drive_root=root, budget_drive_root=root,
task_metadata={"root_task_id": task_id, "budget_drive_root": str(root)}, event_queue=None, pending_events=[])
row = load_task_result(root, task_id, strict=True)
result = acceptance_late.run_historical_acceptance(ctx, task_id=task_id,
debt_id=row["acceptance_debt"]["debt_id"], automatic=True)
assert ready.wait(10), 'preparation worker did not reach its control wait'
temporary = output.with_suffix('.tmp')
temporary.write_text(json.dumps(result), encoding="utf-8")
temporary.replace(output)