feat: bound autonomous release command runtime (#329)
CI / lint (pull_request) Successful in 31s
CI / build-frontend (pull_request) Successful in 4s

This commit is contained in:
timmy committed 2026-08-08 18:54:08 +00:00
1 parent 41eaf51953
commit fb6b42bcaf
4 files changed
+216 -15

No files matched your search

+120 -1
View File
@@ -1,6 +1,8 @@
import json
import sys
import threading
import time
from dataclasses import replace
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
@@ -15,6 +17,7 @@ from src.release_engine import (
RunState,
ShellRunner,
StateStore,
_parse_args,
branch_name,
select_issue,
)
@@ -41,6 +44,33 @@ def test_shell_runner_treats_metacharacters_as_literal_arguments(tmp_path):
assert not injected.exists()
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):
@@ -94,10 +124,12 @@ class FakeRunner:
self.results = list(results)
self.commands = []
self.environments = []
self.options = []
def run(self, command, cwd, env=None):
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)
@@ -164,6 +196,41 @@ def test_empty_agent_command_fails_before_claim(tmp_path):
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 == []
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",
])
args = _parse_args()
assert args.agent_timeout == 120
assert args.test_timeout == 45.5
assert args.termination_grace == 2
def test_agent_failure_blocks_tests_and_pr(tmp_path):
issue = Issue(14, "Engine", "body", "open", [], None)
client = FakeClient([issue])
@@ -183,6 +250,33 @@ def test_agent_failure_blocks_tests_and_pr(tmp_path):
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"
@@ -229,6 +323,31 @@ def test_failed_tests_block_push_and_pr(tmp_path):
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(-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[2] == {
"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])