tests: replace dead shim tripwires with a live no-coverage check - #77
Conversation
The two basename-collision scenarios grep stderr for `failed to resolve binary_id` and `basename fallback ambiguous` — strings the pre-NEXTEST_BINARY_ID shim emitted and the current one does not, so five assertions were permanently true. A binary_id the shim can't resolve is a soft failure: the test lands as Skipped and collect still exits 0 on the other binary's rows, so status.success() doesn't cover it either. Assert on "produced no coverage" instead — the line collect prints for any skip.
cargo-affected-bot
left a comment
There was a problem hiding this comment.
Self-review. CI is green on all three platforms, and the core claim checks out: rg finds neither failed to resolve binary_id nor basename fallback ambiguous anywhere in src/, and Skipped is only ever constructed from real extraction failures in shim.rs — never for a test that merely executes nothing — so the new assertions can't false-positive on the empty builds() test.
One observation beyond the inline nit. The replacements are still string-greps against a collect message, the same shape as the two they retire: a future reword of eprintln!("{skipped} test{s} produced no coverage") in collect.rs puts all four assertions back in the permanently-true state this PR exists to fix, silently. On the first collect in both scenarios that's cushioned — the SELECT DISTINCT binary_id assertions below would fail independently if a target were dropped. The uncushioned spot is the second collect in each scenario (recollect): neither re-queries test_regions afterwards, so the stderr grep is the sole guard there and also the only genuinely new coverage this PR adds. Re-running the same binary_id query after the recollect would pin the property against message edits as well as against a dropped target.
Review noted the replacements are still string-greps against a collect message, so a reword would silently retire them again — the same drift this PR fixes. The first collect is cushioned by the existing SELECT DISTINCT binary_id assertions; the second collect had no DB re-check at all. Hoist those queries into per-file helpers and run them after both collects, so the property is pinned independently of the message text.
…g output accepted (#864) ## Problem Step 2 asks the survey subagent whether each run's output was "accepted or rejected" but never says who has to do the accepting. Nothing in the prompt distinguishes a human merging a PR from the bot replying to its own review thread, so the subagent fills the gap with the most natural reading — a reply arrived, therefore someone accepted it — and reports a self-conversation as human acceptance. That is the worst-shaped failure this skill has: it produces a **false all-clear**. A survey that credits the bot's own reply to a human doesn't add noise a later gate can filter, it removes the signal that would have sent the leg to Step 3. ## Evidence This leg ([run 31081926964](https://github.com/max-sixty/tend/actions/runs/31081926964), target `max-sixty/cargo-affected`). The survey subagent reported PR [#77](max-sixty/cargo-affected#77) twice under "Runs with accepted output": > Outcome: ACCEPTED - human developer responded positively to feedback > Outcome: ACCEPTED - human developer incorporated feedback and concluded `PR 77: Developer accepted and incorporated bot feedback (2 cycles of acceptance)`. There is no developer. The PR is bot-authored and every actor on it is the bot: ``` $ gh api "repos/max-sixty/cargo-affected/pulls/77/comments?per_page=100" --jq '[.[].user.login] | unique' ["cargo-affected-bot"] ``` The same report also listed the PR author as `cargo-affected-bot`, so the contradiction was internal to the summary — the acceptance claim was reached without ever comparing the replying login against the bot's. This is the fourth cheap-subagent mis-attribution recorded in the [evidence gist](https://gist.github.com/dca23a6e6a0d8cae2665944ba31676fb), and the second of this specific sub-class (bot activity attributed to a human): | Occurrence | Shape | | --- | --- | | gist "Analysis-side", leg ending 07:5x | inline replies + commit `551e059` credited to "human applied suggested changes" | | gist "Analysis-side", following leg | PR #54 bodies credited to a run that had not yet started | | gist "Analysis-side", streak-break leg | fabricated no-op causes; window boundary ignored | | **this leg** | **bot's own reply chain on PR #77 reported as human acceptance** | Prior legs' response was to stop delegating — ten consecutive legs record "this leg used **no** survey subagent". That is a per-leg workaround for a defect in the shared prompt, and it leaves the skill's stated default (`Delegate all broad exploration to a cheap subagent`) pointing at a step that mis-reports actors. ## Fix Three small changes to Step 2, all inside the prompt template: - Require the subagent to **name the login** behind every acceptance/rejection signal, sourced from a single `gh pr view --json number,state,author,mergedBy,reviews,comments,commits` that covers all five actor surfaces — merge actor, reviews (with state), inline comments, conversation comments, and commits. A comments-only check can't source it: neither comments endpoint carries review records or the merge actor, so a maintainer merging a bot PR without commenting would come back as bot-only. Inline commenters need no separate call — every inline comment, including a standalone reply, belongs to a review record that `reviews` returns. The block notes that `commits` truncates at 100 (oldest-first) while `comments` and `reviews` paginate in full. - Give bot-only threads their own report bucket (`bot-only — no human signal`) so they can't be silently filed under "accepted". - Tell the main agent to verify actor attribution before it enters a finding, and to judge bot-only threads on content rather than treating the bucket as either outcome. The rule is stated so it stays correct when the bot legitimately works on its own PR: bot-only is *not* a defect (a self-review followed by the author's fix is designed behavior), it just isn't **acceptance**. Nothing here discourages delegation — it fixes the instruction the delegation was missing. ## Gate assessment - **Evidence level**: High — consistent pattern across multiple sessions. 4 occurrences total, 2 in the human/bot sub-class this change addresses. High needs 2–3. - **Structural or stochastic**: structural in the prompt. The subagent is never asked for actor identity, so no wording of the reply can be relied on to supply it; which specific claim it invents is stochastic, the omission is not. - **Change type**: targeted fix — a missing requirement in an existing prompt, plus a report bucket. Normal bar, met. - **Passes both gates.** Second finding this leg did **not** pass and is recorded in the gist instead: the three-hop `tend-mention` chain on the same PR #77. That one is already fixed by open PR #849, and I've added this window's evidence there rather than opening a duplicate. --------- Co-authored-by: tend-agent <270458913+tend-agent@users.noreply.github.com>
…estore The rebuild comment credited "the pre-run listing refreshes paths" for the lookup staying exact. That is not the mechanism: attribution comes from NEXTEST_BINARY_ID, and the map is found by the binary's file name, which a rebuild preserves — the BinaryStamp check in load_function_map is what rejects a stale map. Name both halves instead. The trailing `git checkout -- mock-stub/src/lib.rs` ran after the edit was already committed, so it restored the edited content onto itself: a no-op on a tempdir about to be dropped.
cargo-affected-bot
left a comment
There was a problem hiding this comment.
Self-review of the two commits since the last review. 56406d7 closes the gap the previous review raised — both scenarios now re-query test_regions after the second collect, and because plain collect takes the store_coverage path (DELETE FROM test_regions WHERE env_fingerprint = ?1 before insert), those re-queries can't be satisfied by the first collect's rows. a56181c's comment corrections check out against the source: map_path looks the map up by binary.file_name(), and the BinaryStamp comparison in load_function_map is what rejects a stale map, so NEXTEST_BINARY_ID + stamp is the right pair to name. The dropped git checkout -- mock-stub/src/lib.rs is a genuine no-op — the edit it appears to undo is committed ~15 lines above — and git is still used by that commit, so the import stays.
One correction to the PR body's overlap section, which is now stale in a way that could misinform the merge order. It says #63's non-message hunks are "independent and unaffected". #63's second hunk carries "expected bin binary_id in {ids:?}", and the ); after it as leading context, and this PR replaced that whole block with the assert_both_binary_ids(dir, "after the first collect") call — that string no longer exists anywhere in lib_bin_collision.rs at this head, so #63 will conflict on both of its hunks, not just the message one. Worth rewording so whoever sequences the two isn't planning around a stale reading.
One inline note on the residual prose in the same file.
The last surviving "pre-run listing ... probe path" line in this file described the pre-NEXTEST_BINARY_ID shim, three lines below a hunk that already replaced its assertions. Attribution comes from NEXTEST_BINARY_ID, and each target's function map is keyed by the binary's own file name, whose hash suffix differs even where the stem collides — no listing, no probe. Same drift a56181c corrected in duplicate_target_names.rs.
Problem
Five assertions across the two basename-collision scenarios grep stderr for strings the code no longer emits, so they are permanently true:
Both strings belonged to the pre-
NEXTEST_BINARY_IDshim, which resolved binaries by path and probed byte markers to disambiguate colliding basenames. That resolver is gone — the shim now readsNEXTEST_BINARY_IDstraight from the env (src/shim.rs#L115-L120) — but the assertions that pinned its behavior stayed behind.tests/CLAUDE.mdcalls silent under-selection "the one failure mode this tool cannot detect downstream"; these two scenarios are supposed to be part of that guard and currently aren't.The gap is not theoretical, because a failure to resolve a
binary_idis a soft failure. That test lands asTestOutcome::Skipped, and as long as some other binary produced rows,collectstill exits 0 (src/collect.rs#L376-L379). So the survivingcollect.status.success()assertion doesn't cover it either: a green collect whose database is missing one of the two colliding binaries is exactly the shape both scenarios exist to catch.Fix
Point each assertion at
"produced no coverage"— the linecollectprints whenever any test is skipped — and drop the now-redundant secondbasename fallback ambiguouscheck. The module docs'used to bail with …history is untouched; those describe the old shim in the past tense and are accurate.Verification
Per
tests/CLAUDE.md("a test written for a bug must be seen failing against the pre-fix behavior"), each new assertion was run against an injected break — a temporary branch inshim.rsreturningSkippedfor one of the two colliding binaries, simulating a resolution failure on exactly one target. Both scenarios failed on the new assertion; neither would have failed on the old one.Injected-break output
Break applied in
src/shim.rs(not committed):duplicate_basename_with_stripped_debuginfo_resolves_correctly— nextest reports both tests passing,collectexits 0, the database gets one test's rows instead of two, and the old string appears nowhere in stderr:lib_bin_same_basename_resolves_via_nextest_binary_idwith the break narrowed tobin/wt-perf(both of its targets share thewt_perf_collidepackage, so the broader predicate tripped the hardmappings.is_empty()bail instead):With the break reverted,
cargo testis green: 41 passed, 0 failed.cargo fmt --checkandcargo clippy --all-targetsboth clean.Follow-up commit: the same drift, in the prose next to it (
a56181c)The nightly survey drew
duplicate_target_names.rsand found two more artifacts of the same pre-NEXTEST_BINARY_IDshim, in the lines immediately around the hunks above. Folded in here rather than opened as a third PR against this file — #99 is already the second.The rebuild comment named the wrong mechanism. It credited "the pre-run listing refreshes paths, so the lookup remains exact" for the partial-rebuild case surviving. Paths are not what saves it, and the shim's own module doc says so directly (
src/shim.rs#L38-L48): attribution comes fromNEXTEST_BINARY_ID, and the map is looked up by the binary's file name (map_path), which a rebuild preserves — "cargo's hash suffix tracks build metadata rather than contents — a rebuilt binary keeps its name." What actually rejects a stale map after the rebuild this scenario forces is theBinaryStampcheck inload_function_map. The comment now names both halves. A second comment further down ("pre-run listing must still produce a working binary_map") had the same error and is corrected to what the second collect actually has to do: re-export a map whose stamp matches the rebuilt binary.A trailing restore that restores nothing. The scenario ended with
git checkout -- mock-stub/src/lib.rs, but the edit it looks like it's undoing was committed ~15 lines earlier, so the checkout wrote the edited content back over itself — a no-op, on atempdirthat drops on the next line either way. Removed.Both are comment/dead-code changes with no behavioral effect, so there's no regression test to add; the existing scenario is the coverage. Verified with
cargo clippy --all-targetsclean and the fullcargo testsuite green (41 passed, 0 failed) after provisioningllvm-toolsandcargo-nextest, which the agent sandbox doesn't ship — the gap #96 fixes.Third commit: the last "probe path" line in this file (
469ee12)The review pass over
a56181cfound the same artifact three lines below a hunk this PR already rewrites, inlib_bin_collision.rsthis time: "A second collect drives the pre-run listing through the same probe path again". There is no probe, and the pre-run listing is not what attributes a test to a target. The comment now names what the second collect actually re-exercises —NEXTEST_BINARY_IDfor attribution, and a function map filed under the binary's own file name (map_path), whose-<hash>suffix differs per target even though thewt_perfstem is what collides in this scenario. Comment-only;cargo fmt --checkandcargo check --testsclean.Overlap with open PRs
This section previously read that #31's and #63's non-message hunks were "independent and unaffected". That is wrong — most of their hunks land on lines this PR rewrote:
!stderr.contains("basename fallback ambiguous"), an assertion67e96ecdeleted. Hunk 2 rewords the recollect comment469ee12just rewrote, and carries"expected bin binary_id in {ids:?}",plus its);as leading context — a block that became theassert_both_binary_ids(dir, "after the first collect")call. So both hunks conflict, not just the message one.duplicate_target_names.rscommentsa56181ccorrected; hunk 5 is docs: drop stale "marker probe" wording in lib_bin_collision test #63's deleted assertion and hunk 6 its recollect comment. Only its module-doc hunk and itscargo_affected_strippeddoc-comment hunk are independent of this PR.duplicate_target_names.rsimports and thecargo_affected_strippedhelper — no overlap with anything here, though it does sit on the same function as tests: refresh duplicate-basename comments for NEXTEST_BINARY_ID shim #31's surviving hunk.Every one of those conflicts is "the stale line this PR rewords is already gone", so none is a reason to hold anything; the point is only that whoever sequences the three shouldn't plan around the earlier reading.