# The fixture library

`test_data/music_library_test/` is **generated, not committed**:
`make testdata` (~1 s) builds 31 deterministic tracks across MP3, FLAC,
Ogg Vorbis and WAV. `make testdata-force` rebuilds unconditionally,
`make testdata-clean` deletes it. `make test` and `make sandbox-seed`
depend on it, so it is rarely run by hand.

Tests that need it fetch it through `internal/testfixtures` and skip
themselves when it has not been generated.

## Select by case, never by path

```go
m := testfixtures.Load(t)
paths := m.Case(t, testfixtures.CaseCoverDedup)
track := m.Track(t, rel)
```

Cases: `cover-dedup`, `multi-disc`, `various-artists`, `flac-album`,
`ogg-album`, `wav-tracks`, `partial-tags`, `unicode`, `duplicates`,
`edge-lengths`, `broken`.

Two invariants worth not breaking:

- **The clean library is exactly 31 tracks**, because `sandbox-seed`
  verifies the scan against that count. Deliberately malformed files
  live in a *sibling* root, `test_data/music_library_broken/`
  (`m.BrokenPath()`), so the scanner never sees them.
- **Tags are written by `backend/tagwriter`, not by ffmpeg** (which
  encodes with `-map_metadata -1`). Fixture and reader therefore cannot
  drift into agreeing with each other and disagreeing with reality.

The manifest (`test_data/music_library_test.manifest.json`, outside the
scanned root) hashes the *spec* — paths, formats, durations, tags,
cover identity — not the bytes, because ffmpeg stamps encoder version
strings and identical specs produce different bytes on different builds.

## In e2e specs

- **Every fixture except one is 2–6 seconds.** A spec that starts
  playback and then clicks pause races the track ending and fails
  against a correct UI. Use `LONG_TRACK` (90 s, `edge-lengths`) exported
  from `e2e/support/fixtures.ts`.
- **WAV tracks scan in untitled.** `backend/tagwriter` writes WAV tags
  into a RIFF `id3 ` chunk and `dhowden/tag` has no RIFF parser, so
  there is no "Field Recordings" artist in the Artists view. This is a
  known open bug pinned by `TestWAVTagsAreNotReadableYet`; do not
  "fix" a spec by asserting the broken behaviour elsewhere.

## Seeds

```bash
make sandbox-seed NAME=default   # build (boots a fresh YJ_HOME and drives the app)
make sandbox-seeds               # list
make dev-headless SEED=default   # restore into a run
```

A seed is a tarred `YJ_HOME` produced by *running the app*: fresh home →
real `AddLibrary` binding → poll until the real scan reports the
manifest's track count → SIGTERM so shutdown hooks persist state → tar.
Never hand-write one. Seeding points `YJ_CORE_INDEX_URL` at a dead
address on purpose, so no seed depends on what the explore artifact
server happened to be serving.

Rebuild a seed after any schema change. Nothing migrates a restored
database: `applySchema` is `CREATE TABLE IF NOT EXISTS`, so an old seed
keeps its old columns, the app starts, and the first query dies on
`no such column`.

**Restoring the seed does not disable the artifact fetch — only
*building* it does.** `dev-headless` leaves `YJ_CORE_INDEX_URL` alone,
so on a developer machine the restored app immediately downloads and
imports the real ~1.1M-row catalog, through the one writer connection,
while whatever you started it for is running. A full `make e2e` against
that reported **14 failures** that were all contention; the same suite
against the same seed with

```bash
YJ_CORE_INDEX_URL='http://127.0.0.1:1/none.tar.zst' make dev-headless SEED=default
```

is the configuration CI runs (`ci.yml` sets exactly that address) and is
what to use before believing a failure. The tell is in `.dev/app.log` —
an import logging progress — and in how the failures look: timeouts
spread across unrelated specs rather than one surface being wrong.
