diff --git a/scripts/repo_sync.py b/scripts/repo_sync.py index 79f6d7b..ff29424 100644 --- a/scripts/repo_sync.py +++ b/scripts/repo_sync.py @@ -36,6 +36,11 @@ from __future__ import annotations import subprocess +# The admitted receive scanner has a 90-second total budget. Leave bounded +# headroom for transport and Git hooks; a timeout never proves remote refusal. +PUSH_TIMEOUT_SECONDS = 120 + + def pull_ff(repo_path: str) -> tuple[bool, str]: """Run ``git pull --ff-only`` on *repo_path*. @@ -66,11 +71,13 @@ def push_ff(repo_path: str) -> tuple[bool, str]: try: result = subprocess.run( ["git", "-C", repo_path, "push"], - capture_output=True, text=True, timeout=30, + capture_output=True, text=True, timeout=PUSH_TIMEOUT_SECONDS, ) if result.returncode == 0: return True, result.stdout.strip() or "pushed" return False, result.stderr.strip() or "push failed" + except subprocess.TimeoutExpired: + return False, "push timed out; remote outcome unverified; inspect the remote ref before retrying" except Exception as exc: return False, str(exc) diff --git a/tests/test_push_deadline.py b/tests/test_push_deadline.py new file mode 100644 index 0000000..cac2a1c --- /dev/null +++ b/tests/test_push_deadline.py @@ -0,0 +1,37 @@ +"""Client deadlines must accommodate receive scanning without claiming timeout refusal.""" +import subprocess +from pathlib import Path + +import sys +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) +import repo_sync as module + + +def test_push_budget_covers_admitted_scanner(monkeypatch, tmp_path): + observed = {} + + def run(argv, **kwargs): + observed.update(argv=argv, **kwargs) + return subprocess.CompletedProcess(argv, 0, "", "") + + monkeypatch.setattr(module.subprocess, "run", run) + ok, _ = module.push_ff(tmp_path) + assert ok + assert 90 < observed["timeout"] <= 180 + assert "--force" not in observed["argv"] + + +def test_timeout_is_unverified_without_retry_or_child_output(monkeypatch, tmp_path): + calls = [] + + def run(argv, **kwargs): + calls.append(argv) + raise subprocess.TimeoutExpired(argv, kwargs["timeout"], output="private diagnostic fixture") + + monkeypatch.setattr(module.subprocess, "run", run) + ok, message = module.push_ff(tmp_path) + assert not ok + assert "remote outcome unverified" in message + assert "inspect the remote ref" in message + assert "private diagnostic fixture" not in message + assert len(calls) == 1 diff --git a/workplans/ADHOC-2026-10-13.md b/workplans/ADHOC-2026-10-13.md new file mode 100644 index 0000000..e7ede0e --- /dev/null +++ b/workplans/ADHOC-2026-10-13.md @@ -0,0 +1,40 @@ +--- +id: STATE-WP-ADHOC-2026-10-13 +type: workplan +title: "October 13 bounded sync compatibility fixes" +domain: infotech +repo: state-hub +status: finished +owner: codex +created: "2026-10-13" +updated: "2026-10-13" +flavor: implementation +--- + +## Allow admitted receive scanning to finish before timing out pushes + +```task +id: STATE-WP-ADHOC-2026-10-13-T01 +status: done +priority: high +``` + +CUST-WP-0080 found a normal source push accepted remotely after this client's +30-second deadline. The installed forge scanner has a 90-second total budget. +Raise only the push deadline to 120 seconds and label any timeout as an +unverified remote outcome; never infer rejection, retry automatically or allow +subsequent projection writes from that timeout. Repo Manager's 60-second push +path receives the same bounded fix under RMGR-WP-0021-T03. + +Acceptance: ordinary acceptance/refusal and push-seal regressions, delayed real +receive hooks exceeding both old client budgets, a fixed value-free timeout +message with no retry, installed CLI source verification and owner projection. +Evidence is retained in Custodian's October 13 security evidence directory. + +2026-10-13: 34 State Hub push-seal/deadline tests and 36 Repo Manager +deadline/historical/fast-projection tests pass. Both actual source helpers accept +a real 65-second receive hook, then preserve rejection and the unchanged remote +ref. The installed State Hub CLI module matches the reviewed source hash and +uses 120 seconds; Repo Manager imports its editable source. No residual remains +from this bounded compatibility fix. Broader scanner coverage stays in existing +CUST-WP-0080 and RMGR-WP-0021, not in this daily fix.