Skip to content

Latest commit

 

History

History
575 lines (456 loc) · 28.4 KB

File metadata and controls

575 lines (456 loc) · 28.4 KB

Mutation Testing — Triage and Operational Guide

Status: Active. Last updated 2026-05-18.

This document is the canonical place for mutation-testing triage, survivor classifications, and operational lessons. It is the output of the 2026-05-18 mutmut survey of bin/wl_validation.py and bin/wl_csv.py.


Why this doc exists

Two findings during the 2026-05-18 pre-release sweep made it clear that the mutation-testing harness needed documented governance:

  1. The prior mutmut runs were misconfigured. The default TEST_RUNNER_FILES in scripts/mutmut.sh is hard-coded to tests/unit/test_validation.py tests/unit/test_ascii_validation.py tests/unit/test_validator_fuzz.py. When a contributor invokes the script with MUTATE_PATH=bin/wl_csv.py but does not also override TEST_RUNNER_FILES, mutmut applies mutations to wl_csv.py while running tests that exercise wl_validation.py. The "survivors" reported are NOT genuine test-coverage gaps; they are artifacts of test selector / mutated module mismatch.

  2. The mutmut container mounts the host repo read-write. Line 89 of scripts/mutmut.sh is -v "$REPO_ROOT:/work". Every mutation mutmut applies is a real write to the host working tree. If mutmut is interrupted between mutate-and-restore (SIGKILL, container stop, host reboot, OS crash), the file remains in mutated state and git diff shows it as a normal edit. The 2026-05-18 session caught a live mutation (_csv_file_hash(csv_path)None in bin/wl_csv.py:184) that was about to be staged in a Phase 2 commit. The mutation would have silently broken CSV-hash bootstrapping.


Operational rules (mandatory)

1. Test selector MUST match the mutated module

When invoking the harness, both knobs are required:

# CORRECT — wl_csv.py mutations against wl_csv tests
MUTATE_PATH=bin/wl_csv.py \
TEST_RUNNER_FILES="tests/unit/test_csv.py" \
    scripts/mutmut.sh run

# WRONG — wl_csv.py mutations against wl_validation tests
MUTATE_PATH=bin/wl_csv.py \
    scripts/mutmut.sh run    # uses default TEST_RUNNER_FILES → garbage results

Mapping table (keep in sync if new test files land):

Mutated module Test selector
bin/wl_validation.py tests/unit/test_validation.py tests/unit/test_ascii_validation.py tests/unit/test_validator_fuzz.py
bin/wl_csv.py tests/unit/test_csv.py
bin/wl_versions.py tests/unit/test_versions.py (if present)
bin/wl_rbac.py tests/unit/test_rbac.py
bin/wl_audit.py tests/unit/test_audit.py

2. Verify the working tree is clean BEFORE staging anything after a mutmut run

After every mutmut run:

git diff bin/        # MUST be empty
git status --short   # MUST not show modifications to mutated files

If git diff bin/ shows a change, mutmut died mid-mutation and the file is in mutated state. Recover with git checkout -- <file>.

This rule is non-negotiable. A live mutation slipped to a commit once already (caught at stage time on 2026-05-18); next time the catch may not happen.

3. Never mutate bin/wl_handler.py directly

Already documented in scripts/mutmut.sh comments. The handler's tests are integration tests requiring a live Splunk container at ~30s per invocation. Multiplied by hundreds of mutations = multi-day runs. The handler's logic is mostly dispatch + delegation to other modules; mutating wl_validation.py, wl_csv.py, etc. gives a higher-signal result. RBAC paths in the handler are pinned by tests/integration/test_rbac_matrix.py.

4. Time budget per module — hard cap 2 hours

Mutmut on bin/wl_csv.py (1244 lines, 913 mutation candidates) exceeded an estimated 4-8 hour budget at the 11-hour mark on 2026-05-18 with only mutations 191–235 tested (≈5% coverage). If a run cannot complete in 2 hours per module, kill it and either split the test selector (run subsets in parallel) or accept partial coverage as the deliverable. Open-ended runs corrupt the working tree if interrupted (see rule 2).


2026-05-18 wl_validation.py survivor triage

Twelve mutmut survivors were reported on bin/wl_validation.py from the prior session. Manual analysis classifies them as follows.

Genuine test-coverage gaps — 2

These were closed in commit <TBD> by adding tests to tests/unit/test_validation.py. Each test was verified to FAIL on the mutant and PASS on the pristine module.

Mutant Line Mutation Killing test
116 107 "Only ASCII characters are allowed in text fields""XXOnly ASCII characters are allowed in text fieldsXX" TestValidateAsciiTextErrorMessage::test_validate_ascii_text_returns_exact_error_string
151 214 stem = name.rsplit(".", 1)[0]stem = name.rsplit(".", 2)[0] TestIsSafeFilename::test_is_safe_filename_rejects_multi_dot_stem

Why these matter:

  • Mutant 116 — validate_ascii_text returns the error string for direct UI display. Silent corruption to XXOnly ASCII...XX would ship as visible garbage to analysts on every non-ASCII rule name attempt. The original pytest truthy-check was indistinguishable.
  • Mutant 151 — without the test, a future refactor of rsplit(".", 1) to rsplit(".", 2) would accept filenames like foo.bar.csv — exactly the SPL base() / dashboard drilldown URL parsing hazard the validator exists to prevent.

Equivalent mutants — 10 (revised 2026-05-19)

These cannot be killed by any test because they produce indistinguishable behavior from the original. Documenting them here prevents future contributors from wasting time trying.

Mutant Line Mutation Why equivalent
91 31 sys.path.insert(0, ...)sys.path.insert(1, ...) Both achieve import resolution. Position only affects ordering vs. other entries that don't conflict with wl_constants.
104 65 if len(cleaned) > max_lengthif len(cleaned) >= max_length When len(cleaned) == max_length, cleaned[:max_length] == cleaned. The branch executes but produces identical output.
138 191 if "/" in nameif "XX/XX" in name Reclassified 2026-05-19 (was "killed by existing tests"). On POSIX (Linux mutmut container), os.path.basename at line 193 catches every /-containing input the mutated check would have rejected — basename treats / as the separator, so basename(name) != name fires on any path-traversal input. The / check at line 191 is genuinely redundant defense-in-depth on POSIX. The \\ half of the same or is still load-bearing (basename on POSIX does NOT split on \\), but mutmut leaves that part intact.
145 193 os.path.basename(name) != name → return Falsereturn True On POSIX (mutmut container), no constructable input passes the prior "/" and "\\" checks but fails os.path.basename != name — the basename divergence is itself caused by / on POSIX. Branch is unreachable.
146 197 name.startswith(".")name.startswith("XX.XX") Reclassified 2026-05-19 (was "killed by existing tests"). For input .hidden.csv: mutated startswith check returns False; the stem .hidden then fails _ASCII_FILENAME_STEM_RE at line 227 (regex ^[A-Za-z0-9_\-]+\Z rejects the leading dot). Function returns False via stem regex. There is no constructable input where startswith(".") would be load-bearing AND the stem regex would accept — because any input starting with "." produces a stem starting with "." which fails the alphanumeric/underscore/hyphen alphabet.
155 215 if not stem: return Falsereturn True Caught earlier by name.startswith(".") at line 197. Any input reaching the stem check has a non-empty stem.
157 222 ord(c) < 0x20ord(c) <= 0x20 Adds space (0x20) to rejection. Space already excluded by stem regex [A-Za-z0-9_\-]+.
158 222 ord(c) < 0x20ord(c) < 33 Adds space + ! to rejection. Both already excluded by stem regex.
160 222 ord(c) == 0x7ford(c) == 128 DEL (0x7f) already excluded by stem regex (not in alphabet). 128 is in _NON_ASCII_RE which fires earlier (line 218).
161 222 ord(c) < 0x20 or ord(c) == 0x7f... and ord(c) == 0x7f New condition impossible (single ord can't be both < 32 and == 127). Branch dead. Control chars still caught by stem regex.

The pattern is structural: is_safe_filename has overlapping defenses (path-separator check → basename check → extension check → stem regex → control-char check → alphanum check). Many control- char and edge-case mutants are caught by the stem regex regardless, making the control-char check defensive but mutation-test-invisible. This is a design choice (defense-in-depth) we accept.

2026-05-19 fresh-run confirmation

After items A (test-selector mapping) and B (safe :ro+tmpfs mount) landed, mutmut was re-run on bin/wl_validation.py with the corrected test selector (tests/unit/test_validation.py tests/unit/test_ascii_validation.py tests/unit/test_validator_fuzz.py tests/unit/test_frontend_backend_parity.py).

Result: 10 surviving mutants, all matching the equivalent set above.

This is the first run of mutmut on this module with a non-stale test selector. Notable findings:

  • The 2 new tests added in commit eddcb62 (test_validate_ascii_text_returns_exact_error_string and test_is_safe_filename_rejects_multi_dot_stem) successfully killed mutants 116 and 151 as predicted — confirmed by their absence from the survivor list.
  • Mutants 138 and 146 were initially classified as "killed by existing tests" in the original triage. The fresh run proved otherwise — both survived because is_safe_filename's overlapping defenses make the individual checks redundant for any input the existing tests use. The classification table above has been corrected.
  • Total mutation budget on this module: 100 mutations applied, 92 killed by the test suite, 10 survived as equivalent mutants. Effective mutation score: 92% (or 100% kill rate of non-equivalent mutants).

2026-05-18 wl_csv.py survivor triage — DISCARDED

The 45 "survivors" reported for bin/wl_csv.py are NOT actionable. The mutmut command in the prior session was:

mutmut run --paths-to-mutate=bin/wl_csv.py --tests-dir=tests/unit \
  --runner='python -m pytest -x -q --tb=no \
            tests/unit/test_validation.py tests/unit/test_ascii_validation.py'

The test selector exercises wl_validation.py, not wl_csv.py. Every mutation to wl_csv.py survived because the running tests never imported or called any wl_csv function. The 45 mutants are artifacts of misconfiguration (see "Operational rule 1" above).

To get genuine signal on bin/wl_csv.py, re-run:

MUTATE_PATH=bin/wl_csv.py \
TEST_RUNNER_FILES="tests/unit/test_csv.py" \
    scripts/mutmut.sh run

Re-ran 2026-05-19 — see next section for results.


2026-05-19 wl_csv.py fresh-run results (item D)

Mutmut run with the corrected selector (tests/unit/test_csv.py tests/unit/test_diff_fuzz.py) on a fresh :ro-mount + tmpfs container.

Result: 723 mutations applied, 547 survived, 176 killed. Effective mutation score: 24%.

Why the score is low

bin/wl_csv.py is 1244 lines. Only ~600 of those — the diff engine (compute_diff, compute_added, compute_removed, compute_edited) and the hash-registry plumbing (_csv_file_hash, update_csv_expected_hash, bootstrap_csv_expected_hashes, remove_csv_expected_hash) — have unit-test coverage in test_csv.py and test_diff_fuzz.py. The remaining ~600 lines (save_csv_pipeline ~lines 747–1148 and create_csv_pipeline ~lines 1149–1244) are tested via the live Splunk container in tests/integration/, NOT by the unit suite mutmut is exercising.

This shows up in the survivor ID ranges: IDs 191–472 (the unit- testable core) have a mix of killed and survived mutations (~50% each in spot checks), while IDs 494–913 (the integration-only code) are almost entirely survivors.

Triage categories

Spot-checked 10 representative survivors across the range; the pattern divides cleanly:

Class Example mutants Killable? Action
Logger / docstring strings 191 (logger name) Low value Skip
Constant strings flowing into paths 200 (CSV_EXPECTED_HASHES_FILE) Killable Worth a test pin
Value→None crash mutations 207 (parent = None) Killable Trivial unit test
Hidden-column filter strings 264, 388 (startswith("_")) Killable Security boundary, worth pinning
Tuple default-value sentinels 313, 332 (row.get(h, "")) Mostly equivalent Skip
Response-dict key names 431, 600 ("text_diff", "added_row_count") Killable Contract pinning — highest value
JSON formatting params 494 (indent=2 → 3) Equivalent Skip
Integration-path mutations 600+ (most of 494–913) Killable but expensive Defer to integration-test coverage

Rough estimate of GENUINE killable survivors in the unit-tested core (IDs 191–472): ~100–150. Closing them would require ~100–150 new unit tests, ~5–10 lines each. Total work: 1–2 days.

Not addressed in this commit

Closing the 100–150 genuine survivors is in scope of v1.1 item G (test coverage push from 32.4% → 80%). Writing those tests would naturally exercise the same code paths and kill the same mutants. Doing the work twice (once for mutmut-survivors, once for coverage) would be wasteful.

Harness bug discovered + fixed

During this run, a leftover .mutmut-cache (~192 KB) from a prior :rw-mount session was found on the host repo. The populate_scratch() helper's cp -a /repo/. /scratch/ copied this stale cache into the fresh tmpfs, leaking prior-run results (37 phantom wl_audit survivors + 10–12 wl_validation entries) into the cumulative results display. This didn't affect the wl_csv survivor count for the new run (the mutmut progress counter showed 723/723 mutations actually tested), but it polluted the mutmut results aggregate view.

Fix: populate_scratch() now explicitly wipes /scratch/.mutmut-cache and /scratch/mutants after the cp. The host-side leftover at wl_manager/.mutmut-cache was also removed. .gitignore already excluded it from tracking.


2026-05-19 wl_audit.py fresh-run results (item E)

Mutmut run with the corrected selector (tests/unit/test_audit.py tests/unit/test_view_audit_dedup.py) on a fresh :ro-mount + tmpfs container with the cache-leak fix from item D in effect.

Result: 90 mutations applied, 35 survived, 55 killed. Effective mutation score: 61%.

This is the highest kill rate of any module in this v1.1 sweep (wl_validation 92%, wl_csv 24%, wl_audit 61%). The reason: wl_audit.py is small (191 lines) and focused (a single responsibility: post audit events to the wl_audit Splunk index via REST), and the two test files (test_audit.py + test_view_audit_dedup.py, 562 lines combined) provide solid coverage of the event-building logic.

Triage on sampled survivors

Sampled 9 representative survivors. The pattern divides cleanly into 4 classes:

Class Example mutants Killable? Action
Module-level import fallbacks 1 (urllib = None → urllib = "") Equivalent in practice Skip — urllib is always available in the test env
Log / error message text mutations 22, 42, 72, 78 Low value Skip — pinning exact log text adds fragility without security value
HTTP boundary edge cases 68 (200 <= status_code < 300<= 300) Killable but unusual Skip — HTTP 300 (Multiple Choices) is rarely seen and not load-bearing
Event-building + HTTP path 17 (kwarg filter), 55 (auth header template), 88 (None error_msg) Killable, higher value Defer to v1.1 item G

Higher-value survivors (status updated 2026-05-19 during item G)

  • Mutant 17 (line 91): kwarg filter ("app_context", "comment")("XXapp_contextXX", "comment"). RECLASSIFIED AS EQUIVALENT. The original triage claimed this was killable via len(event) == expected_field_count because the unfiltered loop would write app_context to the event again. Closer analysis during item G showed the second write is idempotent: event["app_context"] = kwargs["app_context"] overwrites the line-85 assignment with the same value (both pull from the same kwargs dict), and a dict overwrite does not add a key. No observable behavior changes for any test input. The mutant is structurally indistinguishable from the original.

  • Mutant 55 (line 156): HTTP Authorization header template "Splunk %s""XXSplunk %sXX". CLOSED in test_audit.py test_authorization_header_exact_splunk_prefix (item G, 2026-05-19). The existing test_post_audit_event_sets_headers used `"Splunk " in

    ` (substring), which the mutated `"XXSplunk XX"` passes. The new test asserts exact equality on the full header value. Manually verified: applying the mutation locally produces the assertion failure `expected "Splunk MY_SESSION_KEY_12345", got "XXSplunk MY_SESSION_KEY_12345XX"`.
  • Mutant 88 (line 189): error_msg = str(e)error_msg = None in the generic exception handler. CLOSED in test_audit.py test_generic_exception_returns_non_empty_error_message (item G, 2026-05-19). No prior test exercised the generic except Exception branch (existing tests only triggered HTTPError, URLError, socket.timeout). The new test injects a RuntimeError via mock_urlopen.side_effect and asserts error_msg is not None

    • non-empty + contains the original exception's text. Manually verified: applying the mutation produces assert None is not None failure.

Closing summary (item G first batch, 2026-05-19):

  • Mutant 17 → equivalent (triage corrected — no test possible without destroying program semantics).
  • Mutant 55 → killed by exact-string header assertion.
  • Mutant 88 → killed by mocked-side_effect generic-exception test.
  • Effective wl_audit kill rate after this batch: 57/90 → 63% (was 55/90 = 61%). 33 survivors remain, all in the lower-value classes (log/error message text, equivalent HTTP-status edge cases, integration-only paths).

Recommended improvements

CLOSED (landed in v1.1 prep, 2026-05-19)

  1. Wire the mapping table into scripts/mutmut.sh — DONE. The script now auto-derives TEST_RUNNER_FILES from MUTATE_PATH via the derive_test_files_for function, and hard-fails on unknown modules (rather than silently falling back to wl_validation tests). Run scripts/mutmut.sh mappings to see the table. bin/wl_handler.py is explicitly rejected with a pointer to this doc. Escape hatch: explicit TEST_RUNNER_FILES env var still overrides auto-derivation.

  2. Switch the volume mount to read-only. — DONE. The host repo is now mounted at /repo as :ro; a 512 MiB tmpfs is mounted at /scratch and populated from /repo at container creation. mutmut runs with WORKDIR=/scratch and mutates the tmpfs copy only — the host tree is unreachable from inside the container. Verified: a deliberate write attempt to /repo/bin/wl_validation.py fails with "Read-only file system"; host file sha256 unchanged after a fresh container creation. Source-refresh signal: scripts/mutmut.sh kill then re-run (the tmpfs is repopulated from /repo only on container creation).

  3. Re-run wl_validation with the correct selector — DONE. Result: 10 surviving mutants, all equivalent. See "2026-05-19 fresh-run confirmation" section above. The estimate of "≤8 survivors" was off by 2 — the original triage incorrectly listed mutants 138 and 146 as "killed by existing tests" when they're actually equivalent. Doc corrected.

  4. Run wl_csv.py with the correct selector — DONE. Result: 547 survivors of 723 mutations (24% kill rate). The estimate of "5-20 genuine gaps" was wildly low — the actual genuine-gap count is ~100-150 in the unit-tested core, plus ~400 survivors in integration-only code that won't be killed by unit tests at all. See "2026-05-19 wl_csv.py fresh-run results (item D)" section above. Closing the ~100-150 genuine survivors is deferred to v1.1 item G (test coverage push).

  5. Add bin/wl_audit.py mutation pass. — DONE. Result: 35 surviving mutants of 90 (kill rate 61%). The estimate "37 needs re-validation" was close — fresh run dropped to 35. See "2026-05-19 wl_audit.py fresh-run results (item E)" section below. Most survivors are log/error message text (low value to kill) or HTTP-path mutations that are integration-tested rather than unit-tested. The ~5 higher-value survivors involve event- building and HTTP send logic and are deferred to v1.1 item G.

Open (queued for v1.1 release prep)

(none — all mutmut work items closed as of 2026-05-19)


2026-05-20 hardening pass — wl_csv / wl_limits / wl_trash

Trigger. Pre-release sweep flagged that the baseline kill rates on the three large data modules were all ~50-53% — well below the industry-standard ~75-80% target for KILLABLE mutants (Google "Mutation Testing in Practice", 2020). The user's framing was a reputation risk if clients ran mutmut against the public release and saw ~half of mutations survive.

Baseline (2026-05-19).

Module Mutations Survivors Kill rate
bin/wl_csv.py 723 358 ~50.5%
bin/wl_limits.py 428 203 ~52.6%
bin/wl_trash.py 378 177 ~53.2%
Total 1529 738 ~51.7%

Triage methodology — Five-cluster classification.

Before writing any new tests, we sampled survivors via docker exec wl_manager_mutmut mutmut show <id> to classify them. The samples from bin/wl_csv.py (13 representative survivors out of 358) fell into two broad categories that line up with how production code is shaped:

  • ~30% equivalent / uninteresting. Logger-name string-wraps (logging.getLogger("XXwl_managerXX")), buffer-size off-by-one (fh.read(65536)fh.read(65537)), JSON-indent values that no test asserts on byte-for-byte (json.dump(d, indent=3) vs indent=2), temp-file suffix wraps that survive any test not doing a directory-listing assertion on intermediate files. Chasing these is anti-pattern: the assertion that catches them is brittle (couples tests to implementation detail), and the return on test-author effort is near-zero.

  • ~70% real test gaps. These fall into five clusters with well-defined assertion shapes:

    Cluster What the mutation looks like Test shape that kills it
    A — Registry / checksum integrity file_hash = None or hashes[name] = None Assert returned value is valid hex AND registry stored exactly that value (not None)
    B — Decision-function return shape Tuple arity/element-type changes; pos = None defaults Assert tuple length + element types + canonical sentinels (e.g. admin → (True, 0, -1))
    C — Dict-return contract String wraps on top-level keys ("diff""XXdiffXX") Assert presence of the documented required-key set, on EVERY return path (success, error, OSError, unexpected-exc)
    D — Boundary precision <=<, +-, breakcontinue Test at the EXACT boundary AND one-off either side; include duplicate-match cases where break/continue diverges
    E — Audit-event field shape pos = None, rn = None polluting field-name templates Capture build_audit_event calls + regex-check no key matches *_row_None_*

    Each new test is written to kill a CLUSTER of related mutants in one shot, not a single mutant — the leverage is in pinning the contract, not in matching one mutmut-ID-to-one-test.

Tests added in the 2026-05-20 batch (committed 2358a27 + 5cbb221).

Module New tests Test groups Unit-suite status
bin/wl_csv.py 15 A-E (all five clusters) 79/79 pass
bin/wl_limits.py 16 A-E (Group E adapted to increment-delta) 92/92 pass
bin/wl_trash.py 10 A-C (no audit-emission in trash → no Group E) 45/45 pass

Post-batch mutmut measurements.

Module Baseline kill rate Post-batch kill rate Delta Notes
bin/wl_csv.py 50.5% (358/723 survivors) 53.4% (336/723 survivors + 1 timed out) +2.9% Re-ran 2026-05-20 with clean tmpfs/cache. 22 mutations newly killed.
bin/wl_limits.py 52.6% (203/428 survivors) NOT YET RE-RUN Container time-constrained; queued for v1.1.
bin/wl_trash.py 53.2% (177/378 survivors) NOT YET RE-RUN Container time-constrained; queued for v1.1.

Honest reading of the wl_csv result.

The +2.9% delta is much smaller than the ~70% kill rate that the five-cluster methodology TARGETS. Three reasons surfaced when the 22 newly-killed mutations were compared against survivor IDs:

  1. Many "killable-looking" mutations are functionally equivalent. bin/wl_csv.py calls startswith("_") in THREE adjacent lines (visible_headers, old_vis_set, new_vis_set). Mutmut generated one mutation per line. The Group B test test_compute_diff_excludes_underscore_prefixed_metadata_columns kills the mutations on lines 537-538 (old_vis_set/new_vis_set — they affect common_headers and surface in edited_count), but leaves the mutation on line 534 (visible_headers) alive because that variable is only used for the cosmetic text_diff output, not for actual row comparison. Reviewers reading the mutmut output as a binary pass/fail would call this a survivor; readers who trace the call graph would call it equivalent.

  2. Many mutations are in code paths only exercised by integration tests. save_csv_pipeline and create_csv_pipeline orchestrate versioning, audit emission, FIM registry updates, and atomic writes. A large fraction of their internal branches are reached only when the actual Splunk container is present. Unit tests mock wl_versions.snapshot_version and wl_audit.post_audit_event to side-step those dependencies — which means any mutation INSIDE the calls that hit those mocked functions is unreachable from a unit test.

  3. The cluster-leverage assumption was optimistic. The five-cluster methodology aims for one test killing N related mutations. In practice, mutmut's mutation generator produces variants the tests don't pin in the way I projected (e.g. "diff""XXdiffXX" IS killed, but "added""XXaddedXX" is killed too only because Group B explicitly enumerated that key, while "removed_count""XXremoved_countXX" survives because the Group D REQUIRED_KEYS set in test_csv.py only spelled out the top-level envelope, not every nested key).

What this means for the rc1 release framing.

The user's original concern was reputation risk if customers ran mutmut against the published source and saw ~50% survivors. The honest position to take is:

  • 53.4% kill rate on bin/wl_csv.py (a large data-pipeline file with significant integration-only code) is consistent with mutmut-on-Python projects of this shape.
  • The five-cluster triage documented above tells reviewers what the survivor categories are, so a customer looking at the numbers sees the discipline, not opaque survivors.
  • The remaining work (further test additions to push toward 75%) is scoped as a v1.1 item, NOT a v1.0.0-rc1 blocker.

This is more honest than the earlier framing of this section (now corrected) which conflated the methodology TARGET (75% on killable) with the measured RESULT (53.4% on all).

Open items deferred to v1.1.

  • Re-run mutmut on bin/wl_limits.py + bin/wl_trash.py against the post-batch test files (the 16+10 new tests) so we have a three-module measured set.
  • Triage the 336 wl_csv survivors into "killable / equivalent / integration-only" buckets and write tests for the killable subset.
  • Consider whether a coverage-aware mutator (mutmut's --use-coverage) would skip the integration-only paths and give a more interpretable kill rate.

Lessons captured to memory

These have been added to project memory (MEMORY.md) so they don't have to be rediscovered:

  • feedback_mutmut_host_mount_hazard.md:ro mount or scratch-dir-only writes; verify git diff bin/ is clean before staging.
  • feedback_mutmut_test_selector_match.md — pair MUTATE_PATH and TEST_RUNNER_FILES every run; default selector is wl_validation only.

(Add these files manually when promoting this lesson into project memory — the policy is per-session capture, not auto-extraction.)