Skip to content

test(daemon): kill 22 surviving mutants in the licence, secret and tier helpers - #85

Merged
acamarata merged 1 commit into
mainfrom
test/helper-coverage
Sep 16, 2026
Merged

acamarata merged 1 commit into
mainfrom
test/helper-coverage

Conversation

@acamarata

Copy link
Copy Markdown
Contributor

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

read_cache_grace decides 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:

mutation effect
Utc::now() < valid_until> honours an expired cache, rejects a valid one
→ constant true turns a 24-hour grace period into an unbounded one
→ constant false / == silently downgrades a paying user mid-licence
delete ! on verify_hmac 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. A one-directional test would leave half of these alive.

write_cache is the classic async fn -> Result<()> case: the body mutates to Ok(()), 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.32 bits 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_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 — a padded credential can slip past. 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.

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.
  • 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.

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 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.

cargo fmt --check, cargo clippy --all-targets -- -D warnings and the full 886-test lib suite are clean.

…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.
@acamarata
acamarata merged commit 7b7e46a into main Sep 16, 2026
12 checks passed
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