feat: resume release runs from checkpoints (#331)
This commit is contained in:
1 parent
eb2c84a6c2
commit
503d6f4988
4 files changed
+361
-53
No files matched your search
@@ -312,6 +312,7 @@ def test_failed_tests_block_push_and_pr(tmp_path):
|
||||
runner = FakeRunner([
|
||||
CommandResult(0, "", ""),
|
||||
CommandResult(0, "changed", ""),
|
||||
CommandResult(0, "abc123\n", ""),
|
||||
CommandResult(1, "1 failed", ""),
|
||||
])
|
||||
engine = ReleaseEngine(config(tmp_path), client, runner)
|
||||
@@ -329,6 +330,7 @@ def test_test_timeout_records_terminal_state_and_blocks_push(tmp_path):
|
||||
runner = FakeRunner([
|
||||
CommandResult(0, "", ""),
|
||||
CommandResult(0, "implemented", ""),
|
||||
CommandResult(0, "abc123\n", ""),
|
||||
CommandResult(-15, "3 passed before stall", "", timed_out=True),
|
||||
])
|
||||
cfg = config(tmp_path)
|
||||
@@ -340,7 +342,7 @@ def test_test_timeout_records_terminal_state_and_blocks_push(tmp_path):
|
||||
assert state is not None
|
||||
assert state.status == "tests_timed_out"
|
||||
assert "3 passed before stall" in state.evidence
|
||||
assert runner.options[2] == {
|
||||
assert runner.options[3] == {
|
||||
"timeout_seconds": cfg.test_timeout_seconds,
|
||||
"termination_grace_seconds": cfg.termination_grace_seconds,
|
||||
}
|
||||
@@ -354,6 +356,7 @@ def test_passing_run_pushes_and_opens_linked_pr(tmp_path):
|
||||
runner = FakeRunner([
|
||||
CommandResult(0, "", ""),
|
||||
CommandResult(0, "implemented", ""),
|
||||
CommandResult(0, "abc123\n", ""),
|
||||
CommandResult(0, "8 passed", ""),
|
||||
CommandResult(0, "", ""),
|
||||
])
|
||||
@@ -403,3 +406,222 @@ def test_state_store_round_trip_is_atomic(tmp_path):
|
||||
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 == []
|
||||
Reference in new issue
Block a user