Files
bookhoard/cmd/server/tests/phase13_test.go
T
John O'Keefe 8c3d919aa0 wip(rls): engine-side fail-loud + harness seed identity [pin: work interrupted]
Design correction discovered by the auth chicken-and-egg: the
decorator-level refusal was re-created a convention gate (plan B
thinking). ScopedDB now NEVER refuses — it declares the GUC when the
context carries an identity and declares nothing otherwise; the ENGINE
is the fail-loud layer (policies raise on governed tables, ungoverned
tables like auth lookups simply work). rls.WithInternal removed as a
dead concept.

- search service's raw DBTX path now receives the ScopedDB itself
  (main.go + harness) — a bare pool bypassed the GUC.
- database/schema.go: migration tx declares a synthetic identity
  (schema.sql's dedup DO block SELECTs media_items; with policies
  live, boot 2 raised 'unrecognized configuration parameter' — A11).
- harness: TestServerSetup.DB is a raw-pool SEED handle (write
  policies permissive, other seeded tables ungoverned); the server
  under test keeps the scoped wiring. 33 CreateMediaItem sites use
  seedCtx() — a seed identity with no visibility rows — because
  RETURNING requires SELECT visibility of the new row.
- invariant test: regex UUID-scraping replaced with a structured JSON
  decode of the sections payload.

INTERRUPTION NOTE: an edit to this same effect (seedCtx) was applied,
confirmed by the tool, and subsequently vanished from disk while an
'opencode serve' process (PID 84551) is alive on the machine. This
commit pins the tree against further loss; implementation is PAUSED
pending the user killing that process.
2026-10-29 08:44:12 -04:00

188 lines
7.2 KiB
Go

package main
// Phase 13 (faceted plan): force rescan = original-scan semantics, and
// the manually_edited filter.
//
// Force rescan contract: re-review every field on every book from all
// sources (sidecars → folder structure → tags → fallbacks) on multi-
// file books too — the pre-Phase-13 skip is gone — with
// metadata_overrides as the only protection. A force pass also stamps
// the fresh manifest with the current parser version (success-only)
// and closes the book's open ledger issues.
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"bookhoard/internal/database"
"bookhoard/internal/services"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// pinExtractorVersionOne pins the SHARED dev DB's extractor version to
// the default 1 for the test's duration. Prior interrupted runs can
// leave it bumped (an early test failure aborts before its restore
// cleanup registers), and every stamp assertion in these tests assumes
// the default. Restore-to-prior rides inside setExtractorVersion: the
// suite leaves the setting exactly as it found it.
func pinExtractorVersionOne(t *testing.T, setup *TestServerSetup) {
t.Helper()
setExtractorVersion(t, setup, 1)
}
// forceScanTestFolder runs a FORCE scan over one folder.
func forceScanTestFolder(t *testing.T, setup *TestServerSetup, adminID pgtype.UUID, folder string) {
t.Helper()
scanner := services.NewMediaScanner(setup.DB)
defer func() { _ = scanner.Close() }()
scanner.SetAdminID(adminID)
require.NoError(t, scanner.SetFolders([]string{folder}, false))
scanner.SetForce(true)
require.NoError(t, scanner.ScanFolders(context.Background()))
}
func TestForceRescanMultifileSidecarApply(t *testing.T) {
setup := setupTestServer(t)
t.Cleanup(func() { _ = setup.Close() })
ctx := context.Background()
admin, _ := moveTestAdminID(t, setup)
pinExtractorVersionOne(t, setup)
libID, folder := audioTestLibrary(t, setup, fmt.Sprintf("Test Force Rescan %d", time.Now().UnixNano()))
t.Cleanup(func() { deleteLibraries(t, setup, libID) })
writeTestTrack(t, filepath.Join(folder, "Force Author", "Force Title", "01 First.mp3"))
writeTestTrack(t, filepath.Join(folder, "Force Author", "Force Title", "02 Second.mp3"))
scanTestFolder(t, setup, admin, folder)
rel := filepath.ToSlash("Force Author/Force Title")
book, found := findAudioItemByPath(t, setup, libID, rel)
require.True(t, found, "folder book must import")
require.Equal(t, "Force Title", book.Title)
// Corrupt, then FORCE: the pre-Phase-13 skip is gone — the full
// chain re-derives the title from the folder basename.
corruptItemColumn(t, setup, book.ID, "title", "ZZZ")
forceScanTestFolder(t, setup, admin, folder)
book, found = findAudioItemByPath(t, setup, libID, rel)
require.True(t, found)
require.Equal(t, "Force Title", book.Title, "force rescan must re-apply folder-book metadata")
require.Equal(t, 1, storedManifestEv(t, setup, book.ID), "force rescan stamps the current parser version")
// metadata_overrides is the ONLY protection: a recorded override
// survives the force pass untouched.
_, err := setup.DBPool.Exec(ctx,
`UPDATE media_items SET metadata_overrides = ARRAY['title'], title = 'Manual Title' WHERE id = $1`,
book.ID)
require.NoError(t, err)
forceScanTestFolder(t, setup, admin, folder)
book, _ = findAudioItemByPath(t, setup, libID, rel)
require.Equal(t, "Manual Title", book.Title, "metadata_overrides protect manual edits under force rescan")
}
func TestForceRescanSingleFileSidecarReapply(t *testing.T) {
setup := setupTestServer(t)
t.Cleanup(func() { _ = setup.Close() })
admin, _ := moveTestAdminID(t, setup)
pinExtractorVersionOne(t, setup)
libID, folder := createTypedTestLibrary(t, setup, fmt.Sprintf("Test Force Single %d", time.Now().UnixNano()), "ebooks")
t.Cleanup(func() { deleteLibraries(t, setup, libID) })
bookDir := filepath.Join(folder, "Force Probe Author", "Force Probe Book")
require.NoError(t, os.MkdirAll(bookDir, 0o755))
opf := `<?xml version='1.0' encoding='utf-8'?>
<package xmlns="http://www.idpf.org/2007/opf" version="2.0">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:title>Force Probe Book</dc:title>
<dc:creator>Force Probe Author</dc:creator>
</metadata>
</package>`
require.NoError(t, os.WriteFile(filepath.Join(bookDir, "metadata.opf"), []byte(opf), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(bookDir, "Force Probe Book.epub"), []byte("dummy epub content"), 0o644))
scanTestFolder(t, setup, admin, folder)
rel := filepath.ToSlash("Force Probe Author/Force Probe Book/Force Probe Book.epub")
book, found := findAudioItemByPath(t, setup, libID, rel)
require.True(t, found)
// Single-file force rescan already ran the full chain — pin it, and
// pin the NEW part: the manifest comes out stamped.
corruptItemColumn(t, setup, book.ID, "title", "ZZZ")
forceScanTestFolder(t, setup, admin, folder)
book, found = findAudioItemByPath(t, setup, libID, rel)
require.True(t, found)
require.Equal(t, "Force Probe Book", book.Title, "force rescan re-derives sidecar metadata (single-file)")
require.Equal(t, 1, storedManifestEv(t, setup, book.ID), "force rescan stamps the manifest")
}
func TestManuallyEditedFilter(t *testing.T) {
setup := setupTestServer(t)
t.Cleanup(func() { _ = setup.Close() })
ctx := context.Background()
libID, _ := createTypedTestLibrary(t, setup, fmt.Sprintf("Test Manual Edits %d", time.Now().UnixNano()), "ebooks")
t.Cleanup(func() { deleteLibraries(t, setup, libID) })
createBook := func(path string) pgtype.UUID {
created, err := setup.DB.CreateMediaItem(seedCtx(), database.CreateMediaItemParams{
LibraryID: libID,
Title: path,
Authors: []string{},
FilePath: path,
FileSize: pgtype.Int8{Int64: 64, Valid: true},
MimeType: pgtype.Text{String: "application/epub+zip", Valid: true},
})
require.NoError(t, err)
return created.ID
}
editedID := createBook("edited-book.epub")
cleanID := createBook("clean-book.epub")
_, err := setup.DBPool.Exec(ctx,
`UPDATE media_items SET metadata_overrides = ARRAY['title'] WHERE id = $1`,
editedID)
require.NoError(t, err)
qfIDs := func(op string) []string {
status, rows := qfSearch(t, setup, setup.Token, map[string][]string{
"qf": {qfAll(qfRule("manually_edited", op))},
"library_id": {libID.String()},
})
require.Equal(t, http.StatusOK, status)
ids := make([]string, 0, len(rows))
for _, r := range rows {
ids = append(ids, r["id"].(string))
}
return ids
}
trueIDs := qfIDs("is_true")
require.Len(t, trueIDs, 1)
assert.Equal(t, editedID.String(), trueIDs[0], "is_true matches only manually-edited books")
falseIDs := qfIDs("is_false")
require.Len(t, falseIDs, 1)
assert.Equal(t, cleanID.String(), falseIDs[0], "is_false matches only untouched books")
// The deep-linkable JSON shape round-trips through the qf parser
// (arity-none rule: no values on the wire).
var parsed struct {
Mode string `json:"mode"`
Children []struct {
Field string `json:"field"`
} `json:"children"`
}
require.NoError(t, json.Unmarshal([]byte(qfAll(qfRule("manually_edited", "is_true"))), &parsed))
require.Len(t, parsed.Children, 1)
assert.Equal(t, "manually_edited", parsed.Children[0].Field)
}