test(daemon): kill 22 surviving mutants in the licence, secret and tier helpers - #85
Merged
Merged
Conversation
…er helpers
Covers intelligence/upgrade.rs, policy/secrets.rs and the licence cache in
license/mod.rs, from the gate's exact surviving-mutant lists. Every mutant
was hand-applied and the suite confirmed to fail: 22 killed, 0 survived.
The licence cache is the part that matters most. read_cache_grace decides
whether a cached paid tier is still honoured while the licence server is
unreachable, and it had no test at all, so all of its mutants survived.
Flipping `Utc::now() < valid_until` to `>` honours an expired cache and
rejects a valid one; the constant-true variant turns a 24-hour grace
period into an unbounded one; dropping the `!` on the HMAC check makes
exactly the forged rows the trusted ones. Each is now pinned in both
directions - a fresh cache must be honoured, an expired or forged one
must not.
write_cache is the classic `async fn -> Result<()>` case: the body
mutates to Ok(()), which reports success without writing anything, so the
test reads the row back rather than trusting the return value.
The secret scanner's entropy branch needed fixtures built around the
arithmetic. The threshold is 4.5 bits and a 20-character string carries
at most log2(20) = 4.32 bits even when every character is distinct, so
nothing of the minimum length can ever trip it; the fixtures use 32
distinct characters (5.0 bits) and size their padding to move the
measurement across the threshold in a chosen direction. Forty dots either
side of a token measure 2.29 bits untrimmed and 5.0 trimmed, which
separates four different mutations of the trim predicate at once.
Two notes on what these tests record rather than endorse:
- `the_base64_alphabet_is_kept_by_the_trim` asserts a NEGATIVE. Sixteen
leading '+' pull a real 5.0-bit token down to 4.25 bits, under the
threshold, because '+' is deliberately kept by the trim as part of
the base64 alphabet. That is a genuine limitation of the heuristic.
The test pins the documented rule; it does not bless the gap.
- provider_for_model ignores its argument and answers "claude" for
everything. Correct only because every model in the upgrade chain is
a Claude model - the moment one is not, it mislabels it silently.
Two equivalent mutants are documented in place rather than chased:
- `Utc::now() < valid_until` -> `<=` differs only when the instant
equals the stored expiry to the nanosecond. Unkillable, not uncovered.
- in upgrade_model, `max_model == "sonnet" || max_model == "haiku"`
-> `&&`. The early return it guards is redundant with the tier check
below it: with the cap at sonnet or haiku the only upgrade target from
sonnet is opus, and `next_tier > max_tier` already rejects that. Both
versions return None for every input.
Also worth flagging, left alone because this change is test-only: in
model_tier, the `|| lower == "opus"` clause (and its sonnet twin) is dead
- any string equal to "opus" also contains it, so the equality can never
decide anything.
Inline `mod tests` blocks move to `<module>/tests.rs` with existing cases
kept verbatim. No production code changes - the source-side diff is 6
inserted lines, all `mod tests;` declarations.
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
intelligence/upgrade.rs,policy/secrets.rsand the licence cache inlicense/mod.rs, from the gate's exact surviving-mutant lists.Every mutant was hand-applied and the suite confirmed to fail: 22 killed, 0 survived.
The licence cache is the part that matters
read_cache_gracedecides whether a cached paid tier is still honoured while the licence server is unreachable. It had no test at all, so every one of its mutants survived:Utc::now() < valid_until→>truefalse/==delete !onverify_hmacEach is now pinned in both directions — a fresh cache must be honoured, an expired or forged one must not. A one-directional test would leave half of these alive.
write_cacheis the classicasync fn -> Result<()>case: the body mutates toOk(()), reporting success without writing anything, so the test reads the row back rather than trusting the return value.The entropy branch needed fixtures built around the arithmetic
The threshold is 4.5 bits, and a 20-character string carries at most
log2(20) = 4.32bits even when every character is distinct — so nothing of the minimum length can ever trip that branch. The fixtures use 32 distinct characters (exactly 5.0 bits) and size their padding to move the measurement across the threshold in a chosen direction.Forty dots either side of a token measure 2.29 bits untrimmed and 5.0 bits trimmed, which separates four different mutations of the trim predicate at once.
Two things these tests record rather than endorse
the_base64_alphabet_is_kept_by_the_trimasserts a negative. Sixteen leading+pull a real 5.0-bit token down to 4.25 bits, under the threshold, because+is deliberately kept by the trim as part of the base64 alphabet. That is a genuine limitation of the heuristic — a padded credential can slip past. The test pins the documented rule; it does not bless the gap.provider_for_modelignores its argument and answers"claude"for everything. Correct only because every model in the upgrade chain is a Claude model; the moment one is not, it mislabels it silently.Equivalent mutants, documented rather than chased
Utc::now() < valid_until→<=differs only when the instant equals the stored expiry to the nanosecond. Unkillable, not uncovered.upgrade_model,max_model == "sonnet" || max_model == "haiku"→&&. The early return it guards is redundant with the tier check below it: with the cap at sonnet or haiku the only upgrade target from sonnet is opus, andnext_tier > max_tieralready rejects that. Both versions returnNonefor every input.Noticed in passing
In
model_tier, the|| lower == "opus"clause (and its sonnet twin) is dead — any string equal to"opus"also contains it, so the equality can never decide anything. Left alone because this change is test-only.Inline
mod testsblocks move to<module>/tests.rswith existing cases kept verbatim. No production code changes — the source-side diff is 6 inserted lines, allmod tests;declarations.cargo fmt --check,cargo clippy --all-targets -- -D warningsand the full 886-test lib suite are clean.