tmux-dependent BATS tests fail non-deterministically under a full ./iw ./test run #468
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
iterative-works/iw-cli#468
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Found triaging IW-437 (observed closing out phase 2).
SYMPTOM. Two consecutive ./iw ./test runs over the same unchanged tree failed DIFFERENT tests, and every failing test passed when its file was run alone.
Run 1: test/open.bats:53 and test/open-prompt.bats:118
both on
tmux -L "$TMUX_SOCKET" has-session -t ...returning non-zeroRun 2: test/analyze.bats:54
on the delegated
iw startexiting non-zerobats test/open.bats test/open-prompt.bats -> green
bats test/analyze.bats -> green
(all seven cases)
RULED OUT.
NOT DIAGNOSED. The shape - an assertion immediately after a command that spawns a tmux session, failing only under a full-suite run - points at a race between session creation and the assertion that reads it. That is a hypothesis, not a finding. Please diagnose rather than paper over it.
WHY IT MATTERS BEYOND THE ANNOYANCE. A red ./iw ./test in this repository is not by itself evidence that the change under test broke something. That cuts both ways: someone who re-runs until green can equally well be re-running past a real regression. Until this is fixed, an E2E failure has to be confirmed by running its file alone before it is attributed to the diff - and that is exactly the kind of instruction that gets skipped.
./mill core.test (186/186) is deterministic across every run; the non-determinism is confined to the tmux-dependent BATS suites.
GOAL. A red E2E run means what it says.
Two more instances, from IW-476. I opened #489 for these before finding this issue; #489 is closed as a duplicate and its content is folded in here.
The dashboard suite flakes too, not only the tmux BATS suites. This issue says
./mill core.testis deterministic and confines the non-determinism to tmux-dependent BATS.dashboard.testbelongs on the list as well:That test binds a real port and writes a state file, so it has its own shared-resource surface independent of tmux.
A correlated condition worth testing against the race hypothesis. Both IW-476 sightings happened while a second iw-cli worktree was running
batch-implementon the same host. If the cause is a race between session creation and the assertion that reads it, concurrent load from another worktree is a plausible amplifier — and a cheap way to reproduce on demand rather than waiting for it.The E2E sighting:
./iw ./test e2eexited 1 with 7not oklines across theissue create,openandrmsuites;bats test/rm.batsalone passed both its tests; the next full run returned exit 0 with 190 ok.A third shape, possibly separate — please treat it as its own question. Pushing the IW-476 feature branch on 2026-09-01, the
pre-pushhook printed:The hook's step 3 is
if ./iw ./test unit && ./iw ./test compile. No./iw ./test compileoutput appeared at all, so./iw ./test unitreturned non-zero while printing a passing suite. Run standalone on the same tree both exit 0, andbash .git-hooks/pre-pushrun directly passes. The identical push retried immediately and succeeded with no change.This one is not explained by BATS non-determinism — the hook's unit step runs Mill only. It may be a separate fault in how
.iw/commands/test.scalareports its exit code. Related: #466 (./iw ./testgreen on code the hook rejects) and #465 (a failed push drops the hook's stderr).Recorded as IW-476 LESSONS.md entry 10.
Two more sightings from IW-436, both with a named suspect. Recording them here since #489 was merged in.
1.
project-commands-execute.batstest 2 (2026-08-29, IW-436 Phase 1 verification)./iw ./testexited 1 with a single failure:Line 99 is the first assertion after
run ./iw-run dualname— the shared command invocation returned non-zero.bats test/project-commands-execute.batsalone passed 3/3, and a second full./iw ./testpassed with nonot oklines.Suspect (hypothesis, not reproduced): the test writes two scala-cli command files that declare the same
@mainname (dualname) in two namespaces,.iw-install/commands/and.iw/commands/, and compiles both in the same run. A race on scala-cli's shared build cache between two concurrent compilations of the same main class would explain it.Blocker to diagnosing it: the assertion checks only
$status, and the failure output does not include$output, so no scala-cli error text was captured. First move is to assert on$outputas well, so the next occurrence is readable.2.
CaskServerTestraces on its own port (2026-09-01, pushing IW-436 phase-04)git pushwas blocked by the pre-push hook with one failure:Named mechanism: the test allocates a port with
findAvailablePort()and then starts Undertow on it (dashboard/jvm/test/src/CaskServerTest.scala:1629-1633). The gap between choosing the port and binding it is a TOCTOU window, and the full-suite log shows dozens ofio.undertow.Undertow startlines inside the same three seconds — so two forked test classes can pick the same free port.Confirmed as infrastructure, not a regression:
./mill dashboard.testalone returned 241/241 immediately afterwards, the followinggit pushpassed every check, and the failing case never reaches the code that phase changed.Concrete fix available now: bind the server to port 0 and read back the assigned port, or retry the bind on
BindException. Worth auditing the other tests that callfindAvailablePort()the same way.Recorded during IW-436; see
project-management/issues/IW-436/LESSONS.mdentries 4 and 13.The
pre-pushanomaly reported above is diagnosed, and the cause is Mill client concurrency rather than anything in the tests.The failing push, with the step named. The hook's dashboard step printed:
So
./iw ./test unitexits non-zero because Mill throws, not because a test fails — and the hook's box then printsPUSH BLOCKED: Tests failed. That is the misreporting filed as #497. The competing PID had already exited when checked, so the contention is transient.The daemon's own log names the mechanism.
out/mill-daemon/server.log— which is append-only across daemon restarts, unlikeout/mill-daemon/stderr— shows:A second client connects, one client leaves mid-command, the daemon reads a partial message as the next request, fails to parse it and exits. The client then prints "Worker wire broken, worker likely crashed".
The same signature covers the older sightings. The log retains 2026-08-29 00:54-00:59, when IW-476's unit suite was failing repeatedly: the same
client interrupted/TraceExceptionpair, across several daemon pids in a row. That is why./mill shutdownappeared not to help — each fresh daemon was re-killed within seconds — and why--no-daemonworked, since it has no long-lived RPC wire.Why this belongs on this issue. It supports the concurrent-load hypothesis for the suite flakes too: two Mill clients against one daemon is a shared-resource collision of exactly the kind this issue is looking for, and it is reproducible on demand by running two Mill tasks in the same
out/at once — cheaper than waiting for the BATS races.Whether the tmux BATS non-determinism has the same cause is not established.
dashboard.testand the hook path go through Mill; the BATS suites do not.Recovery guidance is now in
docs/testing.mdunder "When the Mill daemon dies".Fault (c) is fixed on
mainas ofcfe98763(PR #506). Faults (a) and (b) are untouched and stay here.What went in.
ProcessAdapter.killedBySignalnames both 143 and 137 and is public;run'stimedOutuses it, so a child that ignored SIGTERM and had to be forced is no longer reported as a child that failed.runStreamingkeeps returning the signal code — a consumer already matches on it — and prints one line on stderr naming the command it killed and the limit it waited.runUnitTestsgives each Mill suite the ten minutes the E2E and contract suites already had, names the suite that did not pass, and says when a suite was killed rather than calling it failed. Thepre-pushhook runsunitandcompileas separate steps so the banner names the one that blocked the push.A new sighting of the flake class, 2026-09-03.
./iw ./test unitred onIssuePollerTest."a read that throws leaves the poller able to read again". Green alone, and green on the next fulldashboard.testwith no change. That test does no I/O of its own, which puts it with (a) rather than (b): the suite shares a JVM, and something in it is order- or timing-dependent.A fixture hazard found while writing the tests for #467, worth its own issue. A
PhasePrHarnessTestcase that reachesCiGate.pollwith a forge head sha that matches no run in the scripted snapshot does not fail — it spins inCiGate.loop, heartbeating the card, indefinitely.FakeClockdoes not advance on its own, so the wait budget that bounds the real gate never expires in a harness test. Twenty minutes of a test JVM at 190% CPU before it was killed by hand. The gate is right; the fixture gives a mistyped test no way to end.Intake: needs-human
Workflow: diagnostic
Reason: G7 a standing [fix] finding after the one correction: the analysis gives the tmux teardown fix to six suites, but nine have it (port-slot, service and attach-unbounded are missing)
Analysis: project-management/issues/IW-468/analysis.md on branch IW-468