import { expect, test } from "bun:test"; import { readdir, readFile, stat } from "node:fs/promises"; import { join } from "node:path"; import { importPdfDeck } from "../src/jobs/pdf-deck"; import { importVideoFolder, lessonTitle } from "../src/jobs/video-folder"; import { makeEnv } from "./harness"; import { makePdf, makeVideo, tmp } from "./fixtures"; test("lessonTitle strips ordering prefixes", () => { expect(lessonTitle("01 - Intro to Nix.mp4")).toBe("Intro to Nix"); expect(lessonTitle("2_setup_env.mkv")).toBe("setup env"); expect(lessonTitle("12.mp4")).toBe("12"); }); async function folderCourse() { const env = await makeEnv(); const dir = await tmp(); const src = join(dir, "My Homelab Videos"); await makeVideo(join(src, "10 - Last.mp4"), 1); await makeVideo(join(src, "2 - Second Clip.mp4"), 1); await makeVideo(join(src, "1 - Ünïcode first.mp4"), 1); const before = (await readdir(src)).sort(); const r = await importVideoFolder(env.deps, { kind: "video-folder", path: src }, env.jc); return { env, src, before, r }; } test("a folder of videos becomes one lesson per file in natural order", async () => { const { env, src, before, r } = await folderCourse(); expect(r.courseId).toBe("my-homelab-videos"); const m = env.deps.ctx.courses.get(r.courseId)!; expect(m.status).toBe("ok"); expect(m.manifest!.modules[0]!.lessons.map((l) => l.title)).toEqual(["Ünïcode first", "Second Clip", "Last"]); expect(m.manifest!.visibility).toBe("restricted"); expect(m.manifest!.source?.kind).toBe("video-folder"); expect(env.deps.ctx.courses.resolvePath(r.courseId, "media/1 - Ünïcode first.mp4")).toBeTruthy(); expect((await readdir(src)).sort()).toEqual(before); }); test("a deck attaches to a lesson, builds page images and text, and leaves the PDF original alone", async () => { const { env, r } = await folderCourse(); const d = await tmp(); await makePdf(join(d, "Slides Deck.pdf"), ["Alpha slide", "Beta slide", "Gamma slide"]); const before = await readFile(join(d, "Slides Deck.pdf")); const res = await importPdfDeck(env.deps, { kind: "pdf-deck", path: join(d, "Slides Deck.pdf"), options: { courseId: r.courseId, attachTo: "l2", title: "My deck" } }, env.jc); expect(res.warnings).toEqual([]); const c = env.deps.ctx.courses.get(r.courseId)!; expect(c.status).toBe("ok"); expect(c.manifest!.decks).toEqual([{ id: "d-slides-deck", file: "media/decks/Slides Deck.pdf", title: "My deck", attachTo: "l2" }]); const cache = join(env.dataDir, "cache/decks", r.courseId, "d-slides-deck"); expect((await readdir(cache)).sort()).toEqual(["p-001.jpg", "p-002.jpg", "p-003.jpg", "text.json"]); expect(JSON.parse(await readFile(join(cache, "text.json"), "utf8"))[2].text).toContain("Gamma slide"); expect((await stat(join(env.dataDir, "cache/covers", r.courseId, "deck-d-slides-deck.jpg"))).size).toBeGreaterThan(100); expect(Buffer.compare(await readFile(join(d, "Slides Deck.pdf")), before)).toBe(0); }); test("deck attach validates the target and refuses courses the importer did not create", async () => { const { env, r } = await folderCourse(); const d = await tmp(); await makePdf(join(d, "a.pdf")); await expect(importPdfDeck(env.deps, { path: join(d, "a.pdf"), options: { courseId: r.courseId, attachTo: "nope" } }, env.jc)).rejects.toThrow(/not a lesson or module/); await expect(importPdfDeck(env.deps, { path: join(d, "a.pdf"), options: { courseId: r.courseId } }, env.jc)).rejects.toThrow(/attachTo is required/); await expect(importPdfDeck(env.deps, { path: join(d, "a.pdf"), options: { courseId: "sample", attachTo: "x" } }, env.jc)).rejects.toThrow(/only edits courses it created/); await expect(importPdfDeck(env.deps, { path: join(d, "a.pdf"), options: { courseId: "../x", attachTo: "x" } }, env.jc)).rejects.toThrow(/only edits courses it created/); await expect(importPdfDeck(env.deps, { path: join(d, "missing.pdf"), options: {} }, env.jc)).rejects.toThrow(/not a PDF|outside the allowed/); }); test("without PyMuPDF the deck is still attached, with a warning", async () => { const { env, r } = await folderCourse(); env.deps.python = "/nonexistent/python"; const d = await tmp(); await makePdf(join(d, "a.pdf")); const res = await importPdfDeck(env.deps, { path: join(d, "a.pdf"), options: { courseId: r.courseId, attachTo: "m1" } }, env.jc); expect(res.warnings.join()).toContain("page images and text could not be built"); expect(env.deps.ctx.courses.get(r.courseId)!.manifest!.decks).toHaveLength(1); });