import { test, before, after } from "node:test"; import assert from "node:assert/strict"; import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { fileURLToPath } from "node:url"; import { createAppServer } from "../server/index.js"; import { WHACK_CARDS } from "../server/whack_cards.js"; import { buildNanoBananaPrompt } from "../server/nano_banana.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PUBLIC_WHACK_DIR = path.resolve(__dirname, "..", "public", "whack_pack"); let server; let baseUrl; let tmpDir; before(async () => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sparkvault-test-")); const dbPath = path.join(tmpDir, "test-ideas.db"); const mockupsDir = path.join(tmpDir, "mockups"); const app = createAppServer({ dbPath, mockupsDir }); server = app.server; await new Promise((resolve) => { server.listen(0, "127.0.0.1", () => { const addr = server.address(); baseUrl = `http://127.0.0.1:${addr.port}`; resolve(); }); }); }); after(async () => { if (server) { await new Promise((resolve) => server.close(resolve)); } }); test("All 64 Creative Whack Pack cards are indexed and have local JPG scans in public/whack_pack", async () => { assert.equal(WHACK_CARDS.length, 64, "Should have exactly 64 Creative Whack Pack cards"); const suitsCount = { explorer: 0, artist: 0, judge: 0, warrior: 0 }; for (const card of WHACK_CARDS) { suitsCount[card.suit]++; const diskPath = path.join(PUBLIC_WHACK_DIR, card.file); assert.ok(fs.existsSync(diskPath), `Missing card scan on disk: ${card.file}`); assert.ok(card.promptQuestion.length > 10, `Card #${card.number} should have a guiding promptQuestion`); } assert.deepEqual(suitsCount, { explorer: 16, artist: 16, judge: 16, warrior: 16 }); const res = await fetch(`${baseUrl}/api/whack-cards`); assert.equal(res.status, 200); const data = await res.json(); assert.equal(data.cards.length, 64); }); test("Quick Capture parses inline #tags, @category (@tshirt), and !high priority boost", async () => { const res = await fetch(`${baseUrl}/api/ideas`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: "Retro Red Rocks Amphitheatre Tee @tshirt #colorado #screenprint !high" }) }); assert.equal(res.status, 201); const { idea } = await res.json(); assert.equal(idea.title, "Retro Red Rocks Amphitheatre Tee"); assert.equal(idea.category, "creative"); assert.equal(idea.status, "spark"); assert.equal(idea.rating_excitement, 5); assert.ok(idea.tags.includes("tshirt")); assert.ok(idea.tags.includes("colorado")); assert.ok(idea.tags.includes("screenprint")); }); test("Creative Whack Pack Draw, 4-Role Spread, Idea Collider, and Logging a Whack reflection work", async () => { // 1. 4-Role Spread const spreadRes = await fetch(`${baseUrl}/api/whack-cards/draw?mode=spread`); assert.equal(spreadRes.status, 200); const spreadData = await spreadRes.json(); assert.equal(spreadData.cards.length, 4); assert.deepEqual( spreadData.cards.map((c) => c.suit), ["explorer", "artist", "judge", "warrior"] ); // 2. Idea Collider const colliderRes = await fetch(`${baseUrl}/api/whack-cards/draw?mode=collider&suit=artist`); assert.equal(colliderRes.status, 200); const colliderData = await colliderRes.json(); assert.equal(colliderData.card.suit, "artist"); assert.ok(colliderData.idea && colliderData.idea.id); // 3. Log a Whack breakthrough onto the collided idea const targetId = colliderData.idea.id; const whackRes = await fetch(`${baseUrl}/api/ideas/${targetId}/whack`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cardNumber: 27, reflection: "Reverse the color scheme: print cream ink onto a heavyweight forest green shirt.", addChecklistItem: true }) }); assert.equal(whackRes.status, 200); const whackUpdated = await whackRes.json(); assert.equal(whackUpdated.idea.whack_history[0].cardNumber, 27); assert.equal(whackUpdated.idea.whack_history[0].cardTitle, "Reverse"); assert.ok( whackUpdated.idea.checklist.some((c) => c.text.includes("[Card #27 Reverse]")), "Should append reflection to checklist when addChecklistItem is true" ); }); test("Visual Mockup Studio saves canvas T-shirt mockups and builds Nano Banana 2.1 prompts", async () => { // Verify Nano Banana prompt builder for Left-Chest T-Shirt + Whack Card const prompt = buildNanoBananaPrompt({ rawPrompt: "Minimalist Alpine Ibex emblem in burnt orange and gold", ideaTitle: "Ibex Mountain Tee", template: "tshirt-left-chest", garmentColorName: "Vintage Cream", garmentHex: "#F5E8D3", whackCard: WHACK_CARDS[21] // #22 Exaggerate }); assert.ok(prompt.includes("5-inch left-chest")); assert.ok(prompt.includes("Vintage Cream")); assert.ok(prompt.includes("Exaggerate")); // Save a 1x1 PNG data URL via /api/ideas/1/mockups/canvas const samplePngDataUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; const saveRes = await fetch(`${baseUrl}/api/ideas/1/mockups/canvas`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: "Left-Chest Cream Proof", template: "tshirt-left-chest", garmentColor: "#F5E8D3", garmentColorName: "Vintage Cream", placement: "left-chest", imageDataUrl: samplePngDataUrl }) }); assert.equal(saveRes.status, 201); const saveData = await saveRes.json(); assert.equal(saveData.mockup.placement, "left-chest"); assert.equal(saveData.idea.cover_mockup_url, saveData.mockup.imageUrl); // Verify the saved mockup image is served over HTTP const imgRes = await fetch(`${baseUrl}${saveData.mockup.imageUrl}`); assert.equal(imgRes.status, 200); assert.equal(imgRes.headers.get("content-type"), "image/png"); }); test("Head-to-Head Arena and Triage Review endpoints update arena_wins and composite_score", async () => { const revRes = await fetch(`${baseUrl}/api/review/next`); assert.equal(revRes.status, 200); const revData = await revRes.json(); assert.ok(revData.triageQueue.length >= 2); assert.equal(revData.arenaPair.length, 2); const [first, second] = revData.arenaPair; const beforeWins = first.arena_wins; const arenaRes = await fetch(`${baseUrl}/api/review/arena`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ winnerId: first.id, loserId: second.id }) }); assert.equal(arenaRes.status, 200); const arenaData = await arenaRes.json(); assert.equal(arenaData.winner.arena_wins, beforeWins + 1); }); test("External notes and images can be attached to a spark and promoted to Mockup Studio", async () => { const samplePngDataUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; const attRes = await fetch(`${baseUrl}/api/ideas/1/attachments`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: "Vintage Ski Patch Photo", textContent: "Found this 1978 Chamonix felt patch as color reference.", sourceUrl: "https://example.com/chamonix-patch", imageDataUrl: samplePngDataUrl, appendToNotes: true, setAsCover: true }) }); assert.equal(attRes.status, 201); const attData = await attRes.json(); assert.equal(attData.attachment.title, "Vintage Ski Patch Photo"); assert.equal(attData.attachment.type, "image"); assert.ok(attData.idea.notes_markdown.includes("1978 Chamonix felt patch")); assert.ok(attData.idea.links.some((l) => l.url === "https://example.com/chamonix-patch")); // Promote attachment to Mockup Studio const promRes = await fetch( `${baseUrl}/api/ideas/1/attachments/${attData.attachment.id}/promote-mockup`, { method: "POST" } ); assert.equal(promRes.status, 201); const promData = await promRes.json(); assert.equal(promData.idea.mockups[0].title, "Vintage Ski Patch Photo"); }); test("Personalized Random Idea Generator creates a tailored idea from background + Whack Pack card", async () => { const res = await fetch(`${baseUrl}/api/ideas/generate-random`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ category: "creative" }) }); assert.equal(res.status, 201); const data = await res.json(); assert.ok(data.idea && data.idea.id); assert.ok(data.idea.title.length > 5); assert.ok(data.card && data.card.number >= 1 && data.card.number <= 64); assert.equal(data.idea.inspired_by_card, data.card.number); assert.ok(data.idea.whack_history.length >= 1); }); test("Suggest Names endpoint returns 6 creative name and tagline suggestions for an idea", async () => { const res = await fetch(`${baseUrl}/api/ideas/1/suggest-names`, { method: "POST", headers: { "Content-Type": "application/json" } }); assert.equal(res.status, 200); const data = await res.json(); assert.ok(Array.isArray(data.suggestions)); assert.equal(data.suggestions.length, 6); assert.ok(data.suggestions[0].name); assert.ok(data.suggestions[0].tagline); });