import { describe, it, expect, beforeAll } from "vitest"; import { generateDungeon } from "../../src/dungeonGenerator.js"; import { generatePDF } from "../../src/generatePDF.js"; import fs from "fs/promises"; import path from "path"; const hasOllama = !!process.env.OLLAMA_API_URL; describe.skipIf(!hasOllama)("Dungeon generation (Ollama)", () => { let dungeonData; beforeAll(async () => { dungeonData = await generateDungeon(); }, 120000); it("generates dungeon data", () => { expect(dungeonData).toBeDefined(); }); it("title is 2-4 words, no colons", () => { expect(dungeonData.title).toBeTruthy(); const words = dungeonData.title.split(/\s+/); expect(words.length).toBeGreaterThanOrEqual(2); expect(words.length).toBeLessThanOrEqual(4); expect(dungeonData.title).not.toContain(":"); }); it("flavor text is ≤60 words", () => { expect(dungeonData.flavor).toBeTruthy(); const words = dungeonData.flavor.split(/\s+/); expect(words.length).toBeLessThanOrEqual(60); }); it("hooks have no title prefixes", () => { expect(dungeonData.hooksRumors).toBeDefined(); dungeonData.hooksRumors.forEach((hook) => { expect(hook).not.toMatch(/^[^:]+:\s/); }); }); it("has exactly 6 random events", () => { expect(dungeonData.randomEvents).toBeDefined(); expect(dungeonData.randomEvents.length).toBe(6); }); it("encounter details do not start with encounter name", () => { expect(dungeonData.encounters).toBeDefined(); dungeonData.encounters.forEach((encounter) => { if (encounter.details) { const detailsLower = encounter.details.toLowerCase(); const nameLower = encounter.name.toLowerCase(); expect(detailsLower.startsWith(nameLower)).toBe(false); } }); }); it("treasure descriptions do not start with 'description'", () => { expect(dungeonData.treasure).toBeDefined(); dungeonData.treasure.forEach((item) => { if (typeof item === "object" && item.description) { expect(item.description.toLowerCase().startsWith("description")).toBe(false); } }); }); it("NPC traits do not start with 'description'", () => { expect(dungeonData.npcs).toBeDefined(); dungeonData.npcs.forEach((npc) => { if (npc.trait) { expect(npc.trait.toLowerCase().startsWith("description")).toBe(false); } }); }); it("PDF fits on one page", async () => { const testPdfPath = path.join(process.cwd(), "test-output.pdf"); try { await generatePDF(dungeonData, testPdfPath); const pdfBuffer = await fs.readFile(testPdfPath); const pdfText = pdfBuffer.toString("binary"); const pageCount = (pdfText.match(/\/Type\s*\/Page[^s]/g) || []).length; const expectedPages = dungeonData.map ? 2 : 1; expect(pageCount).toBeLessThanOrEqual(expectedPages); } finally { try { await fs.unlink(testPdfPath); } catch { // Ignore cleanup errors } } }, 60000); });