Files
yellowjacket/backend/library/leak_test.go
T
yonluandClaude Sonnet 5 e190fd75b9
Build & publish Arch package / arch-package (push) Successful in 2m12s
Search index maintenance / maintain-index (push) Successful in 2h22m28s
feat: data lifecycle rewrite, download clients, wanted list, and central catalog index
Ships the fresh-start schema cleanup: rebuilt explore catalog index
pipeline (dump import, artifact fetch/build, incremental listen-count
refresh), a new download subsystem (Lidarr/Prowlarr/qBittorrent/SABnzbd/
slskd/yt-dlp providers, staging, reconciliation, wanted list), and the
supporting schema/query/store changes across backend and frontend.

Also includes two smaller follow-ups: bump the central index's
rebuild-after cadence from 90 to 180 days, and remove the Explore
"library only" online/offline toggle entirely (frontend-only, no
backend counterpart) rather than carry unused UI/state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS
2026-08-06 17:12:01 -04:00

130 lines
3.7 KiB
Go

package library
import (
"testing"
"yellowjacket/backend/datamap"
)
// staleTolerated lists tables that deliberately keep rows after the data
// they describe is gone. Each needs a reason: the point of this list is
// that tolerating a leak becomes a decision somebody wrote down, not an
// oversight nobody noticed.
var staleTolerated = map[string]string{
"file_types": "static lookup rows seeded from code, not user data",
"search_index": "contentless FTS5 cannot delete individual rows; " +
"stale entries are filtered by joining track_metadata and are " +
"cleared by a full rescan",
"lyrics_index": "contentless FTS5, same constraint as search_index",
}
// Removing the only library must leave no owned or derived rows behind.
//
// The table list comes from the datamap catalog rather than being
// hardcoded, so a newly added table is covered by this test the moment it
// is catalogued — which is the mechanism that would have caught
// tagging_items blocking removal, and the cover art variants leaking.
func TestRemoveLibraryLeavesNoOwnedOrDerivedRows(t *testing.T) {
t.Parallel()
lib, _ := setupTestLibrary(t)
library := seedRemovableLibrary(t, lib, "/nonexistent/cover.jpg")
if _, err := lib.RemoveLibrary(library.ID); err != nil {
t.Fatalf("RemoveLibrary: %v", err)
}
for _, entry := range datamap.Tables() {
if entry.Kind != datamap.Owned && entry.Kind != datamap.Derived {
continue
}
// FTS5 virtual tables do not answer COUNT(*) meaningfully.
if entry.FTS {
continue
}
if reason, exempt := staleTolerated[entry.Name]; exempt {
t.Logf("skipping %s: %s", entry.Name, reason)
continue
}
if n := countRows(t, lib, entry.Name); n != 0 {
t.Errorf(
"%s (%s) has %d rows after the only library was removed. "+
"Either delete them in RemoveLibrary, or add an entry "+
"to staleTolerated explaining why they stay.",
entry.Name, entry.Kind, n,
)
}
}
}
// Authored data must survive removal of the library it was created
// against — losing it is unrecoverable, so it must never be a casualty
// of cleaning up owned data.
func TestRemoveLibraryPreservesAuthoredData(t *testing.T) {
t.Parallel()
lib, _ := setupTestLibrary(t)
library := seedRemovableLibrary(t, lib, "/nonexistent/cover.jpg")
if _, err := lib.db.ExecContext(
`INSERT INTO playlists (name) VALUES ('Keep Me')`,
); err != nil {
t.Fatalf("seed playlist: %v", err)
}
if _, err := lib.RemoveLibrary(library.ID); err != nil {
t.Fatalf("RemoveLibrary: %v", err)
}
if n := countRows(t, lib, "playlists"); n != 1 {
t.Errorf("playlists = %d rows after removal, want 1 preserved", n)
}
}
// Every table the catalog marks as needing an explicit sweep must
// actually reach zero, or be listed as tolerated. This is a narrower
// restatement of the leak test aimed at the Lifetime axis rather than
// the Kind axis, so a table declared "swept" that nothing sweeps is
// caught.
func TestSweptTablesAreActuallySwept(t *testing.T) {
t.Parallel()
lib, _ := setupTestLibrary(t)
library := seedRemovableLibrary(t, lib, "/nonexistent/cover.jpg")
if _, err := lib.RemoveLibrary(library.ID); err != nil {
t.Fatalf("RemoveLibrary: %v", err)
}
for _, entry := range datamap.Tables() {
if entry.Lifetime != datamap.Swept || entry.FTS {
continue
}
// Cache tables are swept by the janitor on their own schedule,
// not by library removal.
if entry.Kind == datamap.Cache {
continue
}
if _, exempt := staleTolerated[entry.Name]; exempt {
continue
}
if n := countRows(t, lib, entry.Name); n != 0 {
t.Errorf(
"%s declares Lifetime swept but still has %d rows after "+
"removal — nothing is sweeping it",
entry.Name, n,
)
}
}
}