import assert from "node:assert/strict"; import test from "node:test"; import { buildAuditEvidenceInput, type SkillRegistryEntryEvidence, } from "../lib/ai/audit-evidence"; import { LOCAL_AUDIT_SKILL_REGISTRY_SOURCE } from "../lib/ai/local-audit-skill-registry"; import { parseSkillsRegistry } from "../lib/skills-registry"; const SAMPLE_SKILL_REGISTRY: SkillRegistryEntryEvidence[] = [ { name: "Design Audit", purpose: "Designqualität prüfen.", whenToUse: "Nutze diesen Skill, wenn Seitenhierarchie, visuelle Klarheit und visuelle UX bewertet werden sollen.", whenNotToUse: "Nicht für technische Fehlerlisten.", requiredInput: "URL, Seitentypen und Screenshots.", expectedOutput: "Praktische Design-Prioritäten.", category: "design", version: "1.0", source: "skills/design-audit.md", }, { name: "UX Friction Review", purpose: "Nutzerfluss prüfen.", whenToUse: "Nutze diesen Skill bei Formularen, Kontaktwegen und ersten Nutzeraktionen.", whenNotToUse: "Nicht ohne Nutzerfluss.", requiredInput: "URL, Kontaktflüsse, Aktionen.", expectedOutput: "Priorisierte UX-Senkungen.", category: "ux", version: "1.0", source: "skills/ux-friction-review.md", }, { name: "Copy Clarity", purpose: "Textklarheit prüfen.", whenToUse: "Nutze diesen Skill bei unklaren, langen oder abstrakten Website-Texten.", whenNotToUse: "Nicht für technische Datenlisten.", requiredInput: "Textbausteine, Zielgruppe, Tonalität.", expectedOutput: "Klarere Formulierungen.", category: "copy", version: "1.0", source: "skills/copy-clarity.md", }, { name: "Local SEO", purpose: "Lokale Auffindbarkeit prüfen.", whenToUse: "Nutze diesen Skill bei lokaler Relevanz, Impressum, Kontakt- und Google-Nähe.", whenNotToUse: "Nicht ohne lokalen Kontext.", requiredInput: "Ort, Nische, Seitenstruktur.", expectedOutput: "Sichtbarkeits-Verbesserungen.", category: "seo", version: "1.0", source: "skills/local-seo.md", }, { name: "Offer Writing", purpose: "Angebotsstruktur liefern.", whenToUse: "Nutze diesen Skill, wenn ein Angebotsentwurf gebraucht wird.", whenNotToUse: "Nicht bei reinen Buglisten.", requiredInput: "Projektumfang und Umfang.", expectedOutput: "Konkrete Angebotsform.", category: "offer", version: "1.0", source: "skills/offer-writing.md", }, ]; test("buildAuditEvidenceInput sanitizes and caps lead/company context", () => { const actual = buildAuditEvidenceInput({ lead: { companyName: "Bäckerei Muster", niche: "Bäckerei & Kaffeehaus", websiteUrl: "https://example.com/kontakt?ref=ad", address: "Musterstraße 1, 10115 Berlin", city: "Berlin", contactPerson: "Anna Hoffmann", }, crawlPages: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com", pageKind: "homepage", title: "Startseite", }, { sourceUrl: "https://example.com/kontakt", finalUrl: "https://example.com/kontakt", pageKind: "contact", title: "Kontakt", }, { sourceUrl: "https://example.com/kontakt?x=1", finalUrl: "https://example.com/kontakt?x=1", pageKind: "contact", title: "Kontakt", }, ], skillRegistry: SAMPLE_SKILL_REGISTRY, }); assert.equal(actual.companyContext.length > 0, true); assert.equal(actual.companyContext.some((line) => /https?:\/\//.test(line)), false); assert.equal(actual.companyContext.some((line) => /<[^>]+>/.test(line)), false); assert.equal( actual.companyContext.some((line) => line.includes("Bäckerei Muster")), true, ); assert.equal(actual.checkedPages.length >= 2, true); }); test("buildAuditEvidenceInput deduplicates and caps checked pages", () => { const pages = Array.from({ length: 28 }, (_, index) => ({ sourceUrl: `https://example.com/seite-${Math.floor(index / 4)}?cache=${index}`, finalUrl: `https://example.com/seite-${Math.floor(index / 4)}?cache=${index}`, pageKind: index % 2 === 0 ? "other" : "services", title: `Seite ${index}`, })); const actual = buildAuditEvidenceInput({ crawlPages: pages, skillRegistry: SAMPLE_SKILL_REGISTRY, }); const uniqueCount = new Set(actual.checkedPages).size; assert.equal(actual.checkedPages.length, uniqueCount); assert.equal(actual.checkedPages.length <= 8, true); }); test("buildAuditEvidenceInput builds observed UX/content/technical signals and sanitizes long text", () => { const actual = buildAuditEvidenceInput({ crawlPages: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com", pageKind: "homepage", title: "Wir lieben guten Kaffee", hasContactFormSignal: true, hasContactCtaSignal: true, }, { sourceUrl: "https://example.com/ueber-uns", finalUrl: "https://example.com/ueber-uns", pageKind: "about", title: "Über uns", }, ], technicalChecks: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com", usesHttps: false, missingTitle: true, missingMetaDescription: true, hasVisibleContactPath: true, brokenInternalLinkCount: 3, }, ], skillRegistry: SAMPLE_SKILL_REGISTRY, }); assert.equal(actual.observedUxSignals.length > 0, true); assert.equal(actual.observedContentSignals.length > 0, true); assert.equal(actual.observedTechnicalSignals.length > 0, true); assert.equal( actual.observedUxSignals.every((line) => !/https?:\/\//.test(line)), true, ); assert.equal( actual.observedContentSignals.every((line) => !/https?:\/\//.test(line)), true, ); }); test("buildAuditEvidenceInput preserves screenshot references without base64 payloads", () => { const actual = buildAuditEvidenceInput({ screenshots: [ { storageId: "storage-1", sourceUrl: "https://example.com", viewport: "desktop", width: 1200, height: 3000, mimeType: "image/png", capturedAt: 1_700_000_000_000, // builder must ignore any binary-like fields if they exist imageBase64: "iVBORw0KGgoAAAANSUhEUgAAAAUA", }, { storageId: "storage-2", sourceUrl: "https://example.com", viewport: "mobile", width: 390, height: 844, mimeType: "image/png", capturedAt: 1_700_000_001_000, }, ] as const, skillRegistry: SAMPLE_SKILL_REGISTRY, }) as { screenshotReferences: Array< Record & { storageId: string; sourceUrl: string; viewport: string; width: number; height: number; mimeType: string; capturedAt: number; } >; }; assert.equal(actual.screenshotReferences.length, 2); for (const reference of actual.screenshotReferences) { assert.equal(reference.storageId.startsWith("storage-"), true); assert.equal("imageBase64" in reference, false); assert.equal(typeof reference.sourceUrl, "string"); assert.equal(reference.width > 0, true); assert.equal(reference.height > 0, true); } }); test("buildAuditEvidenceInput creates stable evidence ledger refs for source facts", () => { const first = buildAuditEvidenceInput({ crawlPages: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com/", pageKind: "homepage", title: "Startseite", metaDescription: "Bäckerei Muster in Berlin", visibleTextExcerpt: "Bäckerei Muster Berlin mit Kontakt und Öffnungszeiten.", hasContactCtaSignal: true, }, ], technicalChecks: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com/", usesHttps: true, missingMetaDescription: false, hasVisibleContactPath: true, brokenInternalLinkCount: 0, }, ], screenshots: [ { storageId: "storage-home-mobile", sourceUrl: "https://example.com", viewport: "mobile", width: 390, height: 844, mimeType: "image/png", capturedAt: 1_700_000_001_000, }, ], pageSpeedInputs: [ { strategy: "mobile", status: "succeeded", sourceUrl: "https://example.com", normalized: { implications: [ "Die wichtigsten Inhalte erscheinen auf dem Smartphone spürbar verzögert.", ], }, }, ], externalMarkdown: "# Startseite\nBäckerei Muster Berlin. Telefon und Öffnungszeiten sind sichtbar.", skillRegistry: SAMPLE_SKILL_REGISTRY, }); const second = buildAuditEvidenceInput({ ...first, crawlPages: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com/", pageKind: "homepage", title: "Startseite", metaDescription: "Bäckerei Muster in Berlin", visibleTextExcerpt: "Bäckerei Muster Berlin mit Kontakt und Öffnungszeiten.", hasContactCtaSignal: true, }, ], technicalChecks: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com/", usesHttps: true, missingMetaDescription: false, hasVisibleContactPath: true, brokenInternalLinkCount: 0, }, ], screenshots: [ { storageId: "storage-home-mobile", sourceUrl: "https://example.com", viewport: "mobile", width: 390, height: 844, mimeType: "image/png", capturedAt: 1_700_000_001_000, }, ], pageSpeedInputs: [ { strategy: "mobile", status: "succeeded", sourceUrl: "https://example.com", normalized: { implications: [ "Die wichtigsten Inhalte erscheinen auf dem Smartphone spürbar verzögert.", ], }, }, ], externalMarkdown: "# Startseite\nBäckerei Muster Berlin. Telefon und Öffnungszeiten sind sichtbar.", }); assert.deepEqual(first.evidenceLedger, second.evidenceLedger); const evidenceTypes = new Set(first.evidenceLedger.map((entry) => entry.type)); for (const type of [ "crawl_page", "technical_check", "screenshot", "pagespeed", "jina_excerpt", ] as const) { assert.equal(evidenceTypes.has(type), true, `${type} evidence should exist.`); } assert.equal( first.evidenceLedger.every((entry) => entry.id.includes("unknown") === false), true, "Evidence IDs should be stable source refs, not unknown placeholders.", ); }); test("buildAuditEvidenceInput converts PageSpeed implications into sanitized customer-facing text", () => { const actual = buildAuditEvidenceInput({ pageSpeedInputs: [ { strategy: "mobile", status: "succeeded", sourceUrl: "https://example.com", normalized: { implications: [ "Score 0.42: Erster Inhalt liegt deutlich hinter Standards.", "Die Seite zeigt das wichtigste Bild zu langsam.", "Weitere Infos: https://example.com/psi", "{ \"pagespeed\": 0.92, \"score\": 88 }", ], }, }, { strategy: "desktop", status: "failed", sourceUrl: "https://example.com", errorType: "api_error", errorSummary: "Score 0.22: timeout", }, ], skillRegistry: SAMPLE_SKILL_REGISTRY, }); assert.equal(actual.pageSpeedCustomerImplications.length >= 1, true); assert.equal( actual.pageSpeedCustomerImplications.includes( "Score 0.42: Erster Inhalt liegt deutlich hinter Standards.", ), false, ); assert.equal( actual.pageSpeedCustomerImplications.every( (line) => !/https?:\/\/|pagespeed|score|lighthouse|raw storage|rawStorage/i.test(line), ), true, ); assert.equal(actual.pageSpeedCustomerImplications.length <= 8, true); }); test("buildAuditEvidenceInput selects deterministic skills and supports design/ux/copy/seo", () => { const input = { lead: { companyName: "Bäckerei Muster", niche: "Bäckerei", city: "Berlin", websiteDomain: "example.com", }, crawlPages: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com", pageKind: "homepage", title: "Willkommen bei Bäckerei Muster", hasContactFormSignal: true, hasContactCtaSignal: true, }, ], technicalChecks: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com", usesHttps: false, missingMetaDescription: true, missingTitle: false, hasVisibleContactPath: true, brokenInternalLinkCount: 1, }, ], screenshots: [ { storageId: "storage-1", sourceUrl: "https://example.com", viewport: "desktop", width: 1200, height: 3000, mimeType: "image/png", capturedAt: 1700000000000, }, ], skillRegistry: SAMPLE_SKILL_REGISTRY, }; const first = buildAuditEvidenceInput(input); const second = buildAuditEvidenceInput({ ...input, skillRegistry: [...SAMPLE_SKILL_REGISTRY].reverse(), }); assert.equal(first.selectedSkills.length >= 4, true); assert.equal(first.selectedSkills.length, second.selectedSkills.length); assert.equal( first.selectedSkills.every((skill, index) => { const same = second.selectedSkills[index]; return same?.name === skill.name && same?.category === skill.category; }), true, ); const expectedCategories: Array< "design" | "ux" | "copy" | "seo" > = ["design", "ux", "copy", "seo"]; const selectedCategories = new Set(first.selectedSkills.map((skill) => skill.category)); for (const category of expectedCategories) { assert.equal(selectedCategories.has(category), true); } }); test("buildAuditEvidenceInput prioritizes local-audit v3 skills before cap", () => { const skillRegistry = parseSkillsRegistry(LOCAL_AUDIT_SKILL_REGISTRY_SOURCE); assert.equal( skillRegistry.some((skill) => skill.id === "visual-design" && !skill.category), true, ); const actual = buildAuditEvidenceInput({ lead: { companyName: "Bäckerei Muster", niche: "Bäckerei", city: "Berlin", websiteDomain: "example.com", }, crawlPages: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com", pageKind: "homepage", title: "Bäckerei Muster Berlin", visibleTextExcerpt: "Frische Backwaren in Berlin. Rufen Sie uns an oder schreiben Sie uns fuer eine Bestellung.", hasContactCtaSignal: true, }, { sourceUrl: "https://example.com/kontakt", finalUrl: "https://example.com/kontakt", pageKind: "contact", title: "Kontakt", visibleTextExcerpt: "Telefon 030 123456, E-Mail hallo@example.com, Öffnungszeiten und Kontaktformular.", hasContactFormSignal: true, hasContactCtaSignal: true, }, ], technicalChecks: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com", usesHttps: true, missingMetaDescription: true, hasVisibleContactPath: true, }, ], screenshots: [ { storageId: "desktop-storage", sourceUrl: "https://example.com", viewport: "desktop", width: 1280, height: 900, mimeType: "image/png", capturedAt: 1700000000000, }, { storageId: "mobile-storage", sourceUrl: "https://example.com", viewport: "mobile", width: 390, height: 844, mimeType: "image/png", capturedAt: 1700000001000, }, ], pageSpeedInputs: [ { strategy: "mobile", status: "succeeded", sourceUrl: "https://example.com", normalized: { implications: [ "Die wichtigsten Inhalte erscheinen auf dem Smartphone spürbar verzögert.", ], }, }, ], skillRegistry, }); const selectedIds = new Set(actual.selectedSkills.map((skill) => skill.id)); assert.deepEqual(actual.selectedSkills.map((skill) => skill.id), [ "visual-design", "impeccable-critique", "contact-conversion", "local-seo-basics", "performance-experience", "mobile-usability", ]); assert.equal(actual.selectedSkills.length, 6); for (const id of [ "visual-design", "impeccable-critique", "contact-conversion", "local-seo-basics", "performance-experience", ]) { assert.equal(selectedIds.has(id), true, `${id} should be inside the cap.`); } assert.equal( actual.selectedSkills.every((skill) => skill.category === undefined), true, ); }); test("buildAuditEvidenceInput gates v3 skills when declared inputs are missing", () => { const skillRegistry = parseSkillsRegistry(LOCAL_AUDIT_SKILL_REGISTRY_SOURCE); const actual = buildAuditEvidenceInput({ lead: { companyName: "Bäckerei Muster", websiteDomain: "example.com", }, crawlPages: [ { sourceUrl: "https://example.com", finalUrl: "https://example.com", pageKind: "homepage", title: "Bäckerei Muster", }, ], screenshots: [ { storageId: "desktop-storage", sourceUrl: "https://example.com", viewport: "desktop", width: 1280, height: 900, mimeType: "image/png", capturedAt: 1700000000000, }, ], skillRegistry, }); const selectedIds = new Set(actual.selectedSkills.map((skill) => skill.id)); for (const id of [ "visual-design", "impeccable-critique", "first-impression-clarity", "contact-conversion", "mobile-usability", "conversion-copy", "performance-experience", ]) { assert.equal(selectedIds.has(id), false, `${id} should require missing inputs.`); } assert.equal(selectedIds.has("accessibility-basics"), true); });