test: buildReviewGroups — completeness, season buckets, sort, filters
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
161
server/api/__tests__/review-groups.test.ts
Normal file
161
server/api/__tests__/review-groups.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -385,7 +385,7 @@ type ReviewGroup =
|
||||
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
|
||||
.prepare(`
|
||||
SELECT rp.*, mi.name, mi.series_name, mi.series_jellyfin_id,
|
||||
|
||||
Reference in New Issue
Block a user