Repository navigation
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #178 +/- ##
==========================================
+ Coverage 93.48% 93.51% +0.02%
==========================================
Files 272 272
Lines 35363 35363
Branches 6001 6001
==========================================
+ Hits 33060 33068 +8
+ Misses 1489 1485 -4
+ Partials 814 810 -4 🚀 New features to boost your workflow:
|
29385d8 to
432f037
Compare
Add a non-blocking mutation workflow (weekly + workflow_dispatch) that runs mutmut 3.8.0, hash-pinned in the tools lock, over eval/stats.py, eval/binary_stats.py, the score/regression/critical-band/trust gates and caching.py, and writes the score per module and the survivors to the step summary. [tool.mutmut] limits the run to those modules and their tests. gate/novelty_gate.py is left out: mutmut 3 copies the 440-line evaluate_novelty once per mutant. New tests kill the survivors that were real gaps: thresholds at their exact values, the Wilcoxon veto on a significant t-test, MIN_PAIRS, the bootstrap alpha and n_resamples edges, each critical-band condition reading its own key, every trust check's value and the rendered block, the cache key's role, sampling parameters, image blocks and a pinned key. Killed mutants go from 1566 of 2080 (75.3%) to 1913 (92.0%); the rest are equivalent or cosmetic. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
The tests are real and they kill what the PR says they kill. I'd fix the gaps below before merge, because the PR's point is that a surviving mutant is a wrong number that ships, and several mutants of the same kind survive the new tests.
What I ran, on a Linux review host at 432f037:
make ci-local: 27 passed, 1 failed, 7 skipped; 7,175 tests passed. The one failure,test_repo_environment.py::test_unbalanced_quote_in_hook_completes_quickly(1.7 s against a 1 s limit), passes on its own (513 passed in that file). Python 3.13, so the 3.11 leg is not covered here.- Hand-made mutants of the seven pilot modules (about 310), run against the new test files and against the 14-file selection in
[tool.mutmut]. Thedrop >= critical_threshold->>mutant from the description is killed. The survivors are in the inline comments. - The step-summary script from
mutation.yml, run on made-upmutmut resultslines (function and class-method names,no tests,timeout,suspicious): the module grouping and the table are right.zizmor --offlineon the workflow: no findings. - I did not run
mutmut runitself, so the 75.3% -> 92.0% figures are not checked.
Findings that cannot be anchored inline:
- The mutation-only tool now lands in the shared tools lock.
requirements-ci.lock.txtgains 352 lines for mutmut:libcst,textual,linkify-it-py,mdit-py-plugins,pyyaml-ft,setproctitleand others.ci.yml,release.yml,pip-audit.ymland the eval image install that whole lock, so each now installs and audits packages only the weekly workflow uses. An advisory ontextualorlibcstwould fail the daily pip-audit. Suggestion: a separaterequirements-mutation.lock.txtthat onlymutation.ymlinstalls. If the shared lock is intended, say so in the PR description.
Checked, no issues
- Gate modules: all 22 mutants of
critical_bands.pyand all 11 ofscore_gate.pyI tried are killed (per-condition keys, strict<, defaults, guards). - Cache key: the pinned-hash test hashes an explicit JSON literal, not a stored digest, so it is stable across Python versions and platforms. Dropping the role, model,
extraorsort_keys, or changingensure_ascii, fails it. - Config: all 14 files in the test selection exist, the
only_mutateentries end in.pyas mutmut 3 requires, the lock entry formutmut==3.8.0is hashed, andtests/test_dependency_locks.pypasses. - Tests: all four gate test files pass in other orders and with different pytest-randomly seeds; they use no I/O.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy
left a comment
There was a problem hiding this comment.
I pushed a commit per nit, up to d57ed0e, which fix the nit from my review:
d57ed0e:dokimi/CONTRIBUTING.md:147: now names the score, regression, critical-band and trust gates and saysnovelty_gate.pyis not mutated.
make ci-local at d57ed0e: 28 passed, 0 failed, 7 skipped.
Not approving yet. Still open: the seven risks from my review.
|
Review-body item 1 (mutmut in the shared tools lock): fixed in c5c6715. requirements-ci.txt and requirements-ci.lock.txt are back to their pre-PR content. mutmut==3.8.0 now sits in requirements-mutation.txt, locked into requirements-mutation.lock.txt (uv pip compile with the same options as the tools lock: --constraint=requirements.lock.txt, --universal, --python-version=3.11, --exclude-newer=P7D, --generate-hashes). Only mutation.yml installs it (requirements.lock.txt plus the new lock). tests/test_dependency_locks.py now covers the new lock (hashes, header, cooldown, stacked pins agree with the main lock, mutmut not in the tools lock); Renovate's pip-compile pattern and .licenserc.yaml include it. pip-audit does not audit the new lock; that keeps its closure out of the daily audit, as you suggested. Say so if you want it audited. |
… inside the job mutmut and its closure move from requirements-ci.lock.txt to requirements-mutation.lock.txt, which only mutation.yml installs. Run mutmut with a 75-minute step timeout so a slow run is a step failure that continue-on-error absorbs, not a cancelled job. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ilter, coverage floor, bootstrap tails and MIN_PAIRS_FOR_TEST Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Checked the new lock: regenerating requirements-mutation.lock.txt with the command in its header (uv 0.11.9, same options as the tools lock: --constraint=requirements.lock.txt, --universal, --python-version=3.11, --exclude-newer=P7D, --generate-hashes) gives the committed file byte for byte; the header, hashes and stacked pins pass tests/test_dependency_locks.py, and mutation.yml installs only requirements.lock.txt and this lock. One gap found and fixed in c6f1bfe: moving mutmut out of the tools lock had dropped its closure from pip-audit, so pip-audit.yml now audits requirements-mutation.lock.txt too (test_pip_audit_audits_every_lock fails without it), and CONTRIBUTING no longer says it is unaudited. |
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 91d9ae6. The six earlier threads are fixed: the default thresholds, the score flags, the both-arms filter, the coverage floor, MIN_PAIRS_FOR_TEST and the step timeout all hold. The new tests still leave mutants alive in the same modules. The six inline comments and the first numbered item below are worth fixing before merge.
What I ran, on a Linux review host at 91d9ae6:
make ci-local: 28 passed, 1 failed, 7 skipped; 7,315 tests; changed-line coverage 100%. The failure ispip-audit: gitpython 3.2.0 (CVE-2026-114145, fix 3.2.1), pinned inrequirements.lock.txton the PR's base as well, so it is not caused by this PR.- About 350 hand-made mutants of the seven pilot modules, run against the new test files and the 14-file selection from
[tool.mutmut]. The survivors I re-ran are in the comments and below. I did not runmutmut runitself, so the 75.3% -> 92.0% figures are not checked. zizmor --offline --min-severity mediumon the workflows: no findings.tests/test_dependency_locks.py: 28 passed. The summary script frommutation.ymlon made-upmutmut resultslines: the table is right.
Findings that cannot be anchored inline:
- Length guards and the no-SciPy seed are pinned in one direction only. Each of these passes the 14-file selection (881 passed):
paired_test:len(treatment) != len(control)->len(treatment) > len(control)(stats.py:479). A shorter treatment vector raises fromzip(strict=True)instead of returningNone.mean_ci:bootstrap_ci(vals, confidence=confidence, seed=seed)-> withoutseed=seed(stats.py:154). Without SciPy the seed is ignored.
Suggestion:assert stats.paired_test([1, 2, 3], [1, 2, 3, 4]) is Noneand the shorter-treatment mirror; withHAS_SCIPYmonkeypatched toFalse, assertmean_ci(vals, seed=7)differs from the default seed on a sample whose bounds depend on it.
- Nit:
dokimi/SECURITY.md(outside this diff) lists the lockspip-auditruns against and does not namerequirements-mutation.lock.txt. Suggestion: add it to that sentence.
Checked, no issues
- Fixes from my review: the four default-lowering mutants,
has_critical/has_warningsdropping the score and== "WARN"->!= "OK", the dropped context half of the both-arms filter,min_retrieval_coverage0.80 -> 0.75,MIN_PAIRS_FOR_TEST6 -> 5, and thealpha,lo_idxrounding andseed + 1mutants are all killed now. - Locks:
requirements-ci.lock.txtis back to the base content, mutmut sits only inrequirements-mutation.lock.txt, installed bymutation.ymlalone, andpip-audit.ymlaudits it. - Workflow:
Run mutmuthastimeout-minutes: 75under the job's 90, withcontinue-on-errorand anif: always()summary step. Actions are SHA-pinned,permissions: contents: read. - Determinism: the four gate test files and
test_stats.py,test_binary_stats.py,test_caching.pypass under three different pytest-randomly seeds. - Cache key: the pinned hash is of a literal payload with sorted keys, so it is platform-stable.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
test_pip_audit_audits_every_lock matched lock names anywhere in pip-audit.yml, so the comment above the step (and, for the tools lock, the Install tools step) kept three of its four cases green after the lock was dropped from the audited sed list. Parse the step's run instead. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
grounding.py calls bootstrap_ci with every default, so changing seed=0 or n_resamples=10_000 moved every grounding_score interval with no test failing. Assert literal lo/hi for a defaults-only call. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
I pushed a commit per nit, up to b5785d6, which fix the nit from my review:
b5785d6:dokimi/SECURITY.md:47: the list of lockspip-auditruns against now namesrequirements-mutation.lock.txt.
make ci-local at b5785d6: 28 passed, 1 failed (pip-audit, same as at the reviewed head), 7 skipped.
Not approving yet. Still open: the seven risks from my review.
|
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 55b32d8. The only change since b5785d6 is two commits by @xaleryb: the pip-audit lock test now parses the step, and a new test pins bootstrap_ci's defaults. Both are right. Five of my six threads still stand, and one item from my earlier review body is still open.
What I ran, on a Linux review host at 55b32d8:
make ci-local: 28 passed, 1 failed, 7 skipped; 7,316 tests passed. The failure ispip-audit: gitpython 3.2.0, pinned inrequirements.lock.txton the PR's base as well, so this PR does not cause it.- The mutants from my threads and from my earlier body, re-run against
test_stats.py,test_binary_stats.py,test_regression.py,test_trust_gate.py,test_critical_bands.py,test_score_gate.pyandtest_caching.py(267 passed on each): 10 of 10 survive, listed below and in the threads. - Ten mutants of the cache key (role, sampling parameters, image blocks,
sort_keys,ensure_ascii,default=str, block joining): all killed. The twobootstrap_cidefault mutants (seed 0 -> 1,n_resamples10,000 -> 5,000): both killed by the new test. - The five test files above under two
pytest-randomlyseeds and with sockets disabled: 221 passed each time. I did not run mutmut itself.
Findings that cannot be anchored inline:
- Length guard and the no-SciPy seed are still pinned in one direction only. This is the first item of my earlier review body, not yet addressed. Both survive the seven test files:
paired_test:len(treatment) != len(control)->len(treatment) > len(control)(stats.py:479). A control longer than the treatment is no longer rejected as degenerate.mean_ci:bootstrap_ci(vals, confidence=confidence, seed=seed)-> withoutseed=seed(stats.py:154). The seed is ignored on the bootstrap fallback.
Suggestion:assert stats.paired_test([1, 2, 3], [1, 2, 3, 4]) is Noneand the shorter-treatment mirror; withHAS_SCIPYmonkeypatched toFalse, assertmean_ci(vals, seed=7)differs from the default seed on a sample whose bounds depend on it.
I agree with @xaleryb that the weekly summary does not mark a partial run. New evidence: made-up mutmut results lines with 1 killed, 1 survived and 3 not checked print | dokimi.eval.stats | 1 | 1 | 3 | 50.0% |, and a module with nothing checked prints n/a; nothing says the run was cut short.
Still open from my earlier review:
dokimi/tests/test_stats.py:598: thehi_idx/lo_idxmutants (int(...)withoutround,n_resamplesforn_resamples - 1) all still pass; the new defaults test does not reach them.dokimi/tests/test_binary_stats.py:211: removingseed=seedfrommcnemar_test'sbootstrap_cicall still passes.dokimi/tests/test_regression.py:197:has_warningswithoutmetric_regressions, andhas_criticalwith only the first metric, both still pass.dokimi/tests/test_trust_gate.py:318:if agreement_scores:->if len(agreement_scores) > 1:still passes.dokimi/tests/test_trust_gate.py:404:if verdict.warnings:->if verdict.failures:still passes.
Checked, no issues
- pip-audit test:
test_pip_audit_audits_every_locknow reads thepip-auditstep'sruntext. A lock name that survives only in a YAML comment no longer satisfies it: removing it from thesedcommand fails the test. - Cache key: the new tests kill every key mutant I tried; the pinned hash is of a literal payload.
- Determinism: the pinned
bootstrap_ciinterval uses onlyrandom.Random, so it does not depend on SciPy or NumPy. - Locks and workflow: mutmut sits only in
requirements-mutation.lock.txt, installed bymutation.ymlalone and audited bypip-audit.yml;Run mutmuthas a 75-minute step timeout.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…der mutants Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Review-body items, by sha (fork/ci/mutation-pilot at d27b2e3):
|
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. The five tests added since my last review kill the mutants from my threads, and the new "Incomplete run" banner reads the right counts. Two nits inline.
What I ran, on a Linux review host at d27b2e3:
make ci-local: 28 passed, 1 failed, 7 skipped; 7,856 tests passed. The failure ispip-auditonfilelock3.32.7 andsetuptools84.0.0, pinned inrequirements.lock.txtandrequirements-ci.lock.txt, which this PR does not touch.- The mutants from my five threads, the length-guard and no-SciPy-seed mutants from my earlier body, and about 20 further hand-made mutants of the seven pilot modules (thresholds, boundaries, defaults, cache-key and usage handling), run against the seven pilot test files: all killed except the one in the
caching.pynit. I did not runmutmut runitself. - The summary script from
mutation.ymlon made-upmutmut resultslines (clean run, not checked, a module with nothing scored, empty file): the banner appears only for the incomplete runs.
@xaleryb's point on the [tool.mutmut] test selection waits for the author. At this head caching.draw_extra ({**extra, "draw": draw} for draw 0) and caching.first_replayed (range(attempts - 1)) mutants pass the seven pilot test files and fail only test_judge_response_cache.py, so they will show as survivors although a test kills them.
Checked, no issues
- Threads:
hi_idx/lo_idx(noround,n_resamplesforn_resamples - 1),seed=seedinmcnemar_test,has_warningswithoutmetric_regressions,has_criticalwithmetric_regressions[:1],len(agreement_scores) > 1andverdict.failuresforverdict.warningsall fail the new tests. - Length guard and seed:
len(treatment) > len(control)andmean_ciwithoutseed=seedare killed. - Config: all 7
only_mutatepaths and 14 selected test files exist;tests/test_dependency_locks.pypasses.
| assert [p.name for p in tmp_path.iterdir()] == ["k.json"] | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- |
There was a problem hiding this comment.
Nit: _env_flag has a surviving mutant that no test kills. raw.strip().lower() not in _FALSEY -> raw.lower() not in _FALSEY (caching.py:94) passes the seven pilot test files (297 passed). test_prompt_cache_env_switch (line 123, outside this diff) tries only 0, false, off, "", 1 and yes. With the mutant, DOKIMI_..._CACHE="off\n" or " off " leaves the cache on, and OFF or FALSE would too without .lower().
Suggestion: add (" off ", False), ("OFF", False) and ("False\n", False) to the parametrization of test_prompt_cache_env_switch.
| unchecked = sum(c["not checked"] for c in counts.values()) | ||
| unscored = sorted(m for m, c in counts.items() if not c["killed"] + c["survived"]) | ||
| if unchecked or unscored: | ||
| print(f"\n> **Incomplete run: do not read the scores as final.** {unchecked} mutants were " |
There was a problem hiding this comment.
Nit: the banner says "0 mutants were not checked (a step timeout or a crash cut the run short)" when only a module has nothing scored. A module whose mutants are all no tests or timeout has unchecked == 0 and still gets that text.
Suggestion: build the text from the parts that apply, for example print the "not checked" clause only if unchecked, and the "nothing was scored in ..." clause only if unscored.
There was a problem hiding this comment.
Applied in 3ea86b2 (Anton Volkov). The banner names only the clauses that apply and is printed only when a mutant is not checked or a module has nothing scored. Pushed in b74f999. The step script is not under a pytest test; checked by running it on made-up mutmut results lines: clean run (no banner), partial run (not-checked clause), unscored module (nothing-scored clause).
There was a problem hiding this comment.
Fixed in 3ea86b2: the banner text is built only from the parts that apply. The "not checked" clause prints only if unchecked, and the "nothing was scored in ..." clause only if unscored. A module where every mutant is no tests or timeout now gets only the second clause.
…itch Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
The seed assertion in test_the_interval_follows_the_confidence_and_the_seed could not fail: these deltas give the same interval for every seed. Compare the default interval instead and point at the test that does pin the seed. Thread: PRRT_kwDOVBg7g86xmBfu Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The fake bootstrap_ci defaulted confidence to 0.95 and recorded only the other keywords, so dropping confidence=confidence from the fallback call in mean_ci survived. Capture the forwarded confidence and call with 0.8. Thread: PRRT_kwDOVBg7g86xmBgD Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ute one The only degenerate case had noise of 1e-10, below both 1e-9 * 1e6 and a fixed 1e-9, so max(1.0, abs(mean)) -> 1.0 survived. Add noise of 1e-4: it is degenerate around 1e6 and not around 1.0. Thread: PRRT_kwDOVBg7g86xmBgG Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Only the upper side of len(treat) >= 2 was exercised, so >= 1 survived: a
one-pair arm would have shown up in the result. Add the one-pair and the empty
cases, both expecting {}.
Thread: PRRT_kwDOVBg7g86xmBgc
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…low one The test used scores 0.5 and 1.5, a mean of exactly 1.0, so `if with_avg > 0` -> `>= 1` survived. Use 0.25 and 0.75 (mean 0.5), which that mutant turns into a CV of 1.0 instead of 0.707. Thread: PRRT_kwDOVBg7g86xmBgk The six inline findings are also the OpenCodeReview summary comment and its review wrapper, answered here as one batch: Thread: IC_kwDOVBg7g88AAAABg2JulQ Thread: PRR_kwDOVBg7g88AAAABU7QgWw Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Review body item 1 (mutmut in the shared tools lock); the seven inline findings are their own threads. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Status review for the SECURITY.md nit. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
LGTM review; its test-selection point is asked on IC_kwDOVBg7g88AAAABgAyVFQ. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The one question is answered in thread PRRT_kwDOVBg7g86wYhJz. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
CI failures it lists no longer occur: every check of 9b13d65 passes. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Item 1 (mutmut in the shared tools lock) is fixed in c5c6715 by napetrov: |
|
Fixed in b5785d6 by Anton Volkov: |
|
The two inline nits are the threads on |
|
The one question of this review is the thread on |
|
No new claim. The three failing checks listed in the CI table are green now: |
|
Installing the Codecov GitHub app is an organisation setting, not a change to this PR. The report itself says all modified and coverable lines are covered. |
|
Summary banner on a partial run: d27b2e3. PR body: edited (mutmut is in |
|
Waiting on a decision before I change this: which test selection should |
| _EPHEMERAL = {"type": "ephemeral"} | ||
|
|
||
|
|
||
| def test_the_model_name_decides_the_prefix_breakpoint_on_a_shared_provider(): |
There was a problem hiding this comment.
The name says the model decides, but only the positive case (bedrock + Claude) is asserted. The system-breakpoint test has a meta.llama3 negative, this one doesn't. Mutation: build_messages checks provider in {"bedrock", ...} without looking at the model. It would still set the breakpoint on a Llama model on Bedrock, and this test passes. Add a bedrock meta.llama3-70b-instruct-v1:0 case asserting cached is False and that the content is the plain string LONG + "q".
| assert not result.passed | ||
|
|
||
|
|
||
| def test_a_band_without_a_value_has_a_zero_floor(): |
There was a problem hiding this comment.
The None, NaN and empty cases are not covered. {"score": None} makes float(None) raise TypeError, and float('nan') < threshold is False, so a NaN metric passes the gate silently. The new tests only cover a missing key. Add tests that pin the intended behaviour for None and NaN (raise, or treat as a violation). Also cover a spec with no critical_bands key, and an empty fail_on list in the parametrized set.
| @pytest.mark.parametrize("name", ("requirements-ci.lock.txt", "requirements-mutation.lock.txt")) | ||
| def test_tools_locks_are_constrained_to_main_lock_by_header(name: str) -> None: | ||
| assert "--constraint=requirements.lock.txt" in header_command(name) |
| assert pins("requirements-mutation.lock.txt")["mutmut"] == {"3.8.0"} | ||
| requested = (ROOT / "requirements-mutation.txt").read_text(encoding="utf-8") | ||
| assert "mutmut==3.8.0" in requested |
There was a problem hiding this comment.
Nit: this hardcodes the mutmut version, so every Renovate bump of the lock fails the test, which defeats the lock tracking. If mutmut is missing from the lock, the ["mutmut"] lookup raises a bare KeyError. Instead, assert that mutmut is in the mutation lock and not in the ci lock. Then compare the exact pin in requirements-mutation.txt with the locked version, as test_tools_lock_has_every_requested_pin does for the ci lock. The substring check would also accept mutmut==3.8.01.
| def test_bootstrap_with_one_resample_collapses_to_it(): | ||
| res = stats.bootstrap_ci([0, 1], n_resamples=1) | ||
| assert res["lo"] == res["hi"] | ||
| assert res["lo"] in (0.0, 0.5, 1.0) |
There was a problem hiding this comment.
This assertion cannot fail. The mean of two draws from [0, 1] is always 0.0, 0.5 or 1.0. The lo == hi check is also trivial with one resample, since lo_idx and hi_idx both clamp to 0. Mutating the confidence handling or swapping lo/hi still passes. Pin the exact value for seed 0 (for example res["lo"] == res["hi"] == <literal>) so a seed or indexing mutation is caught.
| def test_zero_variance_tolerance_scales_with_the_mean(): | ||
| """At a mean of 1e6, noise of 1e-10 is below the relative tolerance.""" | ||
| pytest.importorskip("scipy") | ||
| vals = [1e6 + (1e-10 if i % 2 else -1e-10) for i in range(8)] |
There was a problem hiding this comment.
This test does not show that the tolerance scales with the mean. Noise of 1e-10 gives sd of about 1e-10, which is below 1e-9 even with an absolute tolerance. Mutation: change max(1.0, abs(mean)) to 1.0 and this test still passes. Only the next test catches that mutation. Use noise that is degenerate only under the scaled tolerance, as the second test does.
| def test_paired_test_rounds_the_wilcoxon_p(): | ||
| pytest.importorskip("scipy") | ||
| res = stats.paired_test([3, 5, 4, 6, 2, 7, 5], [1, 2, 2, 1, 2, 3, 1]) | ||
| assert res["p_wilcoxon"] == round(res["p_wilcoxon"], 4) |
There was a problem hiding this comment.
This compares the value to its own 4-place rounding, so it passes for any rounding of 4 or fewer digits. Mutation: change _round_p to round(p, 3), or round to a coarser value, and the test still passes. Only dropping the rounding entirely would fail it. The 0 < p < 1 check adds almost nothing. Pin the literal, for example assert res["p_wilcoxon"] == <expected 4-place value>.
| def test_a_legacy_artifact_serializes_measured_false(): | ||
| check = _check(evaluate_trust(_evals(10)), "retrieval_hit_rate") |
There was a problem hiding this comment.
The name (and the docstring of the test above: "serializes measured as false (the report JSON reads it)") promise a serialization check, but both tests only read attributes of the in-memory check. The serialized key is written in report/generator.py ("measured": c.measured), and no test in the repo asserts report["trust"]["checks"][...]["measured"]. Mutation: delete "measured": c.measured from that dict, or hard-code it to True. Both tests still pass. Assert on the generated report JSON, or rename the tests to what they check.
|
🔍 OpenCodeReview found 4 issue(s) in this PR. By severity: 1 medium, 3 low
Review details
|
|
|
||
| name: mutation | ||
|
|
||
| # Mutation pilot: mutmut over the measurement core (the statistics, the gate |
| continue-on-error: true | ||
| run: mutmut run --max-children "$(nproc)" |
There was a problem hiding this comment.
continue-on-error: true here plus || true on mutmut results mean a broken run (mutmut crash, bad config, or a missing test file) leaves the job green. Only the summary text shows it. That is acceptable for a pilot, but nobody reads a summary on a green weekly job. Consider failing the job, or emitting a ::warning:: annotation, when the results file is empty or has not-checked mutants.
| # The locks are the full closure (requirements-docs.lock.txt for the | ||
| # documentation site, requirements-ci.lock.txt for the tools), so --no-deps --disable-pip audits them as written | ||
| # documentation site, requirements-ci.lock.txt for the tools, | ||
| # requirements-mutation.lock.txt for the weekly mutation run), so --no-deps --disable-pip audits them as written |
There was a problem hiding this comment.
This edit lands in a 10-line # block (lines 51-60). scripts/check_comment_blocks.py fails any block of 3+ lines once a PR edits a line in it, so this will likely fail CI's static job. The edited line is also over-long. Shorten the block to at most two lines, for example by moving the per-lock list out of the comment.
Suggestion:
| # requirements-mutation.lock.txt for the weekly mutation run), so --no-deps --disable-pip audits them as written | |
| # The locks are the full closure (docs, tools, mutation), so --no-deps --disable-pip audits them as written. | |
| # Each --ignore-vuln is documented in SECURITY.md and osv-scanner.toml; change all three together. |
| # test that leans on another's leftovers fails. The header prints the seed; | ||
| # `--randomly-seed=<seed>` replays that order, `-p no:randomly` keeps file order. | ||
|
|
||
| # Mutation pilot (mutmut 3.x, pinned in requirements-mutation.txt; run by |
There was a problem hiding this comment.
This comment block is 10 lines. The repo rule (scripts/check_comment_blocks.py, AGENTS guide) caps comments at two lines and fails a PR that adds a block of three or more # lines. The scripts/check_comment_blocks.py file isn't in the tree I could search, so I can't confirm it covers TOML. Shorten it to the constraint (what source_paths / only_mutate mean, why novelty_gate.py is excluded) and move the rationale to the PR description.
Summary
This PR adds a mutation-testing pilot for the measurement core: the paired statistics, the gates and the response-cache key. In this code a surviving mutant means a wrong number would ship and no test would catch it. The pilot uses mutmut 3.8.0, with an exact pin, hash-locked in its own
requirements-mutation.lock.txtthat onlymutation.ymlinstalls (andpip-audit.ymlaudits). A weekly or on-demand workflow writes the score and the survivors to the job summary. It never fails a build..github/workflows/ +└── mutation.yml # weekly + workflow_dispatch, contents: read, non-blocking dokimi/ ├── pyproject.toml # + [tool.mutmut]: only_mutate = the 7 pilot modules, │ # test selection = their 14 test files ├── requirements-mutation.txt # mutmut==3.8.0 ├── requirements-mutation.lock.txt # hash-locked closure, constrained to requirements.lock.txt ├── CONTRIBUTING.md # + "Mutation pilot" paragraph └── tests/ # + kills for the survivors that were real gapsMutant counts below are from the first local run; later review rounds added tests that kill further survivors.
eval.statseval.binary_statsgate.regressiongate.score_gategate.critical_bandsgate.trust_gatecachingThe new tests pin behaviour that no existing test checked:
MIN_PAIRS_FOR_TEST.n_resamples=1edge.binary_stats):pseudo_replicated.What is still left is mostly equivalent mutants:
"ZSPLIT".zip(strict=)after a length check.min(2.0, p)with p ≤ 1.<=vs<that differs only at p == 0.05 exactly.gate/novelty_gate.pyis out of scope. mutmut 3 copies the whole 440-lineevaluate_noveltyonce per mutant, and generation alone grew past 26 MB. A diff-only mode for PRs is a follow-up.Evidence
Before: on main, the mutant
dokimi.gate.regression.x__classify_regression__mutmut_7survives:tests/test_regression.pyfrom main against that mutant gives6 passed. A drop of exactly the 0.08 default is reported as WARN instead of CRITICAL, and nothing notices.After: the same mutant against this branch's tests:
Whole pilot: the local
mutmut run --max-children 16takes about 1 minute after the first mutant generation. Before: 1566 killed, 514 survived. After: 1913 killed, 167 survived.Workflow summary: the step-summary script was run on the local results and produced the table above.
Checks:
pytest -n 16 --outcomes: 6986 passed, 2 skippedruff check .: cleanmypy: no issues in 222 fileszizmor --offline --min-severity medium .github/workflows: no findingsscripts/audit_test_quality.py: no new flagstests/test_dependency_locks.py: passesscripts/check_docs_review_triggers.py: no page to reviewMerge Danger
Door: two-way. The workflow is new and never runs on PRs or pushes. Removing it, together with the
[tool.mutmut]table, undoes the change.Blast Radius: tests
requirements-mutation.lock.txt; the tools lock other jobs install is unchanged.test_the_key_of_a_known_request_does_not_changepins the response-cache key format. A future change to the key has to update that hash on purpose, because a new key format orphans every stored entry.Checklist
pytestpassesruff,mypy, and schema checks pass.github/.licenserc.yaml)🤖 Generated with Claude Code