#!/usr/bin/env python3 """Regression checks for bounded Aetherbound Guild test-attempt cleanup.""" from __future__ import annotations import importlib.util import json import os from pathlib import Path import subprocess import sys import tempfile import time from types import SimpleNamespace ROOT = Path(__file__).resolve().parents[1] RUNNER = ROOT / "tools" / "run_test_attempt.py" BROWSER_TEMP = ROOT / "tools" / "browser_temp.js" def invoke( base: Path, label: str, command: list[str], expected: int, *, timeout: str = "5", git_ref: str = "", required_markers: tuple[str, ...] = (), ) -> subprocess.CompletedProcess[str]: arguments = [ sys.executable, str(RUNNER), "--label", label, "--cwd", str(ROOT), "--workspace-root", str(base / "work"), "--artifact-root", str(base / "artifacts"), "--keep-failed", "3", "--keep-passed", "2", "--timeout", timeout, ] if git_ref: arguments.extend(["--git-ref", git_ref]) for marker in required_markers: arguments.extend(["--require-marker", marker]) arguments.extend(["--", *command]) completed = subprocess.run( arguments, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, ) assert completed.returncode == expected, completed.stdout return completed def result_directories(family: Path, outcome: str) -> list[Path]: matches: list[Path] = [] for path in family.iterdir(): result_path = path / "result.json" if path.is_dir() and result_path.is_file(): if json.loads(result_path.read_text(encoding="utf-8"))["outcome"] == outcome: matches.append(path) return matches def main() -> int: spec = importlib.util.spec_from_file_location("abg_test_attempt_runner", RUNNER) assert spec is not None and spec.loader is not None runner_module = importlib.util.module_from_spec(spec) spec.loader.exec_module(runner_module) original_child = runner_module._child original_killpg = runner_module.os.killpg try: runner_module._child = SimpleNamespace(pid=99999998) def deny_killpg(_pgid: int, _signal: int) -> None: raise PermissionError(1, "synthetic denied process group") runner_module.os.killpg = deny_killpg assert runner_module.terminate_child() is False finally: runner_module.os.killpg = original_killpg runner_module._child = original_child with tempfile.TemporaryDirectory(prefix="abg-test-runner-selftest-") as temporary: base = Path(temporary) stale = base / "work" / "stale-owned" stale.mkdir(parents=True) (stale / ".abg-attempt-owner.json").write_text( json.dumps({"created_by": "aetherbound-test-attempt-v1", "pid": 99999999}), encoding="utf-8", ) success = invoke( base, "lifecycle", [ "/bin/sh", "-c", 'printf proof > "$ABG_EVIDENCE_DIR/proof.txt"; test -d "$ABG_BROWSER_TEMP_PARENT"; test -n "$ABG_ORIGINAL_HOME"; test "$HOME" != "$ABG_ORIGINAL_HOME"', ], 0, ) assert "ABG_TEST_ATTEMPT_PASS" in success.stdout assert not stale.exists(), "stale owned workspace must be removed" assert list((base / "work").iterdir()) == [], "successful attempt leaked a workspace" family = base / "artifacts" / "lifecycle" passes = result_directories(family, "PASS") assert len(passes) == 1 result = json.loads((passes[0] / "result.json").read_text(encoding="utf-8")) assert result["workspace_removed"] is True assert result["evidence_files"] == 1 assert (passes[0] / "evidence.tar.gz").is_file() archived = invoke( base, "lifecycle", [ "/bin/sh", "-c", 'test -f README.md; printf "archive\\n"; printf archive > "$ABG_EVIDENCE_DIR/archive.txt"', ], 0, git_ref="HEAD", required_markers=("archive",), ) assert "ABG_TEST_ATTEMPT_PASS" in archived.stdout assert list((base / "work").iterdir()) == [], "git-archive attempt leaked source or cache data" timed_out = invoke(base, "lifecycle", ["/bin/sh", "-c", "sleep 2"], 124, timeout="0.2") assert "ABG_TEST_ATTEMPT_TIMEOUT" in timed_out.stdout strict_failure = invoke( base, "lifecycle", ["/bin/sh", "-c", "printf 'SCRIPT ERROR: synthetic parse failure\\n'"], 86, ) assert "ABG_TEST_ATTEMPT_FAIL" in strict_failure.stdout interrupted_process = subprocess.Popen( [ sys.executable, str(RUNNER), "--label", "lifecycle", "--cwd", str(ROOT), "--workspace-root", str(base / "work"), "--artifact-root", str(base / "artifacts"), "--keep-failed", "3", "--keep-passed", "2", "--timeout", "5", "--", "/bin/sh", "-c", 'python3 -c \'import signal,time; signal.signal(signal.SIGTERM, signal.SIG_IGN); time.sleep(30)\' & echo $! > "$ABG_ATTEMPT_ROOT/descendant.pid"; wait', ], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) deadline = time.monotonic() + 2 descendant_pid_path = None while time.monotonic() < deadline: matches = list((base / "work").glob("*/descendant.pid")) if matches: descendant_pid_path = matches[0] break time.sleep(0.02) assert descendant_pid_path is not None, "interrupted attempt did not create its descendant" descendant_pid = int(descendant_pid_path.read_text(encoding="utf-8")) interrupted_process.terminate() interrupted_output, _ = interrupted_process.communicate(timeout=5) assert interrupted_process.returncode == 143, interrupted_output assert "ABG_TEST_ATTEMPT_INTERRUPTED" in interrupted_output assert list((base / "work").iterdir()) == [], "handled interruption leaked a workspace" interrupted_results = result_directories(family, "INTERRUPTED") assert len(interrupted_results) == 1 interrupted_result = json.loads( (interrupted_results[0] / "result.json").read_text(encoding="utf-8") ) assert interrupted_result["process_group_removed"] is True try: os.kill(descendant_pid, 0) except ProcessLookupError: pass else: raise AssertionError("handled interruption leaked a descendant process") for _index in range(5): invoke(base, "lifecycle", ["/bin/sh", "-c", "exit 7"], 7) failed = sum( len(result_directories(family, outcome)) for outcome in ("FAIL", "TIMEOUT", "INTERRUPTED", "HARNESS_ERROR") ) assert failed == 3, "all failed evidence classes combined must be bounded to three" journal = (family / "attempts.jsonl").read_text(encoding="utf-8").splitlines() assert len(journal) == 10, "all attempt terminals remain in the compact append-only journal" assert list((base / "work").iterdir()) == [], "failed attempts leaked workspaces" browser_parent = base / "browser" browser_parent.mkdir() stale_browser = browser_parent / "stale-browser" stale_browser.mkdir() (stale_browser / ".abg-browser-owner.json").write_text( json.dumps({"createdBy": "aetherbound-browser-temp-v1", "pid": 99999999}), encoding="utf-8", ) browser_check = subprocess.run( [ "node", "-e", ( f'process.env.ABG_BROWSER_TEMP_PARENT={json.dumps(str(browser_parent))};' f'const h=require({json.dumps(str(BROWSER_TEMP))}).installBrowserTemp("selftest");' 'require("node:fs").writeFileSync(require("node:path").join(h.root,"owned"),"1");' ), ], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, ) assert browser_check.returncode == 0, browser_check.stdout assert list(browser_parent.iterdir()) == [], "browser temp helper leaked current or stale owned roots" print("ABG_TEST_ATTEMPT_RUNNER_OK pass=2 fail_attempts=8 retained_failures=3 archive_cleanup=true interrupt_cleanup=true strict_diagnostics=true marker_gate=true python_site_isolated=true stale_cleanup=true browser_cleanup=true descendant_cleanup=true") return 0 if __name__ == "__main__": raise SystemExit(main())