* fix(metadata): publish artwork revisions atomically * fix(metadata): harden artwork revision cleanup * fix(metadata): address artwork revision review findings - restore image applies for all media_items types and reject unsupported target/image combinations with 400 before uploading; episodes coerce to stills and the web dialog no longer offers image tabs episodes can't use - add WHEN clauses to displacement triggers and hoist to_jsonb so bulk catalog upserts that assign unchanged artwork columns skip the trigger - make artworkkey the single variant-ladder owner: imagecache derives its widths from it and triggers store image_type instead of hardcoded variant arrays, expanded by the collector at deletion time - sweep dormant registry rows periodically so references lost through untriggered surfaces degrade to slow cleanup instead of leaking - park just-published revisions dormant, keep dormant rows dormant on re-cache, and batch the GC reference pre-check per run - heal rows re-referencing a just-deleted revision via reconciler-style resets after the deletion commits - share a per-URL image-loaded hook across DetailHero, ItemCard, SectionItemCard, GlobalSearch, and CollectionPosterCard - deduplicate Cache/CacheBytes finalization and drop unused VariantPaths plumbing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(catalog): cast reused timestamp parameter in revision upsert Postgres cannot deduce one type for $3 used both as a plain value and inside a CASE arm; the dev deploy surfaced it as SQLSTATE 42P08 on every publication. Cast both uses and cover the arm/park/track upserts with database-backed tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(metadata): address artwork revision review comments - keep a durable heal path: deletion marks deleted_at instead of removing the registry row, so a failed post-delete heal retries with backoff and broken references never park; trackers clear the marker on re-upload - never treat bare existence as an immutable-content match; backends without content verification rewrite the object - exercise revisioned cover keys in scanner/enrichment fakes, compare the tracked manifest exactly, and honor cancellation in the blocking test deleter Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
102 lines
3.3 KiB
Go
102 lines
3.3 KiB
Go
package catalog
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
func newArtworkSelectionTestPool(t *testing.T) *pgxpool.Pool {
|
|
t.Helper()
|
|
dsn := os.Getenv("SILO_TEST_DATABASE_URL")
|
|
if dsn == "" {
|
|
t.Skip("SILO_TEST_DATABASE_URL is not set")
|
|
}
|
|
pool, err := pgxpool.New(context.Background(), dsn)
|
|
if err != nil {
|
|
t.Fatalf("connect test database: %v", err)
|
|
}
|
|
t.Cleanup(pool.Close)
|
|
return pool
|
|
}
|
|
|
|
func TestQueueAndParkArtworkRevisionUpserts(t *testing.T) {
|
|
pool := newArtworkSelectionTestPool(t)
|
|
ctx := context.Background()
|
|
suffix := time.Now().UnixNano()
|
|
path := fmt.Sprintf("tmdb/movies/%d/poster/original.rev.webp", suffix)
|
|
t.Cleanup(func() {
|
|
_, _ = pool.Exec(ctx, `DELETE FROM artwork_revision_gc_candidates WHERE original_path = $1`, path)
|
|
})
|
|
|
|
// Arming registers the revision for verification after the grace period.
|
|
if err := queueArtworkRevisionGC(ctx, pool, path, "poster", time.Now().Add(time.Hour)); err != nil {
|
|
t.Fatalf("queueArtworkRevisionGC: %v", err)
|
|
}
|
|
var imageType string
|
|
var nextAttempt *time.Time
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT image_type, next_attempt_at FROM artwork_revision_gc_candidates
|
|
WHERE original_path = $1`, path).Scan(&imageType, &nextAttempt); err != nil {
|
|
t.Fatalf("load armed candidate: %v", err)
|
|
}
|
|
if imageType != "poster" {
|
|
t.Fatalf("image_type = %q, want poster", imageType)
|
|
}
|
|
if nextAttempt == nil {
|
|
t.Fatal("armed candidate has NULL next_attempt_at")
|
|
}
|
|
|
|
// Publication parks the selected revision: referenced by construction.
|
|
if err := parkArtworkRevision(ctx, pool, path, "poster", time.Now().Add(time.Hour)); err != nil {
|
|
t.Fatalf("parkArtworkRevision: %v", err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT next_attempt_at FROM artwork_revision_gc_candidates
|
|
WHERE original_path = $1`, path).Scan(&nextAttempt); err != nil {
|
|
t.Fatalf("load parked candidate: %v", err)
|
|
}
|
|
if nextAttempt != nil {
|
|
t.Fatalf("parked candidate next_attempt_at = %v, want NULL", *nextAttempt)
|
|
}
|
|
}
|
|
|
|
func TestTrackArtworkRevisionKeepsDormantRowsDormant(t *testing.T) {
|
|
pool := newArtworkSelectionTestPool(t)
|
|
ctx := context.Background()
|
|
suffix := time.Now().UnixNano()
|
|
path := fmt.Sprintf("tmdb/movies/%d/poster/original.live.webp", suffix)
|
|
keys := []string{path, fmt.Sprintf("tmdb/movies/%d/poster/w500.live.webp", suffix)}
|
|
t.Cleanup(func() {
|
|
_, _ = pool.Exec(ctx, `DELETE FROM artwork_revision_gc_candidates WHERE original_path = $1`, path)
|
|
})
|
|
|
|
if err := parkArtworkRevision(ctx, pool, path, "poster", time.Now().Add(time.Hour)); err != nil {
|
|
t.Fatalf("parkArtworkRevision: %v", err)
|
|
}
|
|
// A re-cache of live artwork must not re-arm the parked row.
|
|
tracker := NewArtworkRevisionTracker(pool)
|
|
if err := tracker.TrackArtworkRevision(ctx, path, "poster", keys); err != nil {
|
|
t.Fatalf("TrackArtworkRevision: %v", err)
|
|
}
|
|
|
|
var nextAttempt *time.Time
|
|
var storedKeys []string
|
|
if err := pool.QueryRow(ctx, `
|
|
SELECT next_attempt_at, object_keys FROM artwork_revision_gc_candidates
|
|
WHERE original_path = $1`, path).Scan(&nextAttempt, &storedKeys); err != nil {
|
|
t.Fatalf("load candidate: %v", err)
|
|
}
|
|
if nextAttempt != nil {
|
|
t.Fatalf("re-cache re-armed dormant row: next_attempt_at = %v", *nextAttempt)
|
|
}
|
|
if !slices.Equal(storedKeys, keys) {
|
|
t.Fatalf("stored manifest = %v, want exact tracked manifest %v", storedKeys, keys)
|
|
}
|
|
}
|