Files
webdev-pipeline/tests/audit-evidence.test.ts

497 lines
15 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import test from "node:test";
import {
buildAuditEvidenceInput,
type SkillRegistryEntryEvidence,
} from "../lib/ai/audit-evidence";
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 <strong>Muster</strong>",
niche: "Bäckerei & Kaffeehaus",
websiteUrl: "https://example.com/kontakt?ref=ad",
address: "Musterstraße 1, 10115 Berlin",
city: "Berlin",
contactPerson: "<b>Anna</b> 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<string, unknown> & {
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 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 source = readFileSync(
join(process.cwd(), "v2_elemente", "skills.md"),
"utf8",
);
const skillRegistry = parseSkillsRegistry(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",
"contact-conversion",
"local-seo-basics",
"performance-experience",
"mobile-usability",
"conversion-copy",
]);
assert.equal(actual.selectedSkills.length, 6);
for (const id of [
"visual-design",
"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 source = readFileSync(
join(process.cwd(), "v2_elemente", "skills.md"),
"utf8",
);
const skillRegistry = parseSkillsRegistry(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",
"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);
});