Files
rune/tests/test_real_async_integration.py
T
conductor 16164c6f5e
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / documentation (push) Successful in 8s
CI Pipeline / lint (push) Failing after 29s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / type-check (push) Successful in 33s
CI Pipeline / lint (pull_request) Failing after 31s
CI Pipeline / best-practices (pull_request) Successful in 7s
CI Pipeline / unit-tests (pull_request) Failing after 37s
CI Pipeline / documentation (pull_request) Successful in 8s
CI Pipeline / canary (pull_request) Successful in 33s
CI Pipeline / gitea-offline (pull_request) Successful in 18s
CI Pipeline / type-check (pull_request) Successful in 34s
Spec gate (headless) / spec-gate (pull_request) Successful in 17s
deploy / deploy (push) Skipped
CI Pipeline / required-checks (pull_request) Failing after 2s
CI Pipeline / CI Pipeline / notify-event-bridge (pull_request) Successful in 8s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / unit-tests (push) Successful in 4m46s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 6s
style: ruff-format branch-added python files
CI lint's format-check on added files (base-range dependent) failed on
files that predate the format gate's effective coverage. Formatting-only:
no semantic changes; integration tests 15/15, ruff check clean, ratchet
PASS, daemon convergence re-verified.
2026-09-19 03:32:24 +01:00

240 lines
8.3 KiB
Python

#!/usr/bin/env python3
"""Frozen tests for REAL_ASYNC_BRIDGE final integration (§2-§5, §9).
State ownership (§2/§3):
- self-resuming waiters register NO resume_cmd (no daemon delivery role)
- a terminal operation state is never overwritten by session delivery
Orphan recovery (§4):
- RESUMING waits past the orphan grace are reconciled truthfully and
their leases released (failure detection, deterministic)
CLI contract (§5):
- every flag is supported or loudly rejected (exit 2) — no silent loss
Task binding (§9):
- task-bound resolution scopes to the bound operation; stored state
wins over model hints
"""
from __future__ import annotations
import importlib
import json
import os
import sys
import tempfile
import time
import unittest
from pathlib import Path
from unittest import mock
REPO = Path(__file__).resolve().parent.parent
SCRIPTS = REPO / "scripts"
DAEMON_SRC = REPO / "bridge" / "daemon"
for p in (str(SCRIPTS), str(DAEMON_SRC), str(REPO / "src")):
if p not in sys.path:
sys.path.insert(0, p)
class _IsolatedRoot(unittest.TestCase):
"""Each test gets an isolated WR_ROOT + reloaded modules."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
root = Path(self._tmp.name) / "wr"
for sub in ("waits", "events", "leases", "resumes"):
(root / sub).mkdir(parents=True)
self.root = root
self._env = {"WR_ROOT": str(root), "RUNE_WR_ROOT": str(root)}
self._patcher = mock.patch.dict(os.environ, self._env, clear=False)
self._patcher.start()
def tearDown(self):
self._patcher.stop()
self._tmp.cleanup()
def _waitstore(self):
import waitstore
importlib.reload(waitstore)
return waitstore
def _daemon(self):
# daemon caches waitstore at import; reload AFTER waitstore so its
# binding follows this test's isolated root
import daemon as wrd
importlib.reload(wrd)
return wrd
def _new_wait(self, ws, *, session_id, status, task_id="", resume_cmd=None, stale_secs=0, sha="ab" * 20, **kw):
rec = ws.new_wait(
session_id=session_id,
provider="gitea",
event_type="ci_completed",
repo="conductor/rune",
sha=sha,
run_ref=f"ci-{sha}",
target_kind="headless",
**kw,
)
rec["status"] = status
rec["ext"] = {"operation_id": rec["run_ref"], "task_id": task_id}
if resume_cmd is not None:
rec["resume_cmd"] = resume_cmd
if stale_secs:
rec["last_updated"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - stale_secs))
ws.save(rec)
return rec
class TestResumeModeSelf(_IsolatedRoot):
"""§3: self-resuming waiters own their continuation."""
def test_self_mode_registers_no_resume_cmd(self):
import importlib as il
os.environ["RUNE_WR_CODE"] = str(DAEMON_SRC)
import rune.event_wait as ew
il.reload(ew)
rec = ew.register_process_wait("ci-selftest0123", project="rune", resume_mode="self")
self.assertIsNone(rec.get("resume_cmd"), "self-mode must not register a delivery worker")
def test_default_mode_preserves_fifo_semantics(self):
import importlib as il
os.environ["RUNE_WR_CODE"] = str(DAEMON_SRC)
import rune.event_wait as ew
il.reload(ew)
rec = ew.register_process_wait("ci-fifotest01234")
self.assertIsNotNone(rec.get("resume_cmd"), "T15 builder semantics must be preserved")
class TestTerminalGuard(_IsolatedRoot):
"""§2: session delivery never overwrites terminal operation truth."""
def test_resume_skips_terminal_wait(self):
ws = self._waitstore()
wrd = self._daemon()
rec = self._new_wait(ws, session_id="s-term", status="COMPLETED", resume_cmd=["/bin/true"])
rec["verdict"] = "success"
ws.save(rec)
wrd.resume(rec)
fresh = ws.load(rec["wait_id"])
self.assertEqual(fresh["status"], "COMPLETED", "terminal state was downgraded by delivery path")
leases = list((self.root / "leases").glob("*.json"))
self.assertEqual(leases, [], "lease created for terminal wait")
class TestOrphanSweep(_IsolatedRoot):
"""§4: bounded mid-run reconciliation of orphaned resume flows."""
def test_stale_resuming_with_dead_owner_recovered(self):
ws = self._waitstore()
wrd = self._daemon()
wrd.ORPHAN_GRACE_SECS = 5 # deterministic, small
rec = self._new_wait(ws, session_id="s-orphan", status="RESUMING", resume_cmd=["/bin/true"])
rec["resume_attempts"] = 1
ws.save(rec)
# backdate AFTER save: save() stamps last_updated=now
stale = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - 60))
rec["last_updated"] = stale
(ws.WAITS / f"{rec['wait_id']}.json").write_text(json.dumps(rec))
# dead-owner lease
lease = {
"session_id": "s-orphan",
"wait_id": rec["wait_id"],
"pid": 2**22,
"pgid": 2**22,
"pid_starttime": "",
"acquired_at": "2020-01-01T00:00:00Z",
}
ws.lease_path("s-orphan").write_text(json.dumps(lease))
healed = wrd.orphan_sweep()
self.assertEqual(healed, 1)
fresh = ws.load(rec["wait_id"])
self.assertIn(fresh["status"], ("READY_TO_RETRY", "RESUME_UNVERIFIED"))
self.assertIn("orphaned mid-run", fresh.get("resume_note", ""))
self.assertFalse(ws.lease_path("s-orphan").exists())
def test_fresh_resuming_untouched(self):
ws = self._waitstore()
wrd = self._daemon()
wrd.ORPHAN_GRACE_SECS = 3600
rec = self._new_wait(ws, session_id="s-live", status="RESUMING", resume_cmd=["/bin/true"])
self.assertEqual(wrd.orphan_sweep(), 0)
self.assertEqual(ws.load(rec["wait_id"])["status"], "RESUMING")
def _waitstore(self):
import waitstore
importlib.reload(waitstore)
return waitstore
class TestCliContract(unittest.TestCase):
"""§5: no silent argument loss."""
def test_unknown_flag_rejected_loudly(self):
import ci_event_wait as cew
with mock.patch.object(sys, "argv", ["ci_event_wait.py", "--sha", "a" * 40, "--watchdoog", "5"]):
rc = cew.main()
self.assertEqual(rc, 2, "unknown flag must exit 2, not be ignored")
def test_positional_rejected_loudly(self):
import ci_event_wait as cew
with mock.patch.object(sys, "argv", ["ci_event_wait.py", "extra"]):
rc = cew.main()
self.assertEqual(rc, 2)
def test_watchdog_flag_reaches_run(self):
import ci_event_wait as cew
with (
mock.patch.object(cew, "run", return_value=0) as run_mock,
mock.patch.object(sys, "argv", ["ci_event_wait.py", "--sha", "a" * 40, "--watchdog", "123"]),
):
rc = cew.main()
self.assertEqual(rc, 0)
self.assertEqual(run_mock.call_args.kwargs.get("watchdog_secs"), 123)
class TestTaskBinding(_IsolatedRoot):
"""§9: task-bound resolution; stored operation wins."""
def _bind_env(self):
import await_intent as ai
importlib.reload(ai)
return ai
def test_task_bound_operation_resolved(self):
ws = self._waitstore()
self._new_wait(ws, session_id="s-stale", status="WAITING_EXTERNAL")
bound = self._new_wait(ws, session_id="s-bound", status="WAITING_EXTERNAL", task_id="T9", sha="cd" * 20)
ai = self._bind_env()
norm = ai.normalize_action({"kind": "await_operation"}, task_id="T9")
self.assertTrue(norm.get("normalized"), norm)
self.assertEqual(norm.get("operation_id"), bound["run_ref"])
self.assertEqual(norm.get("matched_by", norm.get("evidence")), norm.get("matched_by", norm.get("evidence")))
def test_unbound_task_reports_missing(self):
ws = self._waitstore()
self._new_wait(ws, session_id="s-stale2", status="WAITING_EXTERNAL")
ai = self._bind_env()
norm = ai.normalize_action({"kind": "await_operation"}, task_id="T404")
self.assertEqual(norm.get("normalized"), False)
self.assertIn("T404", norm.get("reason", ""))
def _waitstore(self):
import waitstore
importlib.reload(waitstore)
return waitstore
if __name__ == "__main__":
unittest.main()