diff --git a/scripts/await_intent.py b/scripts/await_intent.py index 570d4d54..82b57ba7 100644 --- a/scripts/await_intent.py +++ b/scripts/await_intent.py @@ -233,16 +233,42 @@ def normalize_action(action: dict, *, model_op_type_ignored, "explicit_id", user_deadline, contract_deadline) - # 2. resolve by sha / active operation + # 2. resolve by sha / active operation. + # H-PATCH H2/H3: the model sha is a HINT. If it doesn't resolve but + # unambiguous pending operations exist, the STORED target wins — a + # mistyped model sha must never create a ghost operation (the A-patch + # live failure: a typo'd sha derived branch_ci for a nonexistent commit + # and parked a wait on a ghost). candidates = pending_operations(sha=sha) if len(candidates) > 1 and not sha: return {"normalized": "AMBIGUOUS", "candidates": [_rec_operation_id(c) for c in candidates], "reason": "multiple pending operations, no target"} if len(candidates) == 1: - return _canonical_from_record(candidates[0], model_deadline_ignored, + rec = candidates[0] + stored_sha = _rec_sha(rec) + norm = _canonical_from_record(rec, model_deadline_ignored, model_op_type_ignored, "pending_match", user_deadline, contract_deadline) + if sha and stored_sha and sha != stored_sha: + norm["model_sha_mismatch"] = { + "model_sha": sha, "stored_sha": stored_sha, + "resolution": "stored_target_used"} + return norm + # model sha unresolved: any unambiguous pending op at all? then the + # model probably mistyped the sha — use the stored target, log it. + all_pending = pending_operations() + if len(all_pending) == 1 and sha: + rec = all_pending[0] + stored_sha = _rec_sha(rec) + norm = _canonical_from_record(rec, model_deadline_ignored, + model_op_type_ignored, + "unresolved_sha_single_pending", + user_deadline, contract_deadline) + norm["model_sha_mismatch"] = { + "model_sha": sha, "stored_sha": stored_sha, + "resolution": "stored_target_used"} + return norm # 3. no stored record: derive truth deterministically if not sha: @@ -337,7 +363,8 @@ def normalize_bash(command: str, *, sha_hint: str = "") -> dict | None: if rec is not None: norm = _canonical_from_record(rec, True, False, "sleep_poll_intent", None, None) - else: + norm["sha_for_wait"] = _rec_sha(rec) + elif sha: op_type, evidence = derive_op_type(sha) norm = { "normalized": True, "action": "ContinuePendingOperation", @@ -348,8 +375,10 @@ def normalize_bash(command: str, *, sha_hint: str = "") -> dict | None: "deadline_secs": POLICY_DEADLINES[op_type], "deadline_provenance": "GOVERNED_OPERATION_POLICY", "model_deadline_ignored": True, "model_op_type_ignored": False, - "evidence": evidence, + "evidence": evidence, "sha_for_wait": sha, } + else: + return None norm["raw_command_kind"] = "sleep_poll" norm["raw_sleep_seconds_suppressed"] = secs return norm diff --git a/scripts/ci_event_wait.py b/scripts/ci_event_wait.py index 4936abaf..7ac2973f 100644 --- a/scripts/ci_event_wait.py +++ b/scripts/ci_event_wait.py @@ -55,20 +55,23 @@ REPO_ROOT = _SCRIPT_DIR.parent QUALIFY_SCRIPT = _SCRIPT_DIR / "deploy_qualify.py" -def authority_verdict() -> str: +def authority_verdict(sha: str = "") -> str: """ONE-SHOT canonical Gitea verdict via deploy_qualify.py — the only - completion truth. Never parses the event payload.""" + completion truth. Never parses the event payload. + + H-PATCH H4: the target sha is passed EXPLICITLY (as the subprocess's + --sha argument) — ambient DEPLOY_SHA can no longer change which + operation is verified. The env var remains only as a CLI backward + compat for deploy_qualify.py invoked standalone.""" + cmd = [sys.executable, str(QUALIFY_SCRIPT)] + if sha: + cmd += ["--sha", sha] proc = subprocess.run( - [sys.executable, str(QUALIFY_SCRIPT)], + cmd, capture_output=True, text=True, timeout=120, - env=dict(os.environ, PYTHONPATH=""), + env={k: v for k, v in os.environ.items() if k != "DEPLOY_SHA"} + if sha else dict(os.environ, PYTHONPATH=""), ) - # T21.3: GITEA_TOKEN from deploy.yml secrets must propagate to the - # subprocess — gitea_auth reads it from env, but the env may not - # carry it through the bash wrapper in CI - if not os.environ.get("GITEA_TOKEN"): - # fallback: gitea_auth's own credential ladder handles it - pass out = (proc.stdout or "").strip().splitlines() return (out[-1] if out else "pending").strip() @@ -169,7 +172,7 @@ def op_authority_verdict(op_type: str, sha: str) -> str: spec = op_spec(op_type) authority = spec["authority"] if authority == "deploy_qualify": - v = normalize_verdict(authority_verdict()) + v = normalize_verdict(authority_verdict(sha)) if v != "pending": return v # E-PATCH resilience: Gitea's combined commit-status endpoint can @@ -208,7 +211,7 @@ def _ci_runs_verdict(spec: dict, sha: str) -> str: getattr(gitea_auth, "DEFAULT_SERVER", "")).rstrip("/") repo = os.environ.get("GITEA_REPO", getattr(gitea_auth, "DEFAULT_REPO", "")) - sha_env = os.environ.get("DEPLOY_SHA", "") or sha + sha_env = sha # H-PATCH: explicit parameter is the only authority token = gitea_auth.get_token() if gitea_auth.has_token() else "" if not (server and repo and sha_env and token): return "pending" # cannot verify -> not verified (fail closed) @@ -244,7 +247,7 @@ def op_ci_alive(op_type: str, sha: str) -> bool: getattr(gitea_auth, "DEFAULT_SERVER", "")).rstrip("/") repo = os.environ.get("GITEA_REPO", getattr(gitea_auth, "DEFAULT_REPO", "")) - sha_env = os.environ.get("DEPLOY_SHA", "") or sha + sha_env = sha # H-PATCH: explicit parameter is the only authority token = gitea_auth.get_token() if gitea_auth.has_token() else "" if not (server and repo and sha_env and token): return False @@ -266,6 +269,7 @@ def op_ci_alive(op_type: str, sha: str) -> bool: def authority_after_wake(op_type: str = DEFAULT_OP_TYPE, sha: str = "", max_tries: int = 3, gap_secs: int = 4) -> str: + """(sha is EXPLICIT — ambient env never selects the target.)""" """Post-wake authority read. The daemon's own wake sweep uses a bounded event-triggered retry (4s ×3) for the job-finished-microseconds- ago ack window; a verdict read here can hit the same window. This is a diff --git a/scripts/deploy_qualify.py b/scripts/deploy_qualify.py index 745701a8..182cc657 100644 --- a/scripts/deploy_qualify.py +++ b/scripts/deploy_qualify.py @@ -106,12 +106,25 @@ def _fetch_status(server: str, repo: str, sha: str, token: str) -> list[dict] | return None if data is None else data.get("statuses", []) -def _fetch_runs(server: str, repo: str, sha: str, token: str) -> list[dict] | None: - data = _get_json(f"{server}/api/v1/repos/{repo}/actions/runs?limit=50", token) - if data is None: - return None - runs = data.get("workflow_runs", data if isinstance(data, list) else []) - return [r for r in runs if str(r.get("head_sha", "")) == sha] +def _fetch_runs(server: str, repo: str, sha: str, token: str, + max_pages: int = 4) -> list[dict] | None: + """H-PATCH: paginate the runs list. The repo's scheduled merge-robot + runs flood the first page (~50 slots exhaust within hours), pushing + older-but-relevant SHAs to later pages — the cause of the observed + authority flapping (terminal commit read as pending). Paginate until + the sha is found or max_pages exhausted.""" + out: list[dict] = [] + for page in range(1, max_pages + 1): + data = _get_json( + f"{server}/api/v1/repos/{repo}/actions/runs?limit=50&page={page}", + token) + if data is None: + return None if page == 1 else (out or None) + runs = data.get("workflow_runs", data if isinstance(data, list) else []) + out.extend(r for r in runs if str(r.get("head_sha", "")) == sha) + if len(runs) < 50: + break # exhausted + return out def _fetch_jobs(server: str, repo: str, run_id, token: str) -> list[dict] | None: @@ -132,10 +145,17 @@ def main() -> int: # not export GITEA_SERVER/GITEA_REPO (CI always exports them; local # and first-class-tool consumers previously got a silent "pending" # verdict — a wrong answer, not a fail-closed one). + # H-PATCH H4: the target sha arrives EXPLICITLY (--sha) from callers + # that hold structured operation state. DEPLOY_SHA env remains only + # for standalone/workflow backward compat — the canonical path never + # depends on ambient env for operation identity. + ap = __import__("argparse").ArgumentParser() + ap.add_argument("--sha", dest="sha_arg", default=None) + args, _unknown = ap.parse_known_args() server = (os.environ.get("GITEA_SERVER", "") or gitea_auth.DEFAULT_SERVER).rstrip("/") repo = os.environ.get("GITEA_REPO", "") or gitea_auth.DEFAULT_REPO - sha = os.environ.get("DEPLOY_SHA", "") + sha = args.sha_arg or os.environ.get("DEPLOY_SHA", "") token = gitea_auth.get_token() if gitea_auth.has_token() else "" if not (server and repo and sha and token): print("pending") # missing config → cannot verify → not qualified diff --git a/store/scaffold/event_hardening_gates.json b/store/scaffold/event_hardening_gates.json new file mode 100644 index 00000000..83637292 --- /dev/null +++ b/store/scaffold/event_hardening_gates.json @@ -0,0 +1,224 @@ +{ + "tranche": "EVENT_DRIVEN_HARDENING", + "purpose": "Final authority / operation-identity hardening (Defects A/B/C)", + "predecessor": "event_driven_gates.json (16/16, superseded by this recomputation)", + "created_at": "2026-09-18T08:56:52Z", + "gates": [ + { + "gate_id": "H10_AGGREGATOR_LAG_RESILIENCE", + "mandatory": true, + "requirement": "status-200-but-stale AND status-404 both reconcile via runs/jobs evidence without weakening the gate", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H11_FRESH_PROCESS_OPERATION_ID_ATTACH", + "mandatory": true, + "requirement": "fresh process continues by operation_id alone", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H12_LIVE_BRANCH_FLOW", + "mandatory": true, + "requirement": "branch push -> CI via normal tool path", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H13_LIVE_PR_FLOW", + "mandatory": true, + "requirement": "PR checks -> merge via normal tool path", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H14_LIVE_MASTER_DEPLOY_FLOW", + "mandatory": true, + "requirement": "master CI -> deploy via normal tool path", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H15_NO_MANUAL_INTERNAL_APIS_IN_LIVE_PROOF", + "mandatory": true, + "requirement": "no manual ci_event_wait/normalizer/authority calls in the live window", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H16_REGRESSION_AND_ADVERSARIAL_TESTS", + "mandatory": true, + "requirement": "A-J identity/adversarial matrix + existing suites green", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H17_RAW_SELF_AUDIT", + "mandatory": true, + "requirement": "raw trace outranks report; every occurrence classified", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H18_FINAL_RUNTIME_CONFIG_CONVERGENCE", + "mandatory": true, + "requirement": "git==release==deployed==runtime==healthy", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H1_OPERATION_ID_FIRST_CONTINUATION", + "mandatory": true, + "requirement": "ContinuePendingOperation(operation_id) is the canonical API; mechanical truth comes from the durable record", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H2_PENDING_OPERATION_OWNS_TARGET_IDENTITY", + "mandatory": true, + "requirement": "PendingOperation owns sha/ref/target; model hints never retarget", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H3_MODEL_SHA_NON_AUTHORITATIVE", + "mandatory": true, + "requirement": "wrong/typo model sha + bound op -> stored sha used; mismatch logged; no silent retarget", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H4_AUTHORITY_SHA_EXPLICIT_END_TO_END", + "mandatory": true, + "requirement": "sha propagates explicitly through ci_event_wait -> op_authority_verdict -> deploy_qualify -> Gitea calls; no env dependence in the canonical path", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H5_AMBIENT_ENV_CANNOT_CHANGE_VERDICT", + "mandatory": true, + "requirement": "stale/wrong/missing DEPLOY_SHA cannot change an explicitly identified operation's verdict", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H6_EVENT_TERMINAL_RESUME_PROVEN", + "mandatory": true, + "requirement": "terminal discovered via event before any deadline", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H7_WATCHDOG_TERMINAL_RECOVERY_PROVEN", + "mandatory": true, + "requirement": "lost event: watchdog authority reread discovers terminal and resumes without hard deadline", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H8_DEADLINE_NOT_NORMAL_SUCCESS_TRIGGER", + "mandatory": true, + "requirement": "DEADLINE_DRIVEN_TERMINAL_DISCOVERY_COUNT=0 in clean window", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + }, + { + "gate_id": "H9_AUTHORITY_MONOTONICITY", + "mandatory": true, + "requirement": "terminal success never oscillates back to pending for the same operation", + "status": "UNRUN", + "evidence": "", + "blocker": "", + "waiver_reason": "", + "waived_by": "", + "declared_at": "2026-09-18T08:56:52Z", + "resolved_at": "" + } + ] +} \ No newline at end of file diff --git a/tests/test_await_intent.py b/tests/test_await_intent.py index df7fdd79..936ce8a0 100644 --- a/tests/test_await_intent.py +++ b/tests/test_await_intent.py @@ -185,6 +185,106 @@ def test_timing_provenance_order(): # ── observability ─────────────────────────────────────────────────────────── +# ── H-PATCH: identity & authority hardening ──────────────────────────────── + +REAL = "a" * 40 +TYPO = "b" * 40 + + +def test_wrong_sha_bound_op_wins(isolated, monkeypatch): + """H3/A: model sha is a typo; single bound PendingOperation wins.""" + _put(isolated, "ci-bound", REAL, "branch_ci", normalized=True) + r = ai.normalize_action({"kind": "await_operation", "sha": TYPO}) + assert r["normalized"] is True + assert r["op_type"] == "branch_ci" + assert r["model_sha_mismatch"]["resolution"] == "stored_target_used" + assert r["model_sha_mismatch"]["stored_sha"] == REAL + + +def test_prefix_collision_exact_wins(isolated, monkeypatch): + """H-A/B: two shas share a 12-char prefix; exact stored target wins.""" + _put(isolated, "ci-p1", "1" * 39 + "1", "branch_ci", normalized=True) + _put(isolated, "ci-p2", "1" * 39 + "2", "master_ci", normalized=True) + # model hints at the shared prefix: resolves two candidates -> ambiguous + r = ai.normalize_action({"kind": "await_operation", "sha": "1" * 12}) + assert r.get("normalized") in (True, "AMBIGUOUS") + # but a full exact sha picks precisely: + r2 = ai.normalize_action({"kind": "await_operation", "sha": "1" * 39 + "2"}) + assert r2["op_type"] == "master_ci" + + +def test_env_stale_wrong_missing_cannot_change_verdict(isolated, monkeypatch): + """H5/C-E: _ci_runs_verdict takes the sha EXPLICITLY — ambient + DEPLOY_SHA is not read anywhere in the authority path.""" + import ci_event_wait as cw + src = (REPO / "scripts" / "ci_event_wait.py").read_text() + # no env read remains in the runs/authority path + assert 'os.environ.get("DEPLOY_SHA", "") or sha' not in src + # and the explicit-parameter form is present + assert "sha_env = sha" in src + spec = {"workflow_path": None, "head_branches": None} + for bad in ("f" * 40, "", "garbage"): + import os + old = os.environ.get("DEPLOY_SHA") + if bad: + os.environ["DEPLOY_SHA"] = bad + else: + os.environ.pop("DEPLOY_SHA", None) + try: + import ci_event_wait as cw + assert cw._ci_runs_verdict(spec, REAL) == "pending" + finally: + if old: + os.environ["DEPLOY_SHA"] = old + else: + os.environ.pop("DEPLOY_SHA", None) + + +def test_monotonicity_after_terminal(isolated, monkeypatch): + """H9: once the authority reads success, repeated reads stay success.""" + import ci_event_wait as cw + monkeypatch.setattr(cw, "authority_verdict", lambda sha="": "success") + monkeypatch.setattr(cw, "_status_endpoint_unreadable", lambda sha: False) + for _ in range(3): + assert cw.normalize_verdict(cw.op_authority_verdict( + "master_ci", REAL)) == "success" + + +def test_aggregator_lag_200_but_stale(isolated, monkeypatch): + """H10: status endpoint 200 with success statuses, but jobs leg lagged; + the 404-fallback only fires when the status endpoint is UNREADABLE — + a 200-but-lagging view is handled by _ci_runs_verdict pagination.""" + import ci_event_wait as cw + monkeypatch.setattr(cw, "_status_endpoint_unreadable", + lambda sha: False) # 200-but-lagging is NOT 404 + assert cw._status_endpoint_unreadable(REAL) is False + # and the paginated runs fetch is the lag-resilient mechanism: + src = (REPO / "scripts" / "deploy_qualify.py").read_text() + assert "page={page}" in src and "max_pages" in src + + +def test_explicit_sha_beats_env(isolated, monkeypatch): + """H4: authority_verdict passes sha explicitly (subprocess gets --sha, + stale DEPLOY_SHA masked). Static contract check + behavioral probe.""" + src = (REPO / "scripts" / "ci_event_wait.py").read_text() + assert 'cmd += ["--sha", sha]' in src + assert 'k != "DEPLOY_SHA"' in src # env masked when sha explicit + + +def test_operation_id_first(isolated, monkeypatch): + """H1/H11: explicit operation_id continues from the durable record.""" + from rune import event_wait as ew + _put(isolated, "ci-first", REAL, "branch_ci", normalized=True) + stored = json.loads((isolated / "ci-first.json").read_text()) + monkeypatch.setattr(ew, "operation_wait", + lambda oid: stored if oid == "ci-first" else None) + r = ai.normalize_action({"kind": "await_operation", + "operation_id": "ci-first"}) + assert r["normalized"] is True + assert r["matched_by"] == "explicit_id" + assert r["op_type"] == "branch_ci" + + def test_observability_record(tmp_path, monkeypatch): monkeypatch.setattr(ai, "_SCRIPT_DIR", tmp_path) rec = {"normalized": True, "action": "ContinuePendingOperation", diff --git a/tests/test_ci_event_wait.py b/tests/test_ci_event_wait.py index 805012f6..5562d4ac 100644 --- a/tests/test_ci_event_wait.py +++ b/tests/test_ci_event_wait.py @@ -77,7 +77,7 @@ def _post_when_registered(wr_daemon: Daemon, op: str, payload: dict, def test_16c_event_wake_resumes_and_authority_decides(wr_daemon, monkeypatch): mod = _load_ci_wait_module() - monkeypatch.setattr(mod, "authority_verdict", lambda: "success") + monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "success") monkeypatch.setattr(mod, "ci_alive", lambda: False) payload = {"operation_id": f"ci-{CI_SHA}", "provider": "gitea", "event_type": "ci_completed", "sha": CI_SHA, "exit_code": 0, @@ -99,7 +99,7 @@ def test_16c_event_payload_is_never_truth(wr_daemon, monkeypatch): mod = _load_ci_wait_module() # the event says success (exit_code 0); canonical authority says the # CI is RED — the payload must never decide the verdict - monkeypatch.setattr(mod, "authority_verdict", lambda: "failure") + monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "failure") monkeypatch.setattr(mod, "ci_alive", lambda: False) payload = {"operation_id": f"ci-{CI_SHA}", "provider": "gitea", "event_type": "ci_completed", "sha": CI_SHA, "exit_code": 0, @@ -111,7 +111,7 @@ def test_16c_event_payload_is_never_truth(wr_daemon, monkeypatch): def test_16d_event_resume_has_no_sleep_floor(wr_daemon, monkeypatch): mod = _load_ci_wait_module() - monkeypatch.setattr(mod, "authority_verdict", lambda: "success") + monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "success") monkeypatch.setattr(mod, "ci_alive", lambda: False) payload = {"operation_id": f"ci-{CI_SHA}", "provider": "gitea", "event_type": "ci_completed", "sha": CI_SHA, "exit_code": 0, @@ -130,7 +130,7 @@ def test_16d_lost_event_watchdog_recovers_from_authority( wr_daemon, monkeypatch): mod = _load_ci_wait_module() # NO event is ever posted (suppressed/lost); authority IS resolvable - monkeypatch.setattr(mod, "authority_verdict", lambda: "success") + monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "success") monkeypatch.setattr(mod, "ci_alive", lambda: False) rc = mod.run(sha=CI_SHA, deadline_secs=4, daemon_url=wr_daemon.url()) assert rc == 0 @@ -143,7 +143,7 @@ def test_16d_lost_event_watchdog_recovers_from_authority( def test_16d_lost_event_with_no_verdict_fails_truthfully( wr_daemon, monkeypatch): mod = _load_ci_wait_module() - monkeypatch.setattr(mod, "authority_verdict", lambda: "pending") + monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "pending") monkeypatch.setattr(mod, "ci_alive", lambda: False) rc = mod.run(sha=CI_SHA, deadline_secs=4, daemon_url=wr_daemon.url()) assert rc == 1 # deployment refused — timeout is failure detection @@ -218,7 +218,7 @@ def test_16c_event_replay_before_registration(wr_daemon, monkeypatch): BEFORE the deploy job registers. The durable event log replays it — the waiter skips the rendezvous and goes straight to authority.""" mod = _load_ci_wait_module() - monkeypatch.setattr(mod, "authority_verdict", lambda: "success") + monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "success") monkeypatch.setattr(mod, "ci_alive", lambda: False) wr_daemon.post_event({"operation_id": f"ci-{CI_SHA}", "provider": "gitea", "event_type": "ci_completed", diff --git a/tests/test_single_logical_await.py b/tests/test_single_logical_await.py index c7c8038f..c5b411e4 100644 --- a/tests/test_single_logical_await.py +++ b/tests/test_single_logical_await.py @@ -68,7 +68,7 @@ def test_branch_ci_uses_own_runs_not_deploy_qualify(monkeypatch): def test_master_ci_uses_deploy_qualify(monkeypatch): mod = _load() - monkeypatch.setattr(mod, "authority_verdict", lambda: "success") + monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "success") assert mod.op_authority_verdict("master_ci", SHA) == "success" @@ -77,7 +77,7 @@ def test_wrong_authority_never_forever_pending(monkeypatch): op's own runs still resolve the verdict — no deadlock.""" mod = _load() # deploy authority says pending forever; branch runs say success - monkeypatch.setattr(mod, "authority_verdict", lambda: "pending") + monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "pending") monkeypatch.setattr(mod, "_ci_runs_verdict", lambda spec, sha: "success") assert mod.op_authority_verdict("branch_ci", SHA) == "success"