fix(credits): replace title cast on refresh and remove duplicate crew rows

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Sonnet 5.5 committed 2026-10-04 16:06:00 -04:00
1 parent 4addc32e76
commit 65d070ff23
5 files changed
+3555 -18

No files matched your search

+111 -1
View File
@@ -1,7 +1,11 @@
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { persons, titleCast } from "@sofa/db/schema";
import { clearAllTables, eq, insertTitle, testDb } from "@sofa/test/db";
import { clearAllTables, eq, insertTitle, testClient, testDb } from "@sofa/test/db";
const { mockGetMovieCredits, mockGetTvAggregateCredits } = vi.hoisted(() => ({
mockGetMovieCredits: vi.fn<
@@ -191,6 +195,77 @@ describe("refreshCredits", () => {
expect(creator).toBeDefined();
});
test("does not duplicate crew rows when refreshed twice", async () => {
insertTitle({ id: "m1", tmdbId: 100, type: "movie" });
mockGetMovieCredits.mockResolvedValue({
cast: [{ id: 1, name: "Actor A", profile_path: "/a.jpg", character: "Hero", popularity: 10 }],
crew: [
{
id: 3,
name: "Dir Person",
profile_path: "/d.jpg",
job: "Director",
department: "Directing",
popularity: 8,
},
],
});
await refreshCredits("m1");
await refreshCredits("m1");
const castRows = testDb.select().from(titleCast).where(eq(titleCast.titleId, "m1")).all();
expect(castRows).toHaveLength(2);
});
test("removes cast members no longer returned by TMDB", async () => {
insertTitle({ id: "m1", tmdbId: 100, type: "movie" });
const actorA = {
id: 1,
name: "Actor A",
profile_path: null,
character: "Hero",
popularity: 10,
};
const actorB = {
id: 2,
name: "Actor B",
profile_path: null,
character: "Villain",
popularity: 5,
};
mockGetMovieCredits.mockResolvedValue({ cast: [actorA, actorB], crew: [] });
await refreshCredits("m1");
expect(testDb.select().from(titleCast).where(eq(titleCast.titleId, "m1")).all()).toHaveLength(
2,
);
mockGetMovieCredits.mockResolvedValue({ cast: [actorA], crew: [] });
await refreshCredits("m1");
expect(testDb.select().from(titleCast).where(eq(titleCast.titleId, "m1")).all()).toHaveLength(
1,
);
});
test("keeps existing credits when TMDB returns an empty payload", async () => {
insertTitle({ id: "m1", tmdbId: 100, type: "movie" });
mockGetMovieCredits.mockResolvedValue({
cast: [{ id: 1, name: "Actor A", profile_path: null, character: "Hero", popularity: 10 }],
crew: [],
});
await refreshCredits("m1");
mockGetMovieCredits.mockResolvedValue({ cast: [], crew: [] });
await refreshCredits("m1");
expect(testDb.select().from(titleCast).where(eq(titleCast.titleId, "m1")).all()).toHaveLength(
1,
);
});
test("does nothing for nonexistent title", async () => {
await refreshCredits("nonexistent");
expect(mockGetMovieCredits).not.toHaveBeenCalled();
@@ -284,3 +359,38 @@ describe("refreshCredits", () => {
expect(jobs).toEqual(["Director", "Writer"]);
});
});
// ─── dedupe migration ───────────────────────────────────────────────
describe("dedupe_title_cast migration", () => {
test("keeps one row per credit", () => {
insertTitle({ id: "m1", tmdbId: 1 });
insertPerson("p1", 100, "Director Person");
for (let i = 0; i < 3; i++) {
testDb
.insert(titleCast)
.values({
titleId: "m1",
personId: "p1",
character: null,
department: "Directing",
job: "Director",
displayOrder: 100,
lastFetchedAt: new Date(),
})
.run();
}
insertCastEntry("m1", "p1", { character: "Hero" });
const dir = fileURLToPath(new URL("../../db/drizzle", import.meta.url));
const folder = readdirSync(dir).find((d) => d.endsWith("_dedupe_title_cast"));
expect(folder).toBeDefined();
testClient.exec(readFileSync(join(dir, folder!, "migration.sql"), "utf8"));
const rows = testDb.select().from(titleCast).where(eq(titleCast.titleId, "m1")).all();
expect(rows).toHaveLength(2);
expect(rows.filter((r) => r.department === "Directing")).toHaveLength(1);
expect(rows.filter((r) => r.department === "Acting")).toHaveLength(1);
});
});