Skip to content

The Claim owns its lifecycle, tested in-process behind a seam - #383

Merged
JacobStephens2 merged 2 commits into
mainfrom
issue-382
Oct 5, 2026
Merged

JacobStephens2 merged 2 commits into
mainfrom
issue-382

Conversation

@JacobStephens2

Copy link
Copy Markdown
Owner

Summary

The Claim now handles its whole lifecycle. The Run makes it, then hands it the goal the run reached, or none for a Failed run. The Claim decides what that means.

 run
-  let claim = claim::make(issue)
+  let claim = claim::make(issue, launch)
   let outcome = run_in_worktree(..)
-  match outcome
-    Merged      => claim.remove_if_closed()
-    ReadyForReview => {}
-    Failed      => claim.release_if_nothing_on_origin(launch)
+  claim.end(outcome.ok().map(goal))
src/claim.rs
├── make / Claim::end       # public interface: make it, end it once
├── Claimed                 # the rules: label, remove if closed, release if nothing on origin
├── retry_if_interrupted    # "an interrupt doesn't stop it", asked through the seam
├── trait Outside           # labels, state, started on origin, apply Edit, interrupted?, step, warn
├── OnGitHub                # production adapter: gh, branch::started, interrupt, progress
└── in_memory::InMemory     # test fake: labels, open, started, interrupted, failing reads/edits

Progress lines, warnings and the requests sent are unchanged.

Evidence

  • Before: the Claim's rules had no unit tests. Every rule was checked by running a whole Run against the fake gh, git and claude, and the retry while interrupted could only be reached by sending a signal to a real process.
    After: 19 unit tests in claim::tests use the in-memory fake and cover making the Claim, ending it as Merged, Ready for review and Failed, the warnings with their commands to run by hand, and a failed step tried once more while interrupted. For example:
    release fails once, interrupted
      => Started, Labels, Step, Edit(fails), Interrupted, Started, Labels, Step, Edit
      => labels back to [ready-for-agent], no warning
    
    This PR removes 15 end-to-end tests that only checked a Claim rule. One wiring test stays for each path in the issue's list.
    cargo test (32 suites, all pass), cargo clippy --all-targets and cargo fmt --check are all clean.

Merge Danger

Door: two-way

This is a refactor inside one module plus its single caller. Nothing persistent or user-facing changes.

Blast Radius: labels

If the wiring were wrong, issue labels (in-progress, ready-for-agent) could be left wrong at the end of a Run or a Spec run. The wiring tests that remain cover making, keeping, releasing and removing the Claim.

Unaddressed findings

Standards

  • Duplicated Code (retry_if_interrupted repeats interrupt::retry_if_interrupted): the Claim's copy has to ask the seam whether the run was interrupted, and it passes the same &mut outside to both the step and that question. Sharing one helper would mean closures that borrow the seam twice, while the helper itself is only two lines. delivery.rs keeps the original.
  • Primitive Obsession (Option<Goal>, None = Failed): the issue chose this on purpose: "The outcome is given as the Goal reached, or none for a Failed run."
  • Middle Man (Claim::end wraps Claimed::end): this split lets unit tests use Claimed without a Git for the Launch directory. Claim only binds the production adapter.
  • Fake's fail_if uses only the first entry per call: this is test-only and no test sets the same call to fail twice.

Spec

  • Merge path keeps two end-to-end tests: an_issue_github_closed_after_the_merge_has_its_claim_removed_too mainly checks that the Delivery skips gh issue close on an issue GitHub has already closed. That is not a Claim rule, so it stays.
  • Tests that create the in-progress label in the repository stay in fresh_run.rs: they check how Edit adds a missing label to the repository, which is a labels rule rather than a Claim rule, and the fake can't reach it.

Closes #382

🤖 Generated with Claude Code

JacobStephens2 and others added 2 commits October 4, 2026 20:19
The Claim module is made, then ended once with the goal the run reached
or none for a Failed run, and decides there whether to remove, keep or
release the Claim. It reaches GitHub, origin, the interrupt flag and
progress lines through a private Outside seam with an OnGitHub adapter
and an in-memory fake, so its rules, the retry while interrupted
included, have unit tests. End-to-end tests that only checked a Claim
rule give way to them, one per wiring path staying.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests assert only on the calls and labels the fake records, not on the
Claim's private fields; the issue's state crosses the seam as a named
struct; the private Claim record is Claimed; the Merged unit test checks
the labels left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JacobStephens2
JacobStephens2 merged commit bf07e62 into main Oct 5, 2026
13 checks passed
@JacobStephens2
JacobStephens2 deleted the issue-382 branch October 5, 2026 00:33
@JacobStephens2 JacobStephens2 mentioned this pull request Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The Claim owns its lifecycle, tested in-process behind a seam

1 participant