Make the forge the authority on a branch's pull request, and stop the test suites lying about themselves #511
No reviewers
Labels
No labels
agent-ready
bug
contract
documentation
duplicate
enhancement
good first issue
help wanted
invalid
needs-human
needs-input
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
iterative-works/iw-cli!511
Loading…
Reference in a new issue
No description provided.
Delete branch "forge-truth-and-test-isolation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Four defects: one that let
--batchstop for a human it did not need, and threethat made the test suites lie about themselves.
phase-pr asks the forge which pull request is on this branch (#507)
A create that comes back
Duplicatemeans the forge already holds a pullrequest for the branch. Reading the url off the review-state card to find out
which one made a local record the input to a merge decision. The card is keyed
on the issue, so it can name an earlier phase's pull request; a repository that
keeps no card could not recover from a duplicate at all; and the guard that
0.12.0 added for the first case turned a stale card into a refusal that needed a
person.
TrackerOps.fetchPullRequestForBranchasks the forge instead. It reports thestate rather than filtering on it — merged, closed and open are three different
answers to a caller, and a lookup that returned only open ones would be silent
exactly when a caller asks what became of the work. Forgejo already had this
lookup and it was promoted rather than rewritten; GitHub and GitLab gained one.
A state a forge is not known to send is a
Left, not a guess: this readingdecides whether work gets merged.
Only an open request can carry the push. A merged or closed one is refused, and
so is a forge that will not answer. Every refusal names the branch, what the
forge said, and that nothing was gated, so a caller can tell it from a CI
failure by the message alone. It stays exit 1 — the 0/1/3 contract is documented
across six generated skills and does not gain a fourth code for this.
fetchPrHeadBranchshipped one release ago to check the card's url against thisbranch. Nothing consults the card here now, and all three lookups filter on the
branch at the source, so the check and the port method it needed are gone.
A slept fake clock passes time (#508)
A wait reads its budget off the clock and sleeps between polls. The fake
recorded the sleep and left the clock where it was, so a harness test that
scripted a state the wait could not leave spun in the poll loop instead of
failing — no assertion, no output, twenty minutes of a test JVM at 190% CPU
before it was killed by hand. Sleeping now moves the clock. Scripted readings
still win over it, so tests that drive a wait by hand are unchanged, and the
whole suite passed the change without an edit.
A test server proves it owns its port (#468, fault (a))
A port is chosen by asking the OS for a free one and letting it go again, so two
JVMs can be handed the same number. The loser's bind failed on a bare thread
where nothing caught it, its readiness probe was answered 200 by the winner, and
it spent the rest of the test writing into another test's server and reading
that server's state back as its own. Nothing in that produced an error — only a
worktree count that made no sense.
/healthnow reports which server answered. Both harnesses generate an identityper server, accept readiness only on their own, and move to a new port rather
than losing to whoever holds this one. A startup failure that is not a lost race
is reported with its own stack trace instead of being retried four more times.
Two tests picked their port by guessing a number in a range with no availability
check at all; the integration suite's two hand-rolled start loops are one
recursive helper.
The E2E suites stop deleting each other's worktrees (#468, fault (b))
Six suites created worktrees as siblings of a directory under the shared temp
directory and cleaned up by globbing that shared directory for anything named
after the project. Two runs on one machine deleted each other's worktrees
mid-test. Each suite now gives its project a parent of its own; every assertion
is relative to the project directory and is unaffected.
The E2E runner also pointed
IW_CORE_JARat a release path a developmentcheckout never has.
iw-rundiscards an override it cannot read, so the overridedid not save a resolution — it destroyed the good value
iw-runhad alreadyexported, and each of ~190 tests asked the build tool for the jar again. It now
names the jar the bootstrap actually left, and refuses the run if there is none.
API surface
TrackerOpsgainsfetchPullRequestForBranchand losesfetchPrHeadBranch; thethree clients lose their head-branch readers. Breaking only for something that
implements the trait or called those members.
PRStategainsfromGitHubandfromGitLab.CaskServergains aninstanceIdwith a generated default, and/healthgains that field.What the review changed
Eight review skills ran over this range. Three findings were real defects, not
polish.
The Forgejo lookup was a window, not an answer. No endpoint filters a
pull-request listing by head branch, so it scanned the 50 most recently updated
requests and filtered client-side.
Right(None)meant "not among those", andphase-prturned that into a refusal — on the forge this project actually uses.The base branch is now an input and every forge narrows on it. Verified against
live data on all three: a merged request with a deleted head resolves, and a
pair with none answers cleanly.
A branch name reached the GitLab query string unescaped. Git allows
&and#in a ref, so a branch could add a parameter to the request or truncate it.A merged request whose branch was deleted was invisible to the branch-only
listing. Forgejo rewrites
head.reftorefs/pull/<n>/headonce the branchis gone, and this tool's own merge deletes the branch by default — so the
dashboard read "no pull request" for every merged phase.
head.labelkeeps thebranch it was opened from, and the listing matches either now.
The rest:
runStreamingno longer prints its own diagnostic — it printed acaller's whole argv where a credential could sit, on a channel
Consoleowns,duplicating what its one attentive caller already reported;
killedBySignalisthe channel, and the tmux and herdr adapters now say a kill was a kill. One
selection rule instead of one per forge.
ForgeConfig.credentialsinstead ofthe same two lines in six places.
CoreJarbesideDashboardJarover oneInstalledJarsearch, so the path convention has one home rather than three.The E2E runner tells "could not start" from "ran and failed", and a suite killed
before its limit is no longer reported as having reached it. Both test-server
helpers are provably tail-recursive.
FakeClockrefuses to be slept a thousandtimes, so a loop that cannot end fails instead of hanging.
CI
Two runs of this branch went red with the tree green locally. Neither was the
code: every job began with
actions/checkout@v4, which the runner fetches fromgithub.com without credentials, and GitHub rate-limited it. The job died before
running a step, so a run read as a random subset of the suite failing. Both
actions now come from
code.forgejo.org, which the workflow already used forupload-artifact, and
CONTRIBUTING.mdrecords the rule.A third run hit an unrelated intermittent crash in the formatter itself, filed
as #512; it did not reproduce.
Verification
Full suite green locally: unit, lint, command compile, 192 E2E, dashboard unit
and integration. All six CI jobs green. The consumer compiled all six of its
commands and 23 hook tests against this branch, tested the deleted-head case
that its own path depends on, and confirmed the removals reach nothing of
theirs.
Closes #507
Closes #508
🤖 Generated with Claude Code
https://claude.ai/code/session_013UpRzj7oiV1UDArn4hfu7U