import { test } from "node:test"; import assert from "node:assert"; import { generateDungeon } from "../dungeonGenerator.js"; import { generatePDF } from "../generatePDF.js"; import fs from "fs/promises"; import path from "path"; const OLLAMA_API_URL = process.env.OLLAMA_API_URL; test("Integration tests", { skip: !OLLAMA_API_URL }, async (t) => { let dungeonData; await t.test("Generate dungeon", async () => { dungeonData = await generateDungeon(); assert(dungeonData, "Dungeon data should be generated"); }); await t.test("Title is 2-4 words, no colons", () => { assert(dungeonData.title, "Title should exist"); const words = dungeonData.title.split(/\s+/); assert(words.length >= 2 && words.length <= 4, `Title should be 2-4 words, got ${words.length}: "${dungeonData.title}"`); assert(!dungeonData.title.includes(":"), `Title should not contain colons: "${dungeonData.title}"`); }); await t.test("Flavor text is ≤60 words", () => { assert(dungeonData.flavor, "Flavor text should exist"); const words = dungeonData.flavor.split(/\s+/); assert(words.length <= 60, `Flavor text should be ≤60 words, got ${words.length}`); }); await t.test("Hooks have no title prefixes", () => { assert(dungeonData.hooksRumors, "Hooks should exist"); dungeonData.hooksRumors.forEach((hook, i) => { assert(!hook.match(/^[^:]+:\s/), `Hook ${i + 1} should not have title prefix: "${hook}"`); }); }); await t.test("Exactly 6 random events", () => { assert(dungeonData.randomEvents, "Random events should exist"); assert.strictEqual(dungeonData.randomEvents.length, 6, `Should have exactly 6 random events, got ${dungeonData.randomEvents.length}`); }); await t.test("Encounter details don't include encounter name", () => { assert(dungeonData.encounters, "Encounters should exist"); dungeonData.encounters.forEach((encounter) => { if (encounter.details) { const detailsLower = encounter.details.toLowerCase(); const nameLower = encounter.name.toLowerCase(); assert(!detailsLower.startsWith(nameLower), `Encounter "${encounter.name}" details should not start with encounter name: "${encounter.details}"`); } }); }); await t.test("Treasure uses em-dash format, no 'description' text", () => { assert(dungeonData.treasure, "Treasure should exist"); dungeonData.treasure.forEach((item, i) => { if (typeof item === "object" && item.description) { assert(!item.description.toLowerCase().startsWith("description"), `Treasure ${i + 1} description should not start with 'description': "${item.description}"`); } }); }); await t.test("NPCs have no 'description' text", () => { assert(dungeonData.npcs, "NPCs should exist"); dungeonData.npcs.forEach((npc, i) => { if (npc.trait) { assert(!npc.trait.toLowerCase().startsWith("description"), `NPC ${i + 1} trait should not start with 'description': "${npc.trait}"`); } }); }); await t.test("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); // Check PDF page count by counting "%%EOF" markers (rough estimate) const pdfText = pdfBuffer.toString("binary"); const pageCount = (pdfText.match(/\/Type\s*\/Page[^s]/g) || []).length; // Should be 1 page for content, or 2 if map exists const expectedPages = dungeonData.map ? 2 : 1; assert(pageCount <= expectedPages, `PDF should have ≤${expectedPages} page(s), got ${pageCount}`); } finally { try { await fs.unlink(testPdfPath); } catch { // Ignore cleanup errors } } }); });