Test-isolation fix. Integration tests previously TRUNCATEd the users table at setup, which wiped the operator's admin login every time the suite ran against a Postgres instance shared with their dev environment. This had been informally tracked as Fable #339 since M3.
What this changes
internal/dbtest/reset.go (new)
Shared helper exposing:
ResetDB(t, pool) — TRUNCATEs every data table EXCEPT users, then DELETE FROM users WHERE username LIKE 'test-%'. Operator's admin row (and any other non-test row) is left untouched.
TestUserPrefix = "test-" — required prefix for any user row a test creates.
Test files migrated to ResetDB (9 files)
internal/api/auth_test.go — seedUser helper now auto-prefixes usernames.
internal/api/me_test.go — assertion updated for prefixed username.
internal/playevents/writer_test.go
internal/playsessions/service_test.go
internal/recommendation/candidates_test.go (covers candidates_v2_test.go via shared fixture)
internal/scrobble/queue_test.go
internal/similarity/worker_integration_test.go
internal/subsonic/scrobble_test.go
internal/subsonic/star_test.go
Hardcoded test usernames ("tester", "bob") now use dbtest.TestUserPrefix + "...". The "alice" / "bob" literals in api/auth_test.go flow through the seedUser helper which prefixes internally — caller-side names stay readable.
internal/auth/bootstrap_test.go (special case)
This test fundamentally requires an empty users table — it exercises the "first time admin is created" path that Bootstrap() guards with a count == 0 check. So it still TRUNCATEs users. To keep the operator's admin intact:
Before TRUNCATE, save every user that doesn't start with test-.
Use cfg.Username = "test-admin" for the bootstrap call so the test row is itself test-prefixed.
t.Cleanup restores the saved rows after assertions complete.
Net effect: operator's admin/<password> login keeps working between test runs.
Test plan
go build ./... clean
go vet ./... clean
golangci-lint run ./... clean
go test -short -race ./... all packages green
Integration suite (in-network docker run, go test -race -p 1 ./...) — all packages green except library/scanner_test which races with the operator's running minstrel container scanning their music library (pre-existing local-only flake; CI's ephemeral DB doesn't see it)
Manual verify: operator's admin/admin login still works after running the full integration suite locally
The "proper" long-term fix is to make tests refuse to run when MINSTREL_TEST_DATABASE_URL is missing or equal to DATABASE_URL — i.e., enforce a separate test database. This PR is the minimal, surgical fix that stops the bleeding without that infra change.
Test-isolation fix. Integration tests previously TRUNCATEd the `users` table at setup, which wiped the operator's admin login every time the suite ran against a Postgres instance shared with their dev environment. This had been informally tracked as Fable #339 since M3.
## What this changes
### `internal/dbtest/reset.go` (new)
Shared helper exposing:
- **`ResetDB(t, pool)`** — TRUNCATEs every data table EXCEPT `users`, then `DELETE FROM users WHERE username LIKE 'test-%'`. Operator's admin row (and any other non-test row) is left untouched.
- **`TestUserPrefix = "test-"`** — required prefix for any user row a test creates.
### Test files migrated to `ResetDB` (9 files)
- `internal/api/auth_test.go` — `seedUser` helper now auto-prefixes usernames.
- `internal/api/me_test.go` — assertion updated for prefixed username.
- `internal/playevents/writer_test.go`
- `internal/playsessions/service_test.go`
- `internal/recommendation/candidates_test.go` (covers `candidates_v2_test.go` via shared fixture)
- `internal/scrobble/queue_test.go`
- `internal/similarity/worker_integration_test.go`
- `internal/subsonic/scrobble_test.go`
- `internal/subsonic/star_test.go`
Hardcoded test usernames (`"tester"`, `"bob"`) now use `dbtest.TestUserPrefix + "..."`. The `"alice"` / `"bob"` literals in `api/auth_test.go` flow through the `seedUser` helper which prefixes internally — caller-side names stay readable.
### `internal/auth/bootstrap_test.go` (special case)
This test fundamentally requires an empty `users` table — it exercises the "first time admin is created" path that `Bootstrap()` guards with a `count == 0` check. So it still TRUNCATEs `users`. To keep the operator's admin intact:
1. Before TRUNCATE, save every user that doesn't start with `test-`.
2. Use `cfg.Username = "test-admin"` for the bootstrap call so the test row is itself test-prefixed.
3. `t.Cleanup` restores the saved rows after assertions complete.
Net effect: operator's `admin/<password>` login keeps working between test runs.
## Test plan
- [x] `go build ./...` clean
- [x] `go vet ./...` clean
- [x] `golangci-lint run ./...` clean
- [x] `go test -short -race ./...` all packages green
- [x] Integration suite (in-network docker run, `go test -race -p 1 ./...`) — all packages green except `library/scanner_test` which races with the operator's running minstrel container scanning their music library (pre-existing local-only flake; CI's ephemeral DB doesn't see it)
- [x] **Manual verify: operator's `admin/admin` login still works after running the full integration suite locally**
- [x] **Manual verify: `bootstrap_test.go` save/restore round-trip preserves admin row**
## Out of scope (for follow-up)
The "proper" long-term fix is to make tests refuse to run when `MINSTREL_TEST_DATABASE_URL` is missing or equal to `DATABASE_URL` — i.e., enforce a separate test database. This PR is the minimal, surgical fix that stops the bleeding without that infra change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Test-isolation fix. Integration tests previously TRUNCATEd the
userstable at setup, which wiped the operator's admin login every time the suite ran against a Postgres instance shared with their dev environment. This had been informally tracked as Fable #339 since M3.What this changes
internal/dbtest/reset.go(new)Shared helper exposing:
ResetDB(t, pool)— TRUNCATEs every data table EXCEPTusers, thenDELETE FROM users WHERE username LIKE 'test-%'. Operator's admin row (and any other non-test row) is left untouched.TestUserPrefix = "test-"— required prefix for any user row a test creates.Test files migrated to
ResetDB(9 files)internal/api/auth_test.go—seedUserhelper now auto-prefixes usernames.internal/api/me_test.go— assertion updated for prefixed username.internal/playevents/writer_test.gointernal/playsessions/service_test.gointernal/recommendation/candidates_test.go(coverscandidates_v2_test.govia shared fixture)internal/scrobble/queue_test.gointernal/similarity/worker_integration_test.gointernal/subsonic/scrobble_test.gointernal/subsonic/star_test.goHardcoded test usernames (
"tester","bob") now usedbtest.TestUserPrefix + "...". The"alice"/"bob"literals inapi/auth_test.goflow through theseedUserhelper which prefixes internally — caller-side names stay readable.internal/auth/bootstrap_test.go(special case)This test fundamentally requires an empty
userstable — it exercises the "first time admin is created" path thatBootstrap()guards with acount == 0check. So it still TRUNCATEsusers. To keep the operator's admin intact:test-.cfg.Username = "test-admin"for the bootstrap call so the test row is itself test-prefixed.t.Cleanuprestores the saved rows after assertions complete.Net effect: operator's
admin/<password>login keeps working between test runs.Test plan
go build ./...cleango vet ./...cleangolangci-lint run ./...cleango test -short -race ./...all packages greengo test -race -p 1 ./...) — all packages green exceptlibrary/scanner_testwhich races with the operator's running minstrel container scanning their music library (pre-existing local-only flake; CI's ephemeral DB doesn't see it)admin/adminlogin still works after running the full integration suite locallybootstrap_test.gosave/restore round-trip preserves admin rowOut of scope (for follow-up)
The "proper" long-term fix is to make tests refuse to run when
MINSTREL_TEST_DATABASE_URLis missing or equal toDATABASE_URL— i.e., enforce a separate test database. This PR is the minimal, surgical fix that stops the bleeding without that infra change.🤖 Generated with Claude Code