338 lines
10 KiB
TypeScript
338 lines
10 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
buildAuditEvidenceInput,
|
|
type SkillRegistryEntryEvidence,
|
|
} from "../lib/ai/audit-evidence";
|
|
|
|
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);
|
|
}
|
|
});
|