import json import os import sys import threading import time from dataclasses import replace from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path import pytest from src.release_engine import ( CommandResult, EngineConfig, GiteaClient, Issue, ReleaseEngine, RunState, ShellRunner, StateStore, _parse_args, branch_name, select_issue, ) def test_shell_runner_treats_metacharacters_as_literal_arguments(tmp_path): output = tmp_path / "argument.txt" injected = tmp_path / "injected.txt" payload = f"release title; touch {injected}" result = ShellRunner().run( [ sys.executable, "-c", "from pathlib import Path; import sys; Path(sys.argv[1]).write_text(sys.argv[2])", str(output), payload, ], tmp_path, ) assert result.returncode == 0 assert output.read_text() == payload assert not injected.exists() def test_shell_runner_uses_only_the_explicit_environment(tmp_path, monkeypatch): output = tmp_path / "environment.json" monkeypatch.setenv("GITEA_TOKEN", "write-capable-secret") monkeypatch.setenv("UNRELATED_SECRET", "also-secret") result = ShellRunner().run( [ sys.executable, "-c", "import json, os, sys; json.dump(dict(os.environ), open(sys.argv[1], 'w'))", str(output), ], tmp_path, {"PATH": os.environ["PATH"], "EXPECTED_VALUE": "available"}, ) environment = json.loads(output.read_text()) assert result.returncode == 0 assert environment["EXPECTED_VALUE"] == "available" assert "GITEA_TOKEN" not in environment assert "UNRELATED_SECRET" not in environment def test_shell_runner_times_out_and_terminates_spawned_children(tmp_path): marker = tmp_path / "child-survived.txt" child = ( "import time; from pathlib import Path; " f"time.sleep(0.3); Path({str(marker)!r}).write_text('survived')" ) parent = ( "import subprocess, sys, time; " f"subprocess.Popen([sys.executable, '-c', {child!r}]); " "time.sleep(10)" ) started = time.monotonic() result = ShellRunner().run( [sys.executable, "-c", parent], tmp_path, timeout_seconds=0.05, termination_grace_seconds=0.05, ) elapsed = time.monotonic() - started time.sleep(0.35) assert result.timed_out is True assert elapsed < 0.5 assert not marker.exists() def test_stale_token_error_explains_how_to_restore_cron_authentication(): class UnauthorizedHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(401) self.end_headers() self.wfile.write(b'{"message":"user does not exist [uid: 0, name: ]"}') def log_message(self, format, *args): pass server = HTTPServer(("127.0.0.1", 0), UnauthorizedHandler) thread = threading.Thread(target=server.handle_request) thread.start() try: client = GiteaClient(f"http://127.0.0.1:{server.server_port}", "stale-token") with pytest.raises(RuntimeError, match="refresh the cron GITEA_TOKEN"): client.list_open_issues("stackchain/stackchain-dashboard") finally: thread.join(timeout=2) server.server_close() class FakeClient: def __init__(self, issues, verified_assignee="timmy", existing_pr=None): self.issues = issues self.verified_assignee = verified_assignee self.existing_pr = existing_pr self.claimed = [] self.created_prs = [] def list_open_issues(self, repo): return self.issues def claim_issue(self, repo, number, assignee): self.claimed.append((repo, number, assignee)) def get_issue(self, repo, number): issue = next(i for i in self.issues if i.number == number) return Issue(**{**issue.__dict__, "assignee": self.verified_assignee}) def find_open_pr(self, repo, head): return self.existing_pr def create_pr(self, repo, head, base, title, body): self.created_prs.append((repo, head, base, title, body)) return {"number": 44, "html_url": "https://forge/pr/44"} class FakeRunner: def __init__(self, results): self.results = list(results) self.commands = [] self.environments = [] self.options = [] def run(self, command, cwd, env=None, **options): self.commands.append(command) self.environments.append(env) self.options.append(options) return self.results.pop(0) @pytest.fixture def issues(): return [ Issue(2, "P2 task", "body", "open", ["P2"], None), Issue(3, "Owned P0", "body", "open", ["P0"], "other"), Issue(4, "Unowned P0", "body", "open", ["P0"], None), ] def config(tmp_path): return EngineConfig( repo="stackchain/stackchain-dashboard", agent="timmy", repo_path=tmp_path, state_path=tmp_path / "state.json", agent_command="agent --issue-from-env", test_command="pytest -q", ) def test_selection_prioritizes_p0_and_skips_other_assignees(issues): assert select_issue(issues, agent="timmy").number == 4 def test_explicit_issue_must_be_eligible(issues): with pytest.raises(ValueError, match="assigned to other"): select_issue(issues, agent="timmy", issue_number=3) def test_branch_name_is_reproducible_and_bounded(): assert branch_name("Timmy", Issue(14, "Autonomous Issue-to-Release Engine!", "", "open", [], None)) == "timmy/14-autonomous-issue-to-release-engine" def test_claim_verification_blocks_execution(tmp_path): issue = Issue(14, "Engine", "body", "open", [], None) client = FakeClient([issue], verified_assignee="other") runner = FakeRunner([]) engine = ReleaseEngine(config(tmp_path), client, runner) with pytest.raises(RuntimeError, match="claim verification failed"): engine.execute(issue_number=14) assert runner.commands == [] def test_empty_agent_command_fails_before_claim(tmp_path): issue = Issue(14, "Engine", "body", "open", [], None) client = FakeClient([issue]) cfg = EngineConfig( repo="stackchain/stackchain-dashboard", agent="timmy", repo_path=tmp_path, state_path=tmp_path / "state.json", agent_command=" ", test_command="pytest -q", ) with pytest.raises(ValueError, match="invalid agent command: command is empty"): ReleaseEngine(cfg, client, FakeRunner([])).execute(issue_number=14) assert client.claimed == [] @pytest.mark.parametrize( ("field", "value"), [ ("agent_timeout_seconds", 0), ("test_timeout_seconds", -1), ("termination_grace_seconds", 0), ], ) def test_non_positive_command_deadlines_fail_before_claim(tmp_path, field, value): issue = Issue(14, "Engine", "body", "open", [], None) client = FakeClient([issue]) cfg = replace(config(tmp_path), **{field: value}) with pytest.raises(ValueError, match=field.replace("_", " ")): ReleaseEngine(cfg, client, FakeRunner([])).execute(issue_number=14) assert client.claimed == [] @pytest.mark.parametrize("name", ["GITEA_TOKEN", "RELEASE_REPO", "bad-name", "1START"]) def test_invalid_agent_environment_allowlist_fails_before_claim(tmp_path, name): issue = Issue(14, "Engine", "body", "open", [], None) client = FakeClient([issue]) cfg = replace(config(tmp_path), agent_env_vars=(name,)) with pytest.raises(ValueError, match="invalid agent environment variable"): ReleaseEngine(cfg, client, FakeRunner([])).execute(issue_number=14) assert client.claimed == [] def test_command_deadlines_are_configurable_from_cli(monkeypatch): monkeypatch.setattr(sys, "argv", [ "release-engine", "--repo", "stackchain/stackchain-dashboard", "--agent-timeout", "120", "--test-timeout", "45.5", "--termination-grace", "2", "--agent-env", "CODEX_HOME", "--agent-env", "HTTPS_PROXY", ]) args = _parse_args() assert args.agent_timeout == 120 assert args.test_timeout == 45.5 assert args.termination_grace == 2 assert args.agent_env == ["CODEX_HOME", "HTTPS_PROXY"] def test_agent_failure_blocks_tests_and_pr(tmp_path): issue = Issue(14, "Engine", "body", "open", [], None) client = FakeClient([issue]) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(1, "", "agent failed"), ]) engine = ReleaseEngine(config(tmp_path), client, runner) with pytest.raises(RuntimeError, match="coding agent failed"): engine.execute(issue_number=14) assert runner.commands == [ ["git", "checkout", "-B", "timmy/14-engine"], ["agent", "--issue-from-env"], ] assert client.created_prs == [] def test_agent_timeout_records_terminal_state_and_blocks_tests(tmp_path): issue = Issue(14, "Engine", "body", "open", [], None) client = FakeClient([issue]) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(-9, "partial output", "stalled", timed_out=True), ]) cfg = config(tmp_path) with pytest.raises(RuntimeError, match="coding agent timed out"): ReleaseEngine(cfg, client, runner).execute(issue_number=14) state = StateStore(cfg.state_path).load() assert state is not None assert state.status == "agent_timed_out" assert "partial output" in state.evidence assert runner.commands == [ ["git", "checkout", "-B", "timmy/14-engine"], ["agent", "--issue-from-env"], ] assert runner.options[1] == { "timeout_seconds": cfg.agent_timeout_seconds, "termination_grace_seconds": cfg.termination_grace_seconds, } assert client.created_prs == [] def test_issue_content_reaches_agent_only_through_environment(tmp_path): title = "$(touch /tmp/title-injected); `id`" body = "line one\n&& touch /tmp/body-injected > stolen" issue = Issue(14, title, body, "open", [], None) client = FakeClient([issue]) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(1, "", "agent stopped"), ]) cfg = EngineConfig( repo="stackchain/stackchain-dashboard", agent="timmy", repo_path=tmp_path, state_path=tmp_path / "state.json", agent_command="agent --work", test_command="pytest -q", ) with pytest.raises(RuntimeError, match="coding agent failed"): ReleaseEngine(cfg, client, runner).execute(issue_number=14) assert runner.commands == [ ["git", "checkout", "-B", "timmy/14-touch-tmp-title-injected-id"], ["agent", "--work"], ] assert runner.environments[1]["RELEASE_ISSUE_TITLE"] == title assert runner.environments[1]["RELEASE_ISSUE_BODY"] == body def test_release_stages_receive_least_privilege_environments(tmp_path, monkeypatch): monkeypatch.setenv("GITEA_TOKEN", "write-capable-secret") monkeypatch.setenv("UNRELATED_SECRET", "also-secret") monkeypatch.setenv("CODEX_HOME", "/approved/codex") issue = Issue(14, "Engine MVP", "sensitive body", "open", [], None) client = FakeClient([issue]) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(0, "implemented", ""), CommandResult(0, "abc123\n", ""), CommandResult(0, "8 passed", ""), CommandResult(0, "", ""), ]) cfg = replace(config(tmp_path), agent_env_vars=("CODEX_HOME",)) ReleaseEngine(cfg, client, runner).execute(issue_number=14) for environment in runner.environments: assert environment is not None assert "PATH" in environment assert "GITEA_TOKEN" not in environment assert "UNRELATED_SECRET" not in environment agent_environment = runner.environments[1] assert agent_environment["CODEX_HOME"] == "/approved/codex" assert agent_environment["RELEASE_ISSUE_NUMBER"] == "14" assert agent_environment["RELEASE_ISSUE_TITLE"] == issue.title assert agent_environment["RELEASE_ISSUE_BODY"] == issue.body assert agent_environment["RELEASE_REPO"] == cfg.repo assert agent_environment["RELEASE_BRANCH"] == "timmy/14-engine-mvp" assert not any( name.startswith("RELEASE_") for name in runner.environments[3] ) def test_failed_tests_block_push_and_pr(tmp_path): issue = Issue(14, "Engine", "body", "open", [], None) client = FakeClient([issue]) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(0, "changed", ""), CommandResult(0, "abc123\n", ""), CommandResult(1, "1 failed", ""), ]) engine = ReleaseEngine(config(tmp_path), client, runner) with pytest.raises(RuntimeError, match="tests failed"): engine.execute(issue_number=14) assert not any(command[:2] == ["git", "push"] for command in runner.commands) assert client.created_prs == [] def test_test_timeout_records_terminal_state_and_blocks_push(tmp_path): issue = Issue(14, "Engine", "body", "open", [], None) client = FakeClient([issue]) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(0, "implemented", ""), CommandResult(0, "abc123\n", ""), CommandResult(-15, "3 passed before stall", "", timed_out=True), ]) cfg = config(tmp_path) with pytest.raises(RuntimeError, match="tests timed out"): ReleaseEngine(cfg, client, runner).execute(issue_number=14) state = StateStore(cfg.state_path).load() assert state is not None assert state.status == "tests_timed_out" assert "3 passed before stall" in state.evidence assert runner.options[3] == { "timeout_seconds": cfg.test_timeout_seconds, "termination_grace_seconds": cfg.termination_grace_seconds, } assert not any(command[:2] == ["git", "push"] for command in runner.commands) assert client.created_prs == [] def test_passing_run_pushes_and_opens_linked_pr(tmp_path): issue = Issue(14, "Engine MVP", "body", "open", ["P1"], None) client = FakeClient([issue]) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(0, "implemented", ""), CommandResult(0, "abc123\n", ""), CommandResult(0, "8 passed", ""), CommandResult(0, "", ""), ]) engine = ReleaseEngine(config(tmp_path), client, runner) result = engine.execute(issue_number=14) assert result["pr_number"] == 44 assert runner.commands[-1] == ["git", "push", "-u", "origin", "timmy/14-engine-mvp"] assert len(client.created_prs) == 1 body = client.created_prs[0][4] assert "Closes #14" in body assert "pytest -q" in body assert "8 passed" in body def test_dry_run_does_not_claim_or_execute(tmp_path): issue = Issue(14, "Engine MVP", "body", "open", [], None) client = FakeClient([issue]) runner = FakeRunner([]) engine = ReleaseEngine(config(tmp_path), client, runner) result = engine.execute(issue_number=14, dry_run=True) assert result["dry_run"] is True assert result["issue"] == 14 assert client.claimed == [] assert runner.commands == [] def test_existing_pr_is_reused_without_agent_execution(tmp_path): issue = Issue(14, "Engine MVP", "body", "open", [], "timmy") client = FakeClient([issue], existing_pr={"number": 9, "html_url": "https://forge/pr/9"}) runner = FakeRunner([]) engine = ReleaseEngine(config(tmp_path), client, runner) result = engine.execute(issue_number=14) assert result["pr_number"] == 9 assert runner.commands == [] assert client.created_prs == [] def test_state_store_round_trip_is_atomic(tmp_path): store = StateStore(tmp_path / "state.json") expected = RunState(issue=14, branch="timmy/14-engine", status="tests_passed", evidence="8 passed") store.save(expected) assert store.load() == expected assert not (tmp_path / "state.json.tmp").exists() def test_restart_from_agent_complete_skips_claim_and_coding(tmp_path): issue = Issue(331, "Resume releases", "body", "open", [], "timmy") client = FakeClient([issue]) cfg = config(tmp_path) branch = "timmy/331-resume-releases" cfg.state_path.write_text(json.dumps({ "schema_version": 1, "repo": cfg.repo, "issue": issue.number, "branch": branch, "status": "agent_complete", "evidence": "", "pr_number": None, "pr_url": "", "head_sha": "abc123", })) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(0, "abc123\n", ""), CommandResult(0, "", ""), CommandResult(0, "8 passed", ""), CommandResult(0, "", ""), ]) result = ReleaseEngine(cfg, client, runner).execute(issue_number=331) assert result["tests"] == "passed" assert client.claimed == [] assert ["agent", "--issue-from-env"] not in runner.commands assert runner.commands[:3] == [ ["git", "checkout", branch], ["git", "rev-parse", "HEAD"], ["git", "status", "--porcelain"], ] def test_restart_from_tests_passed_skips_claim_coding_and_tests(tmp_path): issue = Issue(331, "Resume releases", "body", "open", [], "timmy") client = FakeClient([issue]) cfg = config(tmp_path) branch = "timmy/331-resume-releases" StateStore(cfg.state_path).save(RunState( issue=issue.number, branch=branch, status="tests_passed", evidence="604 passed", repo=cfg.repo, head_sha="abc123", )) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(0, "abc123\n", ""), CommandResult(0, "", ""), CommandResult(0, "", ""), ]) result = ReleaseEngine(cfg, client, runner).execute(issue_number=331) assert result["tests"] == "passed" assert client.claimed == [] assert ["agent", "--issue-from-env"] not in runner.commands assert ["pytest", "-q"] not in runner.commands assert runner.commands == [ ["git", "checkout", branch], ["git", "rev-parse", "HEAD"], ["git", "status", "--porcelain"], ["git", "push", "-u", "origin", branch], ] def test_restart_from_pushed_verifies_remote_and_only_opens_pr(tmp_path): issue = Issue(331, "Resume releases", "body", "open", [], "timmy") client = FakeClient([issue]) cfg = config(tmp_path) branch = "timmy/331-resume-releases" StateStore(cfg.state_path).save(RunState( issue=issue.number, branch=branch, status="pushed", evidence="604 passed", repo=cfg.repo, head_sha="abc123", )) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(0, "abc123\n", ""), CommandResult(0, "", ""), CommandResult(0, f"abc123\trefs/heads/{branch}\n", ""), ]) result = ReleaseEngine(cfg, client, runner).execute(issue_number=331) assert result["pr_number"] == 44 assert client.claimed == [] assert len(client.created_prs) == 1 assert runner.commands == [ ["git", "checkout", branch], ["git", "rev-parse", "HEAD"], ["git", "status", "--porcelain"], ["git", "ls-remote", "--heads", "origin", branch], ] def test_fresh_run_persists_repository_and_commit_identity(tmp_path): issue = Issue(331, "Resume releases", "body", "open", [], None) client = FakeClient([issue]) cfg = config(tmp_path) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(0, "implemented", ""), CommandResult(0, "abc123\n", ""), CommandResult(0, "604 passed", ""), CommandResult(0, "", ""), ]) ReleaseEngine(cfg, client, runner).execute(issue_number=331) state = StateStore(cfg.state_path).load() assert state is not None assert state.status == "pr_opened" assert state.schema_version == 1 assert state.repo == cfg.repo assert state.head_sha == "abc123" assert ["git", "rev-parse", "HEAD"] in runner.commands def test_resume_fails_closed_when_checkpoint_worktree_is_dirty(tmp_path): issue = Issue(331, "Resume releases", "body", "open", [], "timmy") client = FakeClient([issue]) cfg = config(tmp_path) branch = "timmy/331-resume-releases" StateStore(cfg.state_path).save(RunState( issue=issue.number, branch=branch, status="tests_passed", evidence="604 passed", repo=cfg.repo, head_sha="abc123", )) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(0, "abc123\n", ""), CommandResult(0, " M src/release_engine.py\n", ""), CommandResult(0, "", ""), ]) with pytest.raises(RuntimeError, match="worktree is not clean"): ReleaseEngine(cfg, client, runner).execute(issue_number=331) assert not any(command[:2] == ["git", "push"] for command in runner.commands) assert client.created_prs == [] def test_resume_fails_closed_when_checkpoint_targets_another_repository(tmp_path): issue = Issue(331, "Resume releases", "body", "open", [], "timmy") client = FakeClient([issue]) cfg = config(tmp_path) StateStore(cfg.state_path).save(RunState( issue=issue.number, branch="timmy/331-resume-releases", status="tests_passed", repo="other/repository", head_sha="abc123", )) with pytest.raises(RuntimeError, match="checkpoint does not match"): ReleaseEngine(cfg, client, FakeRunner([])).execute(issue_number=331) assert client.created_prs == [] def test_resume_fails_closed_when_local_commit_changed(tmp_path): issue = Issue(331, "Resume releases", "body", "open", [], "timmy") client = FakeClient([issue]) cfg = config(tmp_path) StateStore(cfg.state_path).save(RunState( issue=issue.number, branch="timmy/331-resume-releases", status="tests_passed", repo=cfg.repo, head_sha="abc123", )) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(0, "different-sha\n", ""), ]) with pytest.raises(RuntimeError, match="commit does not match the local branch"): ReleaseEngine(cfg, client, runner).execute(issue_number=331) assert client.created_prs == [] def test_resume_fails_closed_when_remote_commit_changed(tmp_path): issue = Issue(331, "Resume releases", "body", "open", [], "timmy") client = FakeClient([issue]) cfg = config(tmp_path) branch = "timmy/331-resume-releases" StateStore(cfg.state_path).save(RunState( issue=issue.number, branch=branch, status="pushed", evidence="604 passed", repo=cfg.repo, head_sha="abc123", )) runner = FakeRunner([ CommandResult(0, "", ""), CommandResult(0, "abc123\n", ""), CommandResult(0, "", ""), CommandResult(0, f"different-sha\trefs/heads/{branch}\n", ""), ]) with pytest.raises(RuntimeError, match="commit does not match the remote branch"): ReleaseEngine(cfg, client, runner).execute(issue_number=331) assert client.created_prs == []