Skip to content

fix(readiness): stop reporting weft's own failures as the user's tests failing - #216

Open
SoloJiang wants to merge 2 commits into
mainfrom
claude/issue-172-readiness-fixes
Open

fix(readiness): stop reporting weft's own failures as the user's tests failing#216
SoloJiang wants to merge 2 commits into
mainfrom
claude/issue-172-readiness-fixes

Conversation

@SoloJiang

Copy link
Copy Markdown
Owner

Extracted from #215, where these fixes were trapped inside an ~8500-line permission feature they have nothing to do with. They stand alone, they fix things affecting users today, and they are the root cause of the recurring macOS CI flake.

The shape of it

Three defects on the verification path, all the same shape: something that was not a statement about the user's code was recorded as one.

A check that never started was reported as a failing check. run_bounded_check returned Completed{status: "fail"} on a spawn() error — so when weft's own exec fails (a missing binary, a permission, or fork/posix_spawn returning EAGAIN because the machine is out of process slots), it told the user their tests were red. NotProduced is the arm that already means "no verdict was obtained", and it does not fail open: readiness reads it as not-ready. Every neighbouring early return in that function already went there; this one was the exception.

Queue time was charged to the Git work. GitSignatureProbe::sample started its wall-clock budget before waiting on a process-global 4-permit semaphore. A probe that queued too long returned the same error a hung git returns, .ok() turned it into None, and readiness read "the worktree changed" from what was really "the machine was busy". Admission and execution are now separate budgets: a probe still cannot queue forever and the fan-out is still capped, but one that gets scheduled gets its full budget.

A signal on the completed path stays a failure, and that is the deliberate other half. A child that ran and died on a segfault, an abort or an OOM kill is a real red verdict about the user's code. The line is whether a process ran at all.

Why the macOS flake never repeated the same way

Because which check loses its spawn is whichever one runs when the runner is tightest. Under parallel test load a trivially-passing exit 0 check intermittently spawn-failed and was read as Failing, so a different readiness test was named almost every run. That is why it looked like flakiness rather than a bug, and why --test-threads=1 always passed.

Test-only changes

noisy_check_streams_a_bounded_tail_before_its_deadline asserted a claim about output content (the tail is marked truncated) while gating it on a 50 ms wall clock. Passing the 2 KB budget takes the printf loop well under a millisecond once it is scheduled — what 50 ms could not reliably clear is sh being spawned at all. The truncation semantics move to output_tail_buffer_marks_only_the_writes_that_actually_discarded_bytes, a pure unit test decided by arithmetic with no process and no clock (coverage that did not previously exist in any form), and the integration test keeps the streaming assertion with a deadline sized against process-spawn latency rather than against the loop.

proc_registry::real_child_test_lock serializes the tests that spawn real children, so one test's tree-aware reap cannot kill another's.

Unrelated frontend fix, same extraction

spawnWorker was driveDirection minus its status !== "exited" filter and minus the pruning of the stale entry, so it treated an exited session as an occupied slot and returned without ever calling chatOpenWorker — a lane whose worker had exited could not be restarted by the path whose whole job is starting workers. No caller wanted that, so the copy is deleted rather than repaired in parallel. That made dispatchDirection and reviveDirection identical, so they now share one implementation and keep their distinct names for the call sites' sake.

Verification

  • cargo test --lib — 2161 passed. Three failures are environmental to the sandbox and reproduce on the base commit: it runs as root, so checkpoint::mid_restore_failure_rolls_everything_back cannot make a file undeletable via chmod 0o555, and two proc_registry reap tests depend on container process-group semantics. A fourth, completed_check_reaps_a_setsid_child_after_the_direct_parent_exits, passes in isolation — sandbox load, recorded rather than dropped.
  • cargo test --lib readiness:: — 73 passed.
  • pnpm build — clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Xz1Ts3AK1uNbeWZT6YFbzz


Generated by Claude Code

claude added 2 commits August 11, 2026 00:48
…s failing

Three defects on the verification path, all of the same shape: something
that was NOT a statement about the user's code was recorded as one.

`run_bounded_check` reported a `spawn()` failure as
`Completed{status:"fail"}`. When weft's own exec fails — a missing
binary, a permission, or `fork`/`posix_spawn` returning EAGAIN because
the machine is out of process slots — it told the user their tests were
red. `NotProduced` is the arm that already means "no verdict was
obtained", and it does not fail open: readiness reads it as not-ready.
Every neighbouring early return in that function already went there.

`GitSignatureProbe::sample` started its wall-clock budget BEFORE waiting
on a process-global 4-permit semaphore, so time spent queued was charged
to the Git work. A probe that queued too long returned the same error a
hung `git` returns, `.ok()` turned it into `None`, and readiness read
"the worktree changed" from what was really "the machine was busy".
Admission and execution are now separate budgets: a probe still cannot
queue forever and the fan-out is still capped, but one that gets
scheduled gets its full budget.

A child that dies on a signal on the COMPLETED path is the opposite case
and stays `fail`: it ran, and a segfault or an OOM kill is a real red
verdict. The line is whether a process ran at all.

Together these are what named a different readiness test on macOS CI
almost every run — non-deterministic and never the same test twice,
because which check loses its spawn is whichever one runs when the
runner is tightest.

Also splits `noisy_check_streams_a_bounded_tail_before_its_deadline`,
which asserted a claim about output CONTENT while gating it on a 50ms
wall clock: passing the 2 KB budget takes the printf loop under a
millisecond once scheduled, but 50ms could not reliably clear `sh` being
spawned at all. The truncation semantics move to a pure unit test
decided by arithmetic — coverage that did not previously exist in any
form — and the integration test keeps the streaming assertion with a
deadline sized against process-spawn latency.

`proc_registry::real_child_test_lock` serializes the tests that spawn
real children, so one test's tree-aware reap cannot kill another's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xz1Ts3AK1uNbeWZT6YFbzz
`spawnWorker` was `driveDirection` minus its `status !== "exited"` filter
and minus the pruning of the stale entry, so it treated an EXITED session
as an occupied slot and returned without ever calling `chatOpenWorker`. A
lane whose worker had exited could not be restarted by the path whose
whole job is starting workers.

No caller wanted that, so the copy is deleted rather than repaired in
parallel and its call sites go through `driveDirection`, which is
idempotent for a live slot. That made `dispatchDirection` and
`reviveDirection` identical, so they now share one implementation and
keep their distinct names for the call sites' sake.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xz1Ts3AK1uNbeWZT6YFbzz

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d4fa671df

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2853 to +2855
return Ok(BoundedCheckOutcome::NotProduced {
output_tail: format!("could not run {}: {error}", check.program),
}));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep spawn failures out of failed check records

When spawning fails because a binary is missing, permissions deny execution, or the host returns EAGAIN, this new NotProduced outcome is still converted by run_checks_with_timeout_report into a CheckResult with status: "fail"; record_verification_evidence then persists it with collection_state: "ok" and a summary such as 0/1 checks passed. The lane verdict becomes unknown, but the user-facing evidence ledger still records a fresh failed test—the exact misreport this change intends to eliminate. Omit unavailable results or represent them distinctly through aggregation, and cover that path at the report/evidence level rather than only testing run_bounded_check.

AGENTS.md reference: AGENTS.md:L34-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — the finding is correct, and it is not fixed on this branch. Verified on 2d4fa67:

  • run_checks_with_timeout_report, NotProduced arm → status: "fail" (readiness.rs:3048)
  • summary → {passed}/{total} checks passed (readiness.rs:3278)
  • collection_state: EVIDENCE_COLLECTION_OK hardcoded on every verification row (readiness.rs:3312)

One correction to the framing: the ledger misreport predates this PR. All three lines exist unchanged in main (2910, 3137-3142, 3174), where NotProduced is already reached by check timeouts and zero-rung suites. What this PR changes is that spawn failures now reach that same path — so it fixes the verdict and widens the reach of a pre-existing ledger defect. It does not introduce it.

The fix, and a trap worth recording before anyone writes it:

Do not identify "never started" by code == -1. A child killed by a signal has status.code() == None and also yields -1 on the completed path. Sniffing the code would reclassify a real segfault or OOM kill as "did not run" — destroying exactly the distinction this PR draws. The signal must come from the BoundedCheckOutcome variant.

Concretely:

  1. CheckResult.status becomes an enum (pass / fail / not_produced) rather than a bare String, so the ~6 read sites are compiler-checked instead of string-compared. The doc contract at check.rs:21 currently says "pass" | "fail".
  2. readiness.rs:2960's result.status == "fail" must become an exhaustive match — as an equality test it treats a third value as not-a-failure and would fall through to CheckEvidence::Passed, which fails open. That is worse than today's bug and is the natural place to walk into it.
  3. The summary needs a three-way partition. Adding a third value alone changes nothing there — not_produced is still != "pass", so the row still reads 0/1 checks passed. rc.checks.is_empty() at :3193 already has an honest "no checks were produced" string to mirror.
  4. collection_state must be derived per repo from that repo's own outcome, mirroring record_execution_evidence at :1947-1951. Keying it off the whole-report aggregate would mark every repo's row unknown because one repo failed. Stamping it unknown then makes the existing fail-closed chain — evidence_freshness (store/repo.rs:8733) → evidence_row_dtochange_set::evidence_scan — do the right thing with no new code.

Since the defect is in main and reached today by timeouts, I'd land it as its own PR rather than growing this one: the enum change crosses into the frontend (deriveTestsKind, WorkspaceKanban card urgency and progress-bar color), which is a different review surface from this PR's spawn/probe fixes.

Flagging one operational cost of leaving this PR unmerged: the flake it fixes is currently reddening unrelated PRs — #218 hit bounded_check_evidence_keeps_observed_failures_sticky (Failing vs Passed) on macOS for exactly this reason.


Generated by Claude Code

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.

2 participants