test(daemon): kill 26 surviving mutants in the tool and repo-context guards - #82
Merged
Merged
Conversation
…guards Covers the pure helpers in session/claude.rs (12 of its 27 missed mutants) and all 15 in intelligence/repo_context.rs, from the gate's exact lists at b697493. Both claude.rs helpers are long `||` chains, where a `||` -> `&&` mutation pairs the terms on either side of the operator. Such a mutation is only observable for an input that trips exactly one of that pair, so each test uses a tool name isolating a single link: "kill_process" and "terminal_session" for the high-risk chain, "apply_patch" / "replace_text" / "insert_line" / "append_to_file" for the medium one, "run_shell" and "terminal" for the is_shell_tool chain. The home-directory guard gets both directions: a path outside $HOME must be blocked and a path inside it must not. That pins all three of its mutations, two of which would lock an agent out of the user's own working tree rather than letting anything escape. build_modified_section had no coverage at all -- every one of its mutants survived -- so it now has real git2 fixtures. A single-commit repo and a two-commit repo together separate `parent_count() > 0` from `< 0`, `== 0` and `>= 0`: the first commit must diff against the empty tree, the second must diff against its parent and list only the file that changed. Every mutant was hand-applied and the suite confirmed to fail: 26 of 27 killed. The one that survives is equivalent and documented in place: replacing the `Ok(files) if !files.is_empty()` guard with a constant `true`. When files is empty, `files.join("\n")` is the empty string, which is exactly what the fallback arm returns, so no input can tell them apart. Two comparisons needed fixtures built to land on an exact value rather than near it. `total > MAX_ROOT_ENTRIES` is separated from `>=` by a directory holding exactly MAX_ROOT_ENTRIES files, which must produce no "... 0 more files" line. `out.len() > MAX_CHARS` is separated from `>=` by a context of exactly MAX_CHARS, which must not be truncated; that fixture computes its filename lengths from the constants and asserts it hit the cap before asserting anything else, so a later change to the header or section layout fails the test rather than quietly making it vacuous. Not covered here, and still open in claude.rs: the async runner internals (run_turn, event_loop) and the `pid != 0` guards in pause/resume/stop. Those need a live child process, and the pid guards carry a real hazard -- mutating `!= 0` to `== 0` makes the code call libc::kill(0, ...), which signals the entire process group. They need an isolated harness, not an in-process test. Inline `mod tests` blocks move to `<module>/tests.rs` with existing cases kept verbatim. No production code changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Covers the pure helpers in
session/claude.rsand all ofintelligence/repo_context.rs, from the gate's exact surviving-mutant lists atb697493.session/claude.rs—classify_tool_risk+validate_tool_argsintelligence/repo_context.rsEvery mutant was hand-applied and the suite confirmed to fail — 26 of 27 killed, with the single survivor matching the one predicted to be equivalent.
Isolating one link in a
||chainBoth claude.rs helpers are long
||chains. A||→&&mutation pairs the terms on either side of the operator, and that is only observable for an input tripping exactly one of the pair — so each test uses a tool name isolating a single link:kill_processandterminal_sessionfor the high-risk chainapply_patch,replace_text,insert_line,append_to_filefor the medium chainrun_shellandterminalfor theis_shell_toolchainOne term genuinely cannot be isolated from its own side: any name containing
overwritealso containswrite, which appears earlier in the same chain. That mutation is killed from thepatchside instead, which is the side that can be isolated.The home-directory guard is pinned in both directions — a path outside
$HOMEmust be blocked, a path inside it must not. That kills all three of its mutations, and two of them (&&→||, and dropping the!onstarts_with) would lock an agent out of the user's own working tree rather than letting anything escape. A guard tested only on the blocking path would miss both.build_modified_sectionhad no coverage at allEvery one of its mutants survived, so it now has real
git2fixtures rather than a mock. A one-commit repo and a two-commit repo together separateparent_count() > 0from< 0,== 0and>= 0:>= 0and== 0send it down the parent branch, whereparent(0)fails and the section collapses to"";< 0and== 0fall into the empty-tree branch, which reports every file in the tree as recently modified.Dropping the
!onis_sensitiveinverts the diff filter so the section lists only the secrets; that is pinned by committing a.envalongside a normal file.Fixtures built to land on an exact value
Two comparisons differ from their mutants at exactly one input, so the fixtures target that input rather than a value near it:
total > MAX_ROOT_ENTRIESvs>=— a directory holding exactlyMAX_ROOT_ENTRIESfiles, which must produce no... 0 more filesline.out.len() > MAX_CHARSvs>=— a context of exactlyMAX_CHARS, which must not be truncated. That fixture computes its filename lengths from the constants and asserts it hit the cap before asserting anything else, so a later change to the header or section layout fails the test loudly instead of quietly making it vacuous.Equivalent mutant, documented rather than chased
Replacing the
Ok(files) if !files.is_empty()guard with a constanttrue: whenfilesis empty,files.join("\n")is the empty string, which is exactly what the fallback arm returns. No input can tell them apart. This is the one that survived the hand-application run.Still open in claude.rs
The other 15 mutants are the async runner internals (
run_turn,event_loop) and thepid != 0guards inpause/resume/stop. Those need a live child process, and the pid guards carry a real hazard: mutating!= 0to== 0makes the code calllibc::kill(0, ...), which signals the entire process group. They need an isolated harness, not an in-process test, so they are deliberately left for separate work rather than half-covered here.Inline
mod testsblocks move to<module>/tests.rswith existing cases kept verbatim. No production code changes — the source-side diff is 4 inserted lines, allmod tests;declarations.cargo fmt --check,cargo clippy --all-targets -- -D warningsand the full 833-test lib suite are clean.