Skip to content

test(backend): Phase 1 golden-snapshot regression gate - #96

Merged
slittycode merged 5 commits into
mainfrom
claude/asa-architecture-review-DTPvY
May 21, 2026
Merged

test(backend): Phase 1 golden-snapshot regression gate#96
slittycode merged 5 commits into
mainfrom
claude/asa-architecture-review-DTPvY

Conversation

@slittycode

Copy link
Copy Markdown
Owner

Summary

Adds a golden-snapshot regression gate over default-mode analyze.py output — a safety net for upcoming analyze.py/Essentia changes. It runs the analyzer on a fixed, deterministic procedural fixture and fails when the emitted JSON drifts from a committed golden, naming the exact drifted path. Catches drift in any field (not just hand-curated ones), which is what you want when a change's blast radius is unknown.

Test-only change plus one committed fixture JSON. No change to analyze.py, the JSON schema, src/types.ts, or requirements.txt.

What's here

  • apps/backend/tests/test_phase1_golden.py — runs analyze.py (default mode) on an inlined deterministic A-minor fixture and diffs the output against the golden with a self-contained recursive comparator:
    • tolerant numeric compare (abs(a-g) <= max(ABS_TOL, REL_TOL*|g|)), NaN/Inf-aware, bool checked before int (so True vs 1 bites), key-set drift (added/removed), list-length drift, type mismatch, and a MAX_REPORTED cap so a large-array drift can't dump thousands of lines.
    • gate-bites meta-tests (committed, pure-Python, no analyze run) prove the comparator catches drift and doesn't over-bite — including in-memory NaN/Inf/bool, the exclusion mechanism, and cap/truncation.
    • standalone regen helper: UPDATE_PHASE1_GOLDEN=1 ./venv/bin/python -m unittest tests.test_phase1_golden.
  • apps/backend/tests/fixtures/golden/phase1_default.json — the committed golden (~50 KB, 65 top-level keys), written sort_keys=True, indent=2.

Determinism

Output was confirmed byte-identical across 5 repeated local runs, so the tolerance is purely cross-environment float-jitter insurance (e.g. multi-thread reduction order on the CI runner), not run-to-run noise. The default path has no RNG/ML/timestamps. The one non-deterministic field — melodyDetail.midiFile, a transient per-run artifact path — is the sole exclusion, stored in the golden as a self-documenting placeholder so the key-set check still holds.

Test plan

  • analyze.py on the fixture ×5 → byte-identical (determinism proof)
  • ./venv/bin/python -m unittest tests.test_phase1_golden → green (main gate + 10 meta-tests)
  • Manual numeric nudge of a golden value → main gate fails with a clear per-path diff (bpm: 140.4 != 135.4); golden restored byte-identical
  • Real drift caught end-to-end (the melodyDetail.midiFile path drift surfaced before it was excluded)
  • python -m unittest discover -s tests703 tests OK (1 skip = the regen helper), no regressions
  • First CI run confirms checkout-vs-runner float parity (regenerate or widen tolerance if it exceeds the band — documented in the module)

https://claude.ai/code/session_018hYXgv4LK9Hm1BNGWgMZf8


Generated by Claude Code

claude added 2 commits May 20, 2026 04:00
ASA is server-side Python on native Essentia and already computes a
superset of the crate, so a same-class cross-check oracle would be
decorative (cross-impl agreement != correctness; the crate's R128 is
from-scratch, no ebur128 dep). tonnetz, the only gap, has no consumer
and is dropped -- Harmonia can derive it from the chroma ASA already
exports. Python-native validation is tracked separately, trigger-gated.
Supersedes Plan 4's "cheap and high-value" oracle recommendation in
incorporations/forking-plans-2026-05-14.md.

https://claude.ai/code/session_018hYXgv4LK9Hm1BNGWgMZf8
Snapshot default-mode analyze.py output against a committed golden so any
unintended drift in a measured field fails CI -- a safety net for upcoming
analyze.py/Essentia changes.

- tests/test_phase1_golden.py runs analyze.py on a deterministic procedural
  fixture and diffs the JSON against the golden with a recursive tolerant
  comparator (NaN/Inf-aware, bool-before-int, key-set + list-length drift,
  capped report). Output was byte-identical across 5 runs; the tolerance is
  cross-environment float-jitter insurance, not run-to-run noise.
- Committed gate-bites meta-tests prove the comparator catches drift and does
  not over-bite (incl. in-memory NaN/Inf/bool and cap cases).
- melodyDetail.midiFile (a transient per-run artifact path) is the sole
  excluded field, stored in the golden as a placeholder.
- Regenerate after an intentional change with UPDATE_PHASE1_GOLDEN=1.

No analyze.py / schema / requirements.txt change.

https://claude.ai/code/session_018hYXgv4LK9Hm1BNGWgMZf8

@slittycode slittycode left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE

(GitHub won't let you approve your own PR, so this is a COMMENT — verdict is approve.)

Summary

Test-only PR adding a golden-snapshot regression gate over default-mode analyze.py output, plus a decision doc. No production code touched. The comparator implementation is sound — bool-before-int ordering is correct (critical Python gotcha), NaN/Inf are handled, key-set diff catches additions and removals, MAX_REPORTED cap prevents multi-thousand-line noise on large-array drift. Ten meta-tests confirm the comparator bites correctly and doesn't over-bite. Frontend ✅ and Loudness WASM ✅ are green; Backend CI was in progress at time of review.

Findings

Should fix (pre-existing, not introduced here):

The committed golden captures 65 top-level keys from full-mode analyze.py. EXPECTED_TOP_LEVEL_KEYS in test_analyze.py:26 has 59. Six full-mode fields are absent from that set: keyProfile, lufsMomentaryMax, lufsShortTermMax, pitchDetail, tuningCents, tuningFrequency. The fast-mode test at test_analyze.py:342 claims "Fast mode must emit the same top-level key set as full mode" — but full mode now emits 65 keys, not 59. The test passes because fast mode also skips those six fields, so set(payload.keys()) == EXPECTED_TOP_LEVEL_KEYS holds, but the stated invariant is false. The golden makes this discrepancy undeniable for the first time. Either add the six fields to EXPECTED_TOP_LEVEL_KEYS (and decide whether fast mode should null them or omit them), or reword the test comment to "fast mode emits a stable subset of full-mode keys" — whichever matches the intended contract. Not blocking this PR.

Worth considering (tolerance):

ABS_TOL=REL_TOL=1e-3 is reasonable. On LUFS around -20 dB it gives ~0.02 dB slack; on spectral centroid values in the hundreds it gives ~0.1%. The CI float-parity risk is honestly flagged in the PR description with a documented remediation path.

Test results

Local: 703 tests OK, 1 skip (regen helper, by design). CI: Frontend ✅, Loudness WASM ✅, Backend ⏳ at review time.

Phase boundary check

Clean. Three new files only: apps/backend/tests/test_phase1_golden.py, apps/backend/tests/fixtures/golden/phase1_default.json, docs/history/audio-analyzer-rs-decision-2026-05-20.md. No Phase 2/3 code touched, no Phase 1 production code changed, no schema mutations.


Generated by Claude Code

claude added 3 commits May 21, 2026 02:12
Backend CI failed on the new gate. analyze.py rounds every output float
(1-4 decimals); a different BLAS/thread order on the runner can push a raw
value across a rounding boundary, flipping the last digit by a whole step
(e.g. 135.4 -> 135.5). The 1e-3 tolerance was far tighter than a 0.1 step,
so any one of ~1633 boundary-prone leaves was enough to fail in CI while
passing locally (same environment, identical rounding).

Widen the numeric comparison to also absorb one rounding step at each
field's own decimal precision, plus a 1e-9 float-safety epsilon for clean
decimal values at the step boundary. Stays portable across environments
while still biting any change larger than a field's reported resolution and
every string/enum/key/list-length change. Verified by a cross-environment
simulation: +/-1 step on all 2154 numeric leaves -> 0 mismatches; a
multi-step change still bites. Adds a meta-test pinning the behavior.

https://claude.ai/code/session_018hYXgv4LK9Hm1BNGWgMZf8
The fast-mode test's docstring/message said fast emits "the same top-level
key set as full mode", but full mode emits 6 more detail-only fields
(keyProfile, tuningFrequency, tuningCents, lufsMomentaryMax,
lufsShortTermMax, pitchDetail). The assertion itself (== EXPECTED_TOP_LEVEL_KEYS,
the shared contract) is correct; only the wording was misleading. Surfaced
by the new Phase 1 golden snapshot in this PR's review. No behavior change.

https://claude.ai/code/session_018hYXgv4LK9Hm1BNGWgMZf8
The full-output golden snapshot was not reproducible on CI. The fixture WAV
is bit-identical across machines, but the runner's CPU/BLAS/FFT order yields
last-bit-different floats, and many fine-grained fields (per-beat accent
patterns, per-frame spectral series, MFCC/chroma vectors, onset counts) sit
on discrete decision boundaries that flip wholesale between machines. No
numeric tolerance can absorb that, so the gate failed on CI while passing
locally.

Redesign (per maintainer decision) to two portable assertions:
- contract/structure: exact top-level key set + each field's type category
  (catches added/removed fields, type changes, a detector returning null);
- curated core values: environment-stable, high-value measurements (BPM,
  key, loudness, true-peak, crest, 7 spectral-balance bands, stereo)
  compared with the rounding-step tolerance.

The curated set was chosen with a local fragility probe (tiny PCM noise):
only fields that never flip under perturbation are compared by value. The
new gate was validated against noise up to 2 LSB across 9 runs -> 0
mismatches, while real changes still bite (meta-tests). Golden regenerated
to the new {topLevelKeys, topLevelTypes, coreValues} form. Full backend
suite green (702 ok, 1 skip).

https://claude.ai/code/session_018hYXgv4LK9Hm1BNGWgMZf8
@slittycode
slittycode merged commit 8ed48dc into main May 21, 2026
3 checks passed
@slittycode
slittycode deleted the claude/asa-architecture-review-DTPvY branch May 21, 2026 03:45
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.

2 participants