import { test } from "node:test"; import assert from "node:assert/strict"; import { openDb } from "../src/db.js"; import { createProduct } from "../src/domain/product.js"; import { setFact } from "../src/domain/facts.js"; import { registerAsset, decide } from "../src/domain/assets.js"; import { confirmedDims, planBlueprintViews, resolveViewRefs, resolveBlueprintAiRefs, composedOverlaySvg, buildBlueprintViewPrompt, buildBlueprintAiPrompt, aiSheetSvg, } from "../src/domain/blueprint.js"; function freshDb() { return openDb(":memory:"); } function addSource(db: ReturnType, productId: string, name: string, note?: string) { const id = `src_${name.replace(/\W/g, "")}`; db.prepare( `INSERT INTO source_assets (id, product_id, role, sha256, mime, bytes, original_name, path, note, created_at) VALUES (?, ?, 'product_reference', ?, 'image/jpeg', 1, ?, ?, ?, ?)` ).run(id, productId, `sha_${id}`, name, `x/${id}.jpg`, note ?? null, new Date().toISOString()); return id; } const OSKAR_DIMS = { "dimensions.width_cm": 257, "dimensions.depth_cm": 151, "dimensions.height_with_cushions_cm": 88, "dimensions.height_without_cushions_cm": 77, "dimensions.body_depth_cm": 104, "dimensions.seat_height_cm": 46, "dimensions.seat_depth_cm": 58, "dimensions.armrest_width_cm": 30, "legs.height_cm": 4.5, "sleeping.width_cm": 135, "sleeping.length_cm": 190, "cushions.loose_back_count": 5, "sides.photographed": "P", "mechanism.sleep": "seat_pullout_backfold", }; test("confirmedDims: tylko confirmed; bodyDepth, cushionCount i side są czytane", () => { const db = freshDb(); const p = createProduct(db, { slug: "x", name: "X", kind: "corner-sofa" }); for (const [k, v] of Object.entries(OSKAR_DIMS)) { setFact(db, p.id, { key: k, value: v, status: "confirmed", sourceKind: "owner", sourceRef: "t" }); } setFact(db, p.id, { key: "dimensions.chaise_width_cm", value: 130, status: "unverified", sourceKind: "owner" }); const dims = confirmedDims(db, p.id); assert.equal(dims.width, 257); assert.equal(dims.bodyDepth, 104); assert.equal(dims.bodyHeight, null); assert.equal(dims.cushionCount, 5); assert.equal(dims.side, "P"); assert.equal(dims.chaiseWidth, null); // unverified nie wchodzi }); test("planBlueprintViews: panel spania tylko przy potwierdzonej powierzchni", () => { const db = freshDb(); const p = createProduct(db, { slug: "x", name: "X", kind: "sofa" }); setFact(db, p.id, { key: "dimensions.width_cm", value: 200, status: "confirmed", sourceKind: "owner", sourceRef: "t" }); assert.deepEqual(planBlueprintViews(confirmedDims(db, p.id)), ["top", "front"]); setFact(db, p.id, { key: "sleeping.width_cm", value: 135, status: "confirmed", sourceKind: "owner", sourceRef: "t" }); // width bez length → nadal brak panelu assert.deepEqual(planBlueprintViews(confirmedDims(db, p.id)), ["top", "front"]); setFact(db, p.id, { key: "sleeping.length_cm", value: 190, status: "confirmed", sourceKind: "owner", sourceRef: "t" }); assert.deepEqual(planBlueprintViews(confirmedDims(db, p.id)), ["top", "front", "sleep"]); }); test("resolveViewRefs: zatwierdzone assety galerii, potem dopasowane źródła, dedup", () => { const db = freshDb(); const p = createProduct(db, { slug: "x", name: "X", kind: "corner-sofa" }); const mech = addSource(db, p.id, "oskar-rozlozony-oparcie-zlozone.jpg"); addSource(db, p.id, "p-6403.jpg", "zdjęcie hali"); const sleepAsset = registerAsset(db, { productId: p.id, kind: "image", role: "sleep", sha256: "s1", mime: "image/jpeg", bytes: 1, path: "a/s.jpg", origin: "ai_generated", }); decide(db, { productId: p.id, scope: "shot", target: "sleep", assetId: sleepAsset.id, decision: "approved", decidedBy: "u" }); const refs = resolveViewRefs(db, p.id, "sleep"); assert.equal(refs[0], sleepAsset.id); // zatwierdzona galeria najpierw assert.ok(refs.includes(mech)); // dopasowane źródło mechanizmu assert.equal(new Set(refs).size, refs.length); // dedup // kandydat (niezatwierdzony) nie wchodzi do referencji const frontRefs = resolveViewRefs(db, p.id, "front"); assert.ok(!frontRefs.includes(sleepAsset.id)); }); test("composedOverlaySvg: wymiary tylko z confirmed, kotwiczone do placedRect", () => { const db = freshDb(); const p = createProduct(db, { slug: "x", name: "X", kind: "corner-sofa" }); for (const [k, v] of Object.entries(OSKAR_DIMS)) { setFact(db, p.id, { key: k, value: v, status: "confirmed", sourceKind: "owner", sourceRef: "t" }); } const dims = confirmedDims(db, p.id); const svg = composedOverlaySvg(dims, { top: { x: 150, y: 185, w: 560, h: 330 }, front: { x: 820, y: 185, w: 660, h: 360 }, sleep: { x: 150, y: 690, w: 560, h: 330 }, }); for (const label of ["257 cm", "151 cm", "88 cm", "77 cm", "46 cm", "30 cm", "190 cm", "135 cm"]) { assert.ok(svg.includes(label), `brak wymiaru ${label}`); } assert.ok(svg.includes("gł. siedziska 58 cm")); assert.ok(svg.includes("nogi 4,5 cm")); // brak łańcucha podziału — seatSectionWidth/chaiseWidth niepotwierdzone assert.ok(!svg.includes("130 cm")); }); test("buildBlueprintViewPrompt: cechy z faktów, mechanizm z enuma", () => { const ctx = { productName: "Narożnik rozkładany OSKAR", kind: "corner-sofa", side: "P" as const, cushionCount: 5, mechanism: "seat_pullout_backfold", }; const front = buildBlueprintViewPrompt("front", ctx); assert.ok(front.includes("5 loose back cushions")); assert.ok(front.includes("orthographic front elevation")); const top = buildBlueprintViewPrompt("top", ctx); assert.ok(top.includes("right")); // szezlong po prawej dla P const sleep = buildBlueprintViewPrompt("sleep", ctx); assert.ok(sleep.includes("backrest folds down flat")); // nieznany mechanizm → generycznie, bez wymyślania const generic = buildBlueprintViewPrompt("sleep", { ...ctx, mechanism: null }); assert.ok(generic.includes("as shown in the reference")); }); test("buildBlueprintAiPrompt: etykiety verbatim z faktów, jawna liczba, inset spania warunkowo", () => { const db = freshDb(); const p = createProduct(db, { slug: "x", name: "X", kind: "corner-sofa" }); for (const [k, v] of Object.entries(OSKAR_DIMS)) { setFact(db, p.id, { key: k, value: v, status: "confirmed", sourceKind: "owner", sourceRef: "t" }); } const dims = confirmedDims(db, p.id); const ctx = { productName: "X", kind: "corner-sofa", side: dims.side, cushionCount: dims.cushionCount, mechanism: "seat_pullout_backfold", }; const prompt = buildBlueprintAiPrompt(dims, ctx); // wszystkie potwierdzone wymiary jako cytowane literały (polski przecinek) for (const l of ['"257 cm"', '"151 cm"', '"88 cm"', '"77 cm"', '"46 cm"', '"58 cm"', '"30 cm"', '"4,5 cm"', '"190 cm"', '"135 cm"']) { assert.ok(prompt.includes(l), `brak etykiety ${l}`); } // 8 wymiarów mebla + 2 etykiety insetu spania = 10 assert.ok(prompt.includes("Exactly 10 dimension labels")); // inset spania tylko gdy oba wymiary potwierdzone assert.ok(prompt.includes("sleeping surface")); // bodyHeight (104) celowo NIE wchodzi do promptu — tylko tabelka assert.ok(!prompt.includes('"104 cm"')); // niepotwierdzone opcjonalne nie wchodzą assert.ok(!prompt.includes('"130 cm"')); // forma i poduszki z faktów/kind assert.ok(prompt.includes("5 loose back cushions")); assert.ok(prompt.includes("right")); // szezlong P // produkt nierozkładany → brak insetu, liczba spada const promptNoSleep = buildBlueprintAiPrompt({ ...dims, sleepWidth: null, sleepLength: null }, ctx); assert.ok(promptNoSleep.includes("Exactly 8 dimension labels")); assert.ok(!promptNoSleep.includes("sleeping surface")); }); test("aiSheetSvg: tabelka programowa z głębokością korpusu, napisy klienckie", () => { const db = freshDb(); const p = createProduct(db, { slug: "x", name: "X", kind: "corner-sofa" }); for (const [k, v] of Object.entries(OSKAR_DIMS)) { setFact(db, p.id, { key: k, value: v, status: "confirmed", sourceKind: "owner", sourceRef: "t" }); } const svg = aiSheetSvg(confirmedDims(db, p.id), { productName: "X", fabricLabel: "TILIA 86", date: "2026-09-15", }); assert.ok(svg.includes("104 cm")); // korpus w tabelce assert.ok(svg.includes("4,5 cm")); // nogi assert.ok(svg.includes("prawa (P)")); // dokument kliencki — bez wzmianek o AI i fraz wewnętrznych assert.ok(!svg.includes("AI")); assert.ok(!svg.includes("faktów potwierdzonych")); assert.ok(!svg.includes("unieważniany")); assert.ok(svg.includes("Ilustracja poglądowa")); }); test("resolveBlueprintAiRefs: zatwierdzony packshot_34 pierwszy, fallback do źródeł", () => { const db = freshDb(); const p = createProduct(db, { slug: "x", name: "X", kind: "corner-sofa" }); const src = addSource(db, p.id, "p-6403.jpg"); const a34 = registerAsset(db, { productId: p.id, kind: "image", role: "packshot_34", sha256: "s34", mime: "image/jpeg", bytes: 1, path: "a/34.jpg", origin: "ai_generated", }); decide(db, { productId: p.id, scope: "shot", target: "packshot_34", assetId: a34.id, decision: "approved", decidedBy: "u" }); const refs = resolveBlueprintAiRefs(db, p.id); assert.equal(refs[0], a34.id); assert.ok(refs.includes(src)); // fallback źródła });