Skip to content

Fix three intermittent CI failures (one is a real stale-misread race) - #8

Merged
ben-vargas merged 1 commit into
mainfrom
claude/focused-taussig-839cf1
Oct 9, 2026
Merged

ben-vargas merged 1 commit into
mainfrom
claude/focused-taussig-839cf1

Conversation

@ben-vargas

Copy link
Copy Markdown
Owner

Three tests failed intermittently on GitHub-hosted runners on PRs that don't touch the code under test (PR #6 at 7ee1e3f, PR #7 at 76cc047). Each one is fixed below. #1 was a real production bug, not a test bug.

1. cli.test.js › detached run + status + result --wait — run flo_…: stale (root · node 18.17.1)

Cause (production). deriveRunState reads result.json first and run.lock last, with the control-socket probe (≤300 ms) in between. A run that finishes inside that window is seen as "no result, no lock, journal present", and that gets classified stale. The engine closes its socket, writes result.json, and only then releases the lock. The mock workflow goes from lock to result in about 15 ms. CI's failing test ran for 235 ms, so the very first result --wait poll got the verdict. result --wait, flowition status, MCP flowition_result/flowition_status and the viewer all treat stale as a crash.

Fix. After the lock is seen released, if result.json has appeared, derive again from it, once (src/run-state.js). This relies on the engine's existing order: result first, then lock release.

Repro. I simulated CI's interleaving on Linux with Node 18.17.1: the engine starts 300 ms late and the reader stalls after its heartbeat read. Base: 10/10 stale. Fixed: 0/10. On macOS the reader stall alone gave 10/10 before and 0/20 after.

Regression test (test/mcp.test.js): a fake engine finishes on the reader's own probe connection, which fixes the interleaving deterministically. It fails with stale before the fix.

The same misread also showed up once under docker --cpus=1 as seed-from: MCP flowition_run accepts and forwards seedFrom → actual: 'stale'. That is a fast-failing engine with no heartbeat, hitting the same race.

2. viewer-search.test.js › SSE keepalive stays on schedule during a 64 MiB search (root · node lts/*)

Cause (test). The test required gaps of at most 60 ms (3 × a 20 ms keepalive) between pings, measured in the same process as the search. One preemption or GC pause breaks that on its own. CI saw a single 99 ms gap while every other gap was ≤46 ms. Under CPU contention it failed 0/25.

Fix. The test now checks DESIGN §5.4.7's actual contract, "the event loop breathes every ≤1 MiB", by event-loop order instead of milliseconds:

  • The keepalive interval is 1 ms.
  • An injected clock costs at least 1.1 ms per deadline check, so a keepalive is due at every loop turn between chunks.
  • Keepalive writes (at the server's res.write) and per-chunk deadline checks are recorded on one thread.
  • It asserts at least one check per MiB, and at most 2 checks between consecutive keepalive writes (the only 2 is the startup pair).
  • The frozen clock also removes the 2 s search deadline as a wall-clock dependency, so the full 64 MiB is always scanned (truncated: false is now asserted).

The test still uses a real SSE stream, and the keepalives must reach the real client.

Stress and mutation results. The new test passed 25/25 under the same contention on Node 18.17.1 and 24. Mutation checks:

Change to the search Old test New test
8 MiB chunks passed on fast hardware fails ("10 deadline checks across 64 MiB")
Sync scan fails fails
readFile then sync scan fails fails
Variants that still breathe every ≤1 MiB passes passes

3. cockpit.test.tsx › show-args disclosure — expected 'reading args…' to contain '"repo": "flowition"' (viewer · node 18.17.1)

Cause (test). The test ran waitFor(argsFn called), but that call happens synchronously inside the click. So the wait returned at once, and the panel was asserted before the promise's setLoaded render had committed. I reproduced the exact CI error deterministically by delaying argsFn by 2 ms.

Fix. The test now waits for the rendered value, then asserts argsFn was called exactly once for this run. It passes with 2, 50 and 300 ms of args latency, and 8/8 under load on Node 18.17.1/Linux.

Verification

  • npm test (root): 549/549 on macOS. On Linux (Node 18.17.1 and 24, --cpus=4) the only failures were the two ps-based pid-reuse tests, because the node:*-slim image has no ps. GitHub's runners have it.
  • npm --prefix viewer test: 73 files / 1166 tests, plus the walkthrough and perf suites, all green.

…an async assert

1. cli.test.js "detached run + status + result --wait" (`run flo_…: stale`) exposed a
   real bug in deriveRunState: it read result.json first and run.lock last, with the
   control-socket probe (≤300 ms) between, so a run that finished inside that window
   read as "no result, no lock, journal present" — a completed run classified stale.
   The engine writes result.json before releasing run.lock, so derivation now
   re-derives once when a result has appeared by the time the lock is seen released.
   Reproduced 10/10 with a stalled reader + slow engine start (CI's interleaving);
   0/10 after. Pinned by a deterministic mcp.test.js case that completes a fake
   engine on the reader's own probe connection.

2. viewer-search.test.js "SSE keepalive stays on schedule during a 64 MiB search"
   measured wall-clock gaps (≤ 60 ms) in the same process as the search, so a single
   preemption or GC pause failed it (0/25 under CPU contention). It now proves
   DESIGN §5.4.7's contract ("the loop breathes every ≤1 MiB") by event-loop ORDER:
   a 1 ms keepalive made due at every turn by an injected ≥1.1 ms-per-check clock,
   keepalive writes and per-chunk deadline checks recorded on one thread. 25/25 under
   the same contention on Node 18.17.1 and 24, and it now also catches 8 MiB chunks,
   which the old budget missed on fast hardware.

3. cockpit.test.tsx show-args: waited for argsFn to be *called* (synchronous in the
   click) and then asserted the panel before the promise's render committed. It now
   waits for the rendered value, then asserts exactly one read.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T09:44:49.642278Z 4ba201c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ben-vargas
ben-vargas merged commit 967fb70 into main Oct 9, 2026
8 checks passed
@ben-vargas
ben-vargas deleted the claude/focused-taussig-839cf1 branch October 9, 2026 09:52
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.

1 participant