test: buildReviewGroups — completeness, season buckets, sort, filters

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 12:10:47 +02:00
parent 3f910873eb
commit 4e96382097
2 changed files with 162 additions and 1 deletions

View File

@@ -0,0 +1,161 @@
import { Database } from "bun:sqlite";
import { describe, expect, test } from "bun:test";
import { SCHEMA } from "../../db/schema";
import { buildReviewGroups } from "../review";
function makeDb(): Database {
const db = new Database(":memory:");
for (const stmt of SCHEMA.split(";")) {
const trimmed = stmt.trim();
if (trimmed) db.run(trimmed);
}
return db;
}
interface SeedOpts {
id: number;
type: "Movie" | "Episode";
name?: string;
seriesName?: string | null;
seriesJellyfinId?: string | null;
seasonNumber?: number | null;
episodeNumber?: number | null;
confidence?: "high" | "low";
}
function seed(db: Database, opts: SeedOpts) {
const {
id,
type,
name = `Item ${id}`,
seriesName = null,
seriesJellyfinId = null,
seasonNumber = null,
episodeNumber = null,
confidence = "high",
} = opts;
db
.prepare(
"INSERT INTO media_items (id, jellyfin_id, type, name, series_name, series_jellyfin_id, season_number, episode_number, file_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.run(id, `jf-${id}`, type, name, seriesName, seriesJellyfinId, seasonNumber, episodeNumber, `/x/${id}.mkv`);
db
.prepare(
"INSERT INTO review_plans (item_id, status, is_noop, confidence, apple_compat, job_type, notes) VALUES (?, 'pending', 0, ?, 'direct_play', 'copy', NULL)",
)
.run(id, confidence);
}
describe("buildReviewGroups", () => {
test("returns a complete series with every pending episode", () => {
const db = makeDb();
for (let i = 1; i <= 30; i++) {
seed(db, {
id: i,
type: "Episode",
seriesName: "Breaking Bad",
seriesJellyfinId: "bb",
seasonNumber: 1,
episodeNumber: i,
});
}
const { groups, totalItems } = buildReviewGroups(db);
expect(groups).toHaveLength(1);
const series = groups[0];
expect(series.kind).toBe("series");
if (series.kind !== "series") throw new Error("expected series");
expect(series.episodeCount).toBe(30);
expect(series.seasons).toHaveLength(1);
expect(series.seasons[0].episodes).toHaveLength(30);
expect(totalItems).toBe(30);
});
test("buckets episodes by season with null ordered last", () => {
const db = makeDb();
for (let ep = 1; ep <= 3; ep++) {
seed(db, {
id: ep,
type: "Episode",
seriesName: "Lost",
seriesJellyfinId: "lost",
seasonNumber: 1,
episodeNumber: ep,
});
}
for (let ep = 1; ep <= 2; ep++) {
seed(db, {
id: 10 + ep,
type: "Episode",
seriesName: "Lost",
seriesJellyfinId: "lost",
seasonNumber: 2,
episodeNumber: ep,
});
}
seed(db, { id: 99, type: "Episode", seriesName: "Lost", seriesJellyfinId: "lost", seasonNumber: null });
const { groups } = buildReviewGroups(db);
expect(groups).toHaveLength(1);
const lost = groups[0];
if (lost.kind !== "series") throw new Error("expected series");
expect(lost.seasons.map((s) => s.season)).toEqual([1, 2, null]);
expect(lost.seasons[0].episodes).toHaveLength(3);
expect(lost.seasons[1].episodes).toHaveLength(2);
expect(lost.seasons[2].episodes).toHaveLength(1);
});
test("sorts groups: high-confidence first, then by name", () => {
const db = makeDb();
seed(db, { id: 1, type: "Movie", name: "Zodiac", confidence: "high" });
seed(db, { id: 2, type: "Movie", name: "Arrival", confidence: "low" });
seed(db, { id: 3, type: "Movie", name: "Blade Runner", confidence: "high" });
const { groups } = buildReviewGroups(db);
const names = groups.map((g) => (g.kind === "movie" ? g.item.name : g.seriesName));
expect(names).toEqual(["Blade Runner", "Zodiac", "Arrival"]);
});
test("minConfidence is low when any episode in the series is low", () => {
const db = makeDb();
seed(db, {
id: 1,
type: "Episode",
seriesName: "Show",
seriesJellyfinId: "s",
seasonNumber: 1,
episodeNumber: 1,
confidence: "high",
});
seed(db, {
id: 2,
type: "Episode",
seriesName: "Show",
seriesJellyfinId: "s",
seasonNumber: 1,
episodeNumber: 2,
confidence: "low",
});
const { groups } = buildReviewGroups(db);
expect(groups).toHaveLength(1);
if (groups[0].kind !== "series") throw new Error("expected series");
expect(groups[0].minConfidence).toBe("low");
});
test("excludes plans that are not pending or are is_noop=1", () => {
const db = makeDb();
seed(db, { id: 1, type: "Movie", name: "Pending" });
seed(db, { id: 2, type: "Movie", name: "Approved" });
db.prepare("UPDATE review_plans SET status = 'approved' WHERE item_id = ?").run(2);
seed(db, { id: 3, type: "Movie", name: "Noop" });
db.prepare("UPDATE review_plans SET is_noop = 1 WHERE item_id = ?").run(3);
const { groups, totalItems } = buildReviewGroups(db);
expect(groups).toHaveLength(1);
expect(totalItems).toBe(1);
if (groups[0].kind !== "movie") throw new Error("expected movie");
expect(groups[0].item.name).toBe("Pending");
});
});

View File

@@ -385,7 +385,7 @@ type ReviewGroup =
seasons: Array<{ season: number | null; episodes: ReviewItemRow[] }>; seasons: Array<{ season: number | null; episodes: ReviewItemRow[] }>;
}; };
function buildReviewGroups(db: ReturnType<typeof getDb>): { groups: ReviewGroup[]; totalItems: number } { export function buildReviewGroups(db: ReturnType<typeof getDb>): { groups: ReviewGroup[]; totalItems: number } {
const rows = db const rows = db
.prepare(` .prepare(`
SELECT rp.*, mi.name, mi.series_name, mi.series_jellyfin_id, SELECT rp.*, mi.name, mi.series_name, mi.series_jellyfin_id,