Files
aetherbound-guild/tools/test_run_test_attempt.py
T

250 lines
9.1 KiB
Python
Executable File

#!/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())