test: fix 4 integration-suite root causes surfaced by CI (C+A+B+F)

Latent failures exposed now that integration tests run in CI (#339).
All test/test-harness only — no production code changes.

A (dbtest.ResetDB): also truncate cover_art_provider_settings +
cover_art_sources_meta. The monotonic source-version counter (seeded 1
by 0018) accumulated across internal/coverart tests → CurrentVersion=4
want 1, key-only-bump assertions, enricher source skips. SettingsService
re-seeds both at boot, so a truncated start is the correct fresh state.

B (playlists system_test.seedPlayEvent): inserted play_events with a
random session_id → play_events_session_id_fkey violation (7 tests).
Create the parent play_sessions row in the same statement (CTE).

C (similarity worker_integration_test.newTestWorker): built the client
with only BaseURL. Post-4fca0e6 similarity hits the Labs API via
LabsBaseURL, so an unset LabsBaseURL fell through to the real labs.api
and the stub never ran → 0 similarity rows. Set LabsBaseURL to the stub.

F (library scanner_test): NOT a flake — deterministic. Synthetic
ID3-only MP3s have no decodable duration; the scanner intentionally
won't skip duration_ms=0 rows (retries duration backfill), so every
re-scan reported Updated not Skipped. Seed duration_ms>0 before the
second scan so the mtime-based incremental-skip path under test is
actually exercised. Production scanner behavior unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 13:32:42 -04:00
parent 5a048cbea2
commit d212621eaa
4 changed files with 32 additions and 3 deletions
+8
View File
@@ -55,6 +55,14 @@ var dataTables = []string{
"playlist_tracks",
"playlists",
"library_changes", // M7 #357 — must reset to keep cursor isolated per test
// Cover-art provider settings + the monotonic source-version
// counter (cover_art_sources_meta, seeded at 1 by migration 0018).
// Not resetting these let the version accumulate across tests in
// internal/coverart (CurrentVersion=4 want 1, key-only-bump
// assertions, enricher source skips). SettingsService re-seeds both
// at boot, so a truncated start is the correct fresh state.
"cover_art_provider_settings",
"cover_art_sources_meta",
"tracks",
"albums",
"artists",
+11
View File
@@ -103,6 +103,17 @@ func TestScanner_Integration(t *testing.T) {
t.Errorf("artist sort = [%q, %q], want [Artist X, Artist Y]", artists[0].SortName, artists[1].SortName)
}
// The synthetic MP3s carry only an ID3 tag — no decodable audio —
// so ffprobe yields duration 0. The scanner deliberately refuses to
// skip zero-duration rows (it re-runs them so a later scan can
// backfill duration once probing works), which is orthogonal to the
// mtime-based incremental-skip this test covers. Simulate a
// normally-probed library so the skip path is actually exercised;
// updated_at is left untouched (still ≥ file mtime).
if _, err := pool.Exec(ctx, "UPDATE tracks SET duration_ms = 1000 WHERE duration_ms = 0"); err != nil {
t.Fatalf("seed durations: %v", err)
}
stats2, err := scanner.Scan(ctx, nil)
if err != nil {
t.Fatalf("second scan: %v", err)
+8 -1
View File
@@ -24,9 +24,16 @@ func discardLogger() *slog.Logger {
// `InsertPlayEvent` doesn't accept was_skipped (that's set by an UPDATE).
func seedPlayEvent(t *testing.T, pool *pgxpool.Pool, userID, trackID pgtype.UUID, startedAt time.Time, wasSkipped bool) {
t.Helper()
// play_events.session_id has a FK to play_sessions; create a parent
// session in the same statement (a random UUID violates the FK).
_, err := pool.Exec(context.Background(), `
WITH s AS (
INSERT INTO play_sessions (user_id, started_at, last_event_at)
VALUES ($1, $3, $3)
RETURNING id
)
INSERT INTO play_events (user_id, track_id, session_id, started_at, was_skipped)
VALUES ($1, $2, gen_random_uuid(), $3, $4)
SELECT $1, $2, s.id, $3, $4 FROM s
`, userID, trackID, startedAt, wasSkipped)
if err != nil {
t.Fatalf("seed play_event: %v", err)
@@ -114,8 +114,11 @@ func markPlayed(t *testing.T, f fixture, trackID pgtype.UUID) {
func newTestWorker(f fixture, lbBaseURL string) *Worker {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
return &Worker{
pool: f.pool,
client: &listenbrainz.Client{BaseURL: lbBaseURL, HTTP: http.DefaultClient},
pool: f.pool,
// LabsBaseURL must point at the stub too: similarity calls hit
// the Labs API (commit 4fca0e6), so an unset LabsBaseURL would
// fall through to the real labs.api and the stub never runs.
client: &listenbrainz.Client{BaseURL: lbBaseURL, LabsBaseURL: lbBaseURL, HTTP: http.DefaultClient},
logger: logger,
tick: 1 * time.Hour,
batch: 5,