Files
yellowjacket/backend/library/leak_test.go
yonluandClaude Sonnet 5 cbd82a5a74
Build & publish Arch package / arch-package (push) Successful in 2m2s
Search index maintenance / maintain-index (push) Successful in 7s
feat: autotag mixed-bag splitting, search relevance fixes, and multi-library download imports
Autotag: detect "junk drawer" folders with no artist/album consensus
and split them into synthetic per-cluster groups instead of forcing
one match on an unrelated pile of tracks; repair tagging_items rows
left behind by a prior scan orphan-cleanup gap.

Explore: fix an exact artist-name search being drowned out by its own
catalog entries in intent-prior scoring, and prune stale in_library
bookkeeping left behind when a referenced library row is deleted.

Download: fix a multi-library regression where every import failed
with "no library root configured" — the importer resolved the
library root from a legacy single-library config field that nothing
populates in the current multi-library model. It now resolves the
destination library per-request from the request's own library_id.
Also widen the Soulseek search window (12s -> 20s), measured against
real request history to be missing available peers on live queries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS
2026-08-10 11:52:26 -04:00

132 lines
3.8 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",
"schema_migrations": "global migration bookkeeping, not scoped to any " +
"library; removing the only library must not touch it",
}
// 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,
)
}
}
}