description: "Audit test-suite health — coverage gaps, weak assertions, flaky/stale tests" argument-hint: "[--limit <N>]"
Regression / Test-Hygiene Audit
Audit the test suite itself: where coverage is missing on code that can break, where tests assert too little to catch a regression, and where tests are stale, flaky, or testing the wrong thing. The output is a prioritized list of tests to add or strengthen — the safety net for every other audit's fixes.
Shared protocol: .claude/commands/_audit-common.md. Severity:
.claude/commands/_audit-severity.md — a coverage gap on working code is LOW, but a gap on
a path that has had bugs or that emits NES register data is MEDIUM (the blast radius is a
silently-broken ROM).
A recent sprint closed ~100 issues, several against the test suite itself (REG-01 through
REG-11 below, from AUDIT_REGRESSION_2026-06-28.md / -06-29.md). All are now fixed —
don't re-report them — but re-verify status on every run: a fix landing doesn't guarantee it
stays landed, and the skip-masking that REG-10/REG-11 removed (CC65-gated suites reporting
real compile failures as "CC65 may not be installed" skips) is exactly the failure shape
this audit exists to catch.
Parameters
--limit <N>— cap findings (report the top N by risk). Default: no cap.
Step 1: Inventory
ls tests/ # 52 test_*.py + conftest.py
python -m pytest -q --collect-only | tail -5
python -m pytest --cov=. --cov-report=term-missing -q # if pytest-cov present
# The CC65-gated suites now use a real gate: `conftest.py:pytest_runtest_setup`
# skips @requires_cc65 only when ca65/ld65 are genuinely absent (via `shutil.which`),
# so with the toolchain present they compile real ROMs and assert unconditionally.
# Run them directly to confirm they PASS (not skip), don't just trust a green summary:
python -m pytest tests/test_ca65_export.py::TestCA65CompilationIntegration -v
python -m pytest tests/test_rom_validation_integration.py -v
Cross-reference tests/ against _audit-common.md § Project Layout: every subsystem
should have at least one test module. Map test file → subsystem it covers.
Step 2: Coverage-Gap Dimensions
Dimension 1: Untested subsystems / modules
A source module with no corresponding test, or only an import-smoke test. Weight by risk:
the NES-register path (nes/emulator_core.py, nes/pitch_table.py,
nes/envelope_processor.py), the exporters, the mappers, and the compiler are the
high-blast-radius gaps. A module with --cov < ~50% on a pipeline path is a finding.
Four modules previously sat at 36-71% coverage with no dedicated test module, plus one
front-end (arranger/) with zero test references at all — all five closed, keep verifying
the coverage didn't regress rather than re-flagging from scratch:
tracker/track_mapper.py— the legacy multi-track branch ofassign_tracks_to_nes_channels(melody→pulse1, harmony→pulse2 w/ arpeggio, bass→triangle, drum-named→noise) was wholly unverified (REG-08/#48). Fixed bytests/test_track_mapper.py(synthetic 4-track + real-MIDI regression case); currently 71% cover.compiler/cc65_wrapper.py— missing-tool detection (shutil.which→None), nonzero version-probe exit, stderr surfacing, and the fullbuild()pipeline had no direct coverage (REG-09/#49). Fixed bytests/test_cc65_wrapper.py; currently 86%.tracker/pattern_detector_parallel.py— theProcessPoolExecutormap/merge, the fallback toEnhancedPatternDetector, and worker-count invariance were untested (REG-06/#46). Fixed bytests/test_pattern_detector_parallel.py; currently 78% (up from 36%).mappers/factory.py/mappers/base.py—MapperFactoryauto-select and each mapper's header/linker/capacity output were only exercised indirectly viatest_ca65_export.py(REG-07/#47). Fixed bytests/test_mappers.py; factory.py now 74%, base.py 59% (residual gap: base.py's abstract-method stubs and a couple of capacity edge cases — worth a look, but no longer a blank gap).arranger/(role_analyzer.py,voice_allocator.py,gm_instruments.py,pipeline_integration.py) — the entire--arrangerfront-end had zero test references (REG-04/#44). Fixed bytests/test_arranger.py(role tagging, arpeggiation cadence, triangle no-duty invariant, frames-contract parity withprocess_all_tracks), plustests/test_arranger_drum_detection.py,tests/test_arranger_frame_contract.py, andtests/test_voice_allocator.py(DPCM routing, noise-period routing).--arrangernow also gets a real ROM-compile round trip (tests/test_e2e_pipeline.py::TestEndToEndPipeline::test_full_pipeline_arranger_mode, #129) — see Dimension 3.
Dimension 2: Weak assertions
Tests that run code but assert almost nothing — assert result is not None, assert len(x) > 0 where the values matter. The NES-output tests especially must assert exact
register bytes / timer values, not just shape. Flag tests that would pass even if the music
came out wrong.
Closed example, keep as the bar to hold other exporter tests to: several
test_ca65_export.py / test_midi_parser_integration.py tests asserted only that a
section/substring was present (header magic, "PATTERNS" in content) — would pass even
with every note/timer/volume byte wrong (REG-05/#45). Fixed by a golden-bytes test class
that parses test_midi/simple_loop.mid through the real pipeline and asserts the exact
pulse1_sequence macro-bytecode stream and the first ntsc_period_low table bytes. Still
worth a periodic grep for assertIn('PATTERNS' / bare-existence checks elsewhere in
tests/test_ca65_export.py, tests/test_exporter_integration.py, and
tests/test_famistudio_export.py — the golden-bytes fix only covers the one path it
targeted.
Dimension 3: Round-trip / end-to-end gaps
The properties that unit tests miss: parse→...→ROM produces a valid ROM
(tests/test_e2e_pipeline.py is the anchor); pattern compress→decompress equals the
original; a generated .nes passes debug/check_rom.py. Missing round-trip coverage on
compression is MEDIUM (it guards a CRITICAL failure mode).
- Fixed: the 7-method
TestCA65CompilationIntegrationclass intests/test_ca65_export.pywas RED against realca65/ld65— the shippednes/audio_engine.asmhad a relative branch (bcc @is_note) whose target was 130 bytes away, outside the 6502 ±127 range (REG-01/#39). Fixed by restructuring the dispatch (nes/audio_engine.asm:204-213,bcs+jmpinstead of the out-of-rangebcc) — confirmed: all 9 tests inTestCA65CompilationIntegrationpass with the toolchain present. Re-verify this stays green; a relative-branch regression here is silent until an assembler is actually invoked. - Fixed (REG-10/#128):
tests/test_rom_validation_integration.pyis the designated "compile a real ROM and validate its bytes" gate. It previously had 5 of 9 tests SKIP even withca65/ld65present, because the hand-writtenmusic.asmfixture definedinit_music:/update_music:as bare labels with no.export/.global, sold65failed withUnresolved external 'init_music'and anexcept Exception → pytest.skipmasked it as "CC65 may not be installed". Fixed: the fixture now lives intests/conftest.py'sminimal_music_asmand emits.export init_music, update_music(matching the real exporter); the compile tests are gated with@pytest.mark.requires_cc65(a realshutil.whichgate intests/conftest.py:pytest_runtest_setup) and each assertscompile_rom(...)unconditionally, andtest_shared_music_asm_fixture_is_linkablepins the.export. Re-verify these PASS (not skip) with the toolchain present — a re-introducedexcept → skiphere is exactly what this dimension catches. - Fixed (REG-15/#299): the two compile-failure (negative-path) tests in the same file
(
test_compilation_with_invalid_assembly,test_compilation_failure_without_rom_output) kept theexcept → pytest.skip("CC65 not installed")masking after REG-10 removed it from the compile-success tests, compounded by pass-either-way bodies. Now both are@requires_cc65and assertcompile_rom(...) is Falseplus no partial ROM.compile_rom()catches all failures and returns False (never raises), so any survivingexcept → skipin this file is dead masking — re-flag it. - Fixed (REG-11/#129): the anchor
test_full_pipeline_midi_to_validated_rom(tests/test_e2e_pipeline.py) can now fail on a broken pipeline. The oldtry/except → pytest.skip,args.skip_validation = True, andif rom_path.exists():guards are gone: the shared_run_pipelinehelper setsskip_validation = False(validation ON) and_assert_valid_romasserts unconditionally, all under a@pytest.mark.requires_cc65gate. It also now exercises--arranger(test_full_pipeline_arranger_mode) and--no-patterns(test_full_pipeline_no_patterns_direct_export) through a realca65/ld65compile — closing the Dimension 1 arranger note above. Re-verify these assertions stay unconditional.
Dimension 4: Stale / wrong-target tests
Tests referencing renamed symbols, skipped/xfail tests with no tracking issue, tests that
assert old (now-incorrect) behavior, or tests pinned to checked-in artifact files that have
since changed. Confirm against current code.
Two closed examples worth knowing so they aren't re-flagged:
- Four test classes in
test_audio_fixes.pywere@unittest.skip'd with "Obsolete: Assembly generation changed to MMC3 Macro Bytecode" and no tracking issue (REG-03/#42). All four actually targetedexport_direct_frames(the direct/table-based export path, never removed) and still matched every assertion — the skip reason was stale, not the tests. Fixed: un-skipped; the one genuine failure (test_triangle_is_octave_above_pulse_period, a harmless pulse/16 vs triangle/32 integer-quantization drift) had its delta loosened from 0.05 to 0.08 to cover the observed max (~0.071) without masking a real regression. verify_ca65_assembly()intests/test_midi_parser_integration.pyand the e2e assembly check asserted the old standalone.segment "HEADER"format the exporter no longer emits in macro-bytecode mode (REG-02/#40). Fixed (commit184d3c9):verify_ca65_assembly(tests/test_midi_parser_integration.py:41-73) now branches on which export mode produced the content ("MMC3 Macro Bytecode"marker → asserts.segment "CODE_8000"/"BANK_00"/pulse1_sequence:/etc.;"Pattern Compressed"or standalone direct-frame → asserts the legacyHEADER/CODE/VECTORSsections, which are still correct for those modes). Notetests/test_e2e_pipeline.py:235's.segment "HEADER"assertion is not stale — it checksmain.asmfromNESProjectBuilder, a file the project builder (not the CA65 exporter) still always emits with a HEADER segment; don't conflate the two files when re-auditing this.
Recently-fixed stale-artifact case worth knowing: the hand-written music.asm fixture
behind the ROM-validation tests (REG-10 above) had drifted from what the real exporter
emits (missing the .export init_music, update_music directives) — the same failure
shape this dimension exists to catch. Now fixed by moving it to tests/conftest.py's
minimal_music_asm with the .export directives; don't re-flag it.
Dimension 5: Determinism / flakiness
Tests depending on multiprocessing scheduling (ParallelPatternDetector), dict/set
ordering, wall-clock timing, or filesystem temp paths without isolation. These pass locally
and fail in CI. Flag any test whose outcome can vary run-to-run.
Closed example: ParallelPatternDetector's determinism guarantee was not actually held —
_select_best_patterns sorted candidates by score alone, so equal-score candidates
resolved by ProcessPoolExecutor.as_completed arrival order, making which non-overlapping
pattern won depend on host core count (REG-06/#46, same fix as Dimension 1). Fixed: a
deterministic (start, length) tie-break was added, and
tests/test_pattern_detector_parallel.py now pins identical patterns/references/
compression_ratio across max_workers=1/2/4 plus the pool-failure fallback. Re-run this
test file specifically after any change to tracker/pattern_detector_parallel.py's
scoring/selection — it's the one place a reintroduced non-determinism would surface.
Dimension 6: Fixture & isolation hygiene
Tests that write into the repo root instead of a temp dir, leak state between tests, or
depend on a prior test having run. Check tests/conftest.py for shared fixtures (temp_dir,
project_dir, minimal_midi_file, minimal_music_asm, valid_rom_file, and synthetic
ROM-corruption fixtures) and whether they're used consistently — most compiler/ROM tests do
use them. The music.asm fixture that had drifted from reality (missing .export
directives) is now the shared minimal_music_asm fixture in tests/conftest.py, updated
to emit .export init_music, update_music (see Dimension 3/4, REG-10) — re-verify the
shared fixture is used consistently rather than re-hand-rolled per test.
Step 3: For each gap, specify the test
Don't just say "needs a test" — name the module, the property to assert, and the concrete
input (a test_midi/ sample or a crafted JSON). A finding the /fix-issue pipeline can
act on directly.
Output
Write to: docs/audits/AUDIT_REGRESSION_<TODAY>.md (YYYY-MM-DD). Structure:
- Coverage map — subsystem → test module(s) → rough coverage / gap.
- Findings — base format +
Dimension; each names the test to add/strengthen. - Prioritized backlog — the top tests to write first, by blast radius.
Then suggest:
/audit-publish docs/audits/AUDIT_REGRESSION_<TODAY>.md
TDD Red-Green-Refactor
Testing
Skill that guides Claude through the complete TDD cycle.
Web Accessibility Audit
Testing
Performs a comprehensive web accessibility audit following WCAG standards.
UAT Test Case Generator
Testing
Generates structured and comprehensive user acceptance test cases.