Files
webdev-pipeline/tests/german-copy-guard.test.ts

377 lines
16 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
validateCallScriptCopy,
validateCustomerFacingCopy,
validateFollowUpCopy,
} from "../lib/ai/german-copy-guard";
const validPayload = {
auditSummary:
"Ich habe Ihren Webauftritt geprüft. Mir ist aufgefallen, dass die Kontaktseite nicht klar erreichbar ist. Ich empfehle, den Kontaktbereich im Header sichtbar zu platzieren.",
auditBody:
"Mir ist aufgefallen, dass die Kontaktseite nur am Ende der Startseite eingebettet ist. Ich empfehle, sie im Kopfbereich direkt zu platzieren.",
emailSubject: "Kurzer Website-Hinweis",
emailBody:
"Guten Tag, auf Ihrer Kontaktseite ist die Telefonnummer gut sichtbar, der mobile Kontaktbutton liegt aber erst weiter unten. Das kostet Besuchern unterwegs einen extra Schritt, gerade wenn sie schnell anrufen oder einen Termin anfragen wollen. Ich wollte Ihnen das kurz spiegeln, weil es mit wenig Aufwand klarer wirken kann. Mehr braucht es dafür wahrscheinlich nicht. Soll ich Ihnen die zwei Punkte kurz schicken?",
callScript: {
openingLine: "Hallo, ich bin Matthias von der Webberatung.",
callScript: [
"Ich habe eure Website geprüft und gesehen, dass der Kontaktbereich nicht sofort sichtbar ist.",
"Ich schlage vor, den Kontakt-Button in den Header zu setzen und die Mobil-Ansicht anzupassen.",
],
closeLine: "Wenn das hilfreich klingt, kann ich euch in zwei Minuten die nächsten Schritte skizzieren.",
},
followUp:
"Mir ist noch etwas aufgefallen: Auf der Mobilversion fehlt ein klarer Termin- oder Kontakthinweis. Ich schlage vor, diesen Bereich oberhalb der Leistungstexte deutlich zu markieren.",
};
test("validateCustomerFacingCopy passes clean German outreach and audit copy", () => {
const result = validateCustomerFacingCopy(validPayload);
assert.equal(result.passed, true);
assert.equal(result.issues.length, 0);
});
test("validateCustomerFacingCopy passes a natural short formal first-contact email", () => {
const result = validateCustomerFacingCopy({
...validPayload,
emailSubject: "Kurz zur Kontaktseite",
emailBody:
"Guten Tag, Ihre Telefonnummer ist auf der Kontaktseite gut auffindbar. Auf dem Handy rutscht der direkte Kontaktweg aber recht weit nach unten, sodass Besucher erst suchen müssen, bevor sie anrufen oder schreiben können. Ich wollte Ihnen das kurz zurückmelden, weil es ein kleiner Hebel für mehr Anfragen sein kann. Es geht nicht um einen großen Umbau. Soll ich Ihnen die Stelle kurz als Screenshot schicken?",
});
assert.equal(result.passed, true);
assert.deepEqual(result.issues, []);
});
test("validateCustomerFacingCopy rejects likely non-German copy and reports language", () => {
const result = validateCustomerFacingCopy({
...validPayload,
emailBody:
"Your site looks very strong, and your performance score is 0.82 with good Lighthouse numbers.",
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some((issue) =>
issue.field === "emailBody" && issue.rule === "not_german",
),
true,
);
});
test("validateCustomerFacingCopy flags short English artifact-like snippets in content fields", () => {
const shortInputs: Array<{
field: "auditSummary" | "auditBody" | "emailBody" | "followUp";
value: string;
}> = [
{ field: "emailBody", value: "quick audit" },
{ field: "auditBody", value: "bad website" },
{ field: "followUp", value: "AI report" },
];
for (const { field, value } of shortInputs) {
const payload = { ...validPayload, [field]: value };
const result = validateCustomerFacingCopy(payload as typeof validPayload);
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) => issue.field === field && issue.rule === "not_german",
),
true,
`Expected ${field} short snippet "${value}" to fail german language check.`,
);
}
});
test("validateCustomerFacingCopy requires Ich-form in applicable public audit and follow-up fields", () => {
const result = validateCustomerFacingCopy({
...validPayload,
auditBody:
"Ihre Seite hat eine gute Struktur. Der Kontaktbereich sollte klarer werden.",
emailBody:
"Guten Tag, Ihre Kontaktseite ist schon klar aufgebaut. Auf dem Handy liegt der direkte Kontaktweg aber recht weit unten, sodass Besucher erst suchen müssen, bevor sie anrufen oder schreiben können. Das ist vermutlich schnell zu beheben und würde den Einstieg einfacher machen. Soll ich Ihnen die konkrete Stelle kurz schicken?",
followUp: "Die Website sollte verbessert werden. Setzt bitte einen Kontaktbutton.",
});
const hasAuditIssue = result.issues.some(
(issue) => issue.field === "auditBody" && issue.rule === "missing_ich_form",
);
const hasFollowUpIssue = result.issues.some(
(issue) => issue.field === "followUp" && issue.rule === "missing_ich_form",
);
assert.equal(result.passed, false);
assert.equal(hasAuditIssue, true);
assert.equal(hasFollowUpIssue, true);
});
test("validateCustomerFacingCopy blocks PageSpeed-like score artifacts in public text", () => {
const result = validateCustomerFacingCopy({
...validPayload,
auditSummary:
"Aus dem PageSpeed-Check ergibt sich ein score: 0.82 im Bereich Performance.",
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) =>
issue.field === "auditSummary" &&
issue.rule === "pagespeed_score_artifact",
),
true,
);
});
test("validateCustomerFacingCopy blocks price/currency mention", () => {
const result = validateCustomerFacingCopy({
...validPayload,
callScript: {
...validPayload.callScript,
callScript: [
"Der Kontaktpunkt ist gut sichtbar.",
"Ihr Paket kostet nur 99 € pro Monat.",
"Ich habe den Kontaktpunkt geprüft und schlage vor, ihn in der Headerzeile zu fixieren.",
],
},
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) => issue.field === "callScript.callScript[1]" && issue.rule === "price_mention",
),
true,
);
});
test("validateCustomerFacingCopy rejects generic AI slop language", () => {
const result = validateCustomerFacingCopy({
...validPayload,
emailBody:
"Unsere maßgeschneiderte, nahtlose, innovative Lösung hebt Ihre Sichtbarkeit auf ein neues Level und ist wirklich disruptive.",
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) =>
issue.field === "emailBody" && issue.rule === "generic_ai_slop",
),
true,
);
});
test("validateCustomerFacingCopy flags accusatory tone", () => {
const result = validateCustomerFacingCopy({
...validPayload,
auditBody:
"Ihre Website ist katastrophal und wirkt absolut unprofessionell. Das sollte dringend geändert werden.",
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) => issue.field === "auditBody" && issue.rule === "hostile_tone",
),
true,
);
});
test("validateCustomerFacingCopy strips technical artifacts like model ids and raw JSON", () => {
const result = validateCustomerFacingCopy({
...validPayload,
followUp:
'Ich habe folgende Diagnose: {"score": 0.8, "lighthouseResult": "ok", "storageId": "rawstorageid_abc123"}',
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) =>
issue.field === "followUp" &&
issue.rule === "raw_technical_artifact",
),
true,
);
});
test("validateCustomerFacingCopy enforces observation + suggestion style", () => {
const result = validateCustomerFacingCopy({
...validPayload,
auditBody:
"Ihre Website wirkt freundlich und klar.",
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) =>
issue.field === "auditBody" &&
issue.rule === "missing_observation_or_suggestion",
),
true,
);
});
test("validateCustomerFacingCopy blocks formulaic observed-and-suggested email copy", () => {
const result = validateCustomerFacingCopy({
...validPayload,
emailBody:
"Guten Tag, ich habe beobachtet, dass Ihre Website klare Kontaktinformationen bietet. Ich schlage vor, diese Sichtbarkeit auf allen Seiten beizubehalten. Ich habe beobachtet, dass außerdem die Ladezeiten verbesserungswürdig sind. Ich schlage vor, technische Maßnahmen umzusetzen, damit die Nutzererfahrung nachhaltig verbessert wird. Soll ich Ihnen dazu mehr Informationen senden?",
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) =>
issue.field === "emailBody" &&
issue.rule === "formulaic_email_tone",
),
true,
);
});
test("validateCustomerFacingCopy blocks long mini-audit outreach emails", () => {
const result = validateCustomerFacingCopy({
...validPayload,
emailBody:
"Guten Tag, auf Ihrer Website sind Adresse und Telefonnummer gut sichtbar. Außerdem fehlt eine aussagekräftige Meta-Beschreibung. Zudem sind die Ladezeiten auf mobilen Geräten verbesserungswürdig. Ein weiterer Punkt ist die Nutzerführung mit Call-to-Action-Elementen. Schließlich könnten lokale Vertrauenssignale wie Bewertungen ergänzt werden. Ich empfehle, diese Maßnahmen umzusetzen, um die Conversion-Rate zu steigern, Absprungraten zu senken und Ihr Ranking positiv zu beeinflussen. Soll ich Ihnen eine ausführliche Analyse schicken?",
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) =>
issue.field === "emailBody" &&
(issue.rule === "email_reads_like_mini_audit" ||
issue.rule === "brochure_email_language"),
),
true,
);
});
test("validateCustomerFacingCopy blocks inflated outreach subject lines", () => {
const result = validateCustomerFacingCopy({
...validPayload,
emailSubject:
"Optimierungspotenziale für Ihre Website: Mehr Sichtbarkeit und bessere Nutzererfahrung",
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) =>
issue.field === "emailSubject" &&
issue.rule === "unnatural_email_subject",
),
true,
);
});
test("validateCustomerFacingCopy blocks old live audit copy that reads like generated outreach", () => {
const result = validateCustomerFacingCopy({
auditSummary:
"Ich habe beobachtet, dass die Website von Diehl & Pape Rechtsanwälte zwar durch ihre klare Spezialisierung und umfassenden Kontaktinformationen überzeugt, jedoch durch langsame Ladezeiten und sichtbare Inhaltsverschiebungen beim Laden an Nutzerkomfort verliert. Ich schlage vor, gezielt die Ladegeschwindigkeit zu optimieren und das Seitenlayout stabil zu gestalten, um das Vertrauen potenzieller Mandanten zu stärken und die Nutzerbindung nachhaltig zu erhöhen.",
auditBody:
"Ich habe die Website von Diehl & Pape Rechtsanwälte genau betrachtet und festgestellt, dass die langsamen Ladezeiten und die sichtbaren Inhaltsverschiebungen beim Laden den ersten Eindruck deutlich beeinträchtigen. Mir ist aufgefallen, wie wichtig gerade für eine erfahrene Kanzlei mit klarer Spezialisierung ein reibungsloses Nutzererlebnis ist, um Vertrauen bei potenziellen Mandanten aufzubauen. Deshalb schlage ich vor, gezielt die Ladegeschwindigkeit zu optimieren und die Stabilität des Seitenlayouts zu verbessern.",
emailSubject:
"Ich habe beobachtet, dass die Website von Diehl & Pape Rechtsanwälte durch langsame Ladezeiten und sichtbare Inhaltsverschiebungen die Nutzererfahrung beeinträchtigt.",
emailBody:
"Ich habe die Website von Diehl & Pape Rechtsanwälte genau unter die Lupe genommen und festgestellt, dass die langsamen Ladezeiten und die sichtbaren Inhaltsverschiebungen beim Laden den ersten Eindruck deutlich trüben. Mein konkreter Vorschlag: Eine gezielte Optimierung der Ladegeschwindigkeit und eine Stabilisierung des Seitenlayouts könnten die Nutzerzufriedenheit erheblich steigern.",
callScript: {
openingLine:
"Ich habe die Website von Diehl & Pape Rechtsanwälte genau unter die Lupe genommen und dabei ein wichtiges Verbesserungspotenzial entdeckt.",
callScript: [
"Mir ist aufgefallen, dass die Seite beim Laden deutlich sichtbare Inhaltsverschiebungen zeigt.",
"Ich schlage vor, gezielt die Ladegeschwindigkeit zu optimieren und die Stabilität des Seitenlayouts zu verbessern.",
],
closeLine:
"Lassen Sie uns gemeinsam diese technischen Hürden beseitigen und Ihre Website zu einem überzeugenden Aushängeschild Ihrer Expertise machen.",
},
followUp:
"Ich habe beobachtet, dass die Website von Diehl & Pape Rechtsanwälte durch langsame Ladezeiten an Nutzerkomfort verliert. Mein konkreter Vorschlag ist, die Ladegeschwindigkeit gezielt zu optimieren und die Stabilität des Seitenlayouts sicherzustellen.",
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) =>
(issue.field === "emailSubject" &&
issue.rule === "unnatural_email_subject") ||
(issue.field === "emailBody" &&
issue.rule === "formulaic_email_tone"),
),
true,
);
});
test("validateCustomerFacingCopy is permissive for phone numbers and date values", () => {
const result = validateCustomerFacingCopy({
auditSummary:
"Ich habe gesehen, dass eure Kontaktseite am 12.02.2026 aktualisiert wurde. Ich empfehle, den Kontaktbereich als Nächstes im Header zu verbessern.",
auditBody:
"Mir ist aufgefallen, dass die Telefonnummer 0201 123456 in der Fußzeile steht. Ich empfehle, sie zusätzlich im Header zu platzieren.",
emailSubject: "Kurz zum Terminplan",
emailBody:
"Guten Tag, auf Ihrer Seite ist der Termin am 12. Oktober schon erwähnt. Auf dem Handy steht diese Info aber recht weit unten, sodass Besucher sie leicht übersehen können, wenn sie nur schnell nach Öffnungszeiten oder Kontakt suchen. Ich wollte Ihnen das kurz zurückmelden, weil die Stelle ohne großen Umbau klarer werden kann. Mehr als eine kleine Umplatzierung braucht es vermutlich nicht. Soll ich Ihnen den Ausschnitt kurz schicken?",
callScript: {
openingLine:
"Hallo, ich bin Matthias und ich habe eure Seite geprüft.",
callScript: [
"Ich habe auf eurer Seite gesehen, dass der Kontaktbutton erst sehr weit unten erscheint.",
"Mir ist aufgefallen, dass hier noch eine kleine Verbesserung fehlt; ich schlage vor, den Kontaktbereich nach oben zu ziehen.",
],
closeLine: "Dann nehme ich das Thema in den nächsten Schritt mit auf.",
},
followUp:
"Mir ist am 12. Oktober aufgefallen, dass die Telefonnummer 030 1234567 schon gut auffindbar ist; ich schlage vor, eine kleine Sichtbarkeitsanpassung vorzunehmen.",
});
assert.equal(result.passed, true);
});
test("validateCallScriptCopy validates each script line individually and returns field paths", () => {
const result = validateCallScriptCopy({
openingLine: "Hallo, ich bin Matthias.",
callScript: [
"{" +
'"score": 0.82, "rawstorageid":"abc123"' +
"}",
"Ich habe auf der Seite gesehen, dass der Kontaktbutton fehlt.",
"Mir fehlt noch ein konkreter Verbesserungsschritt.",
],
closeLine: "Schöne Grüße",
});
assert.equal(result.passed, false);
assert.equal(
result.issues.some(
(issue) =>
issue.field === "callScript.callScript[0]" &&
issue.rule === "raw_technical_artifact",
),
true,
);
});
test("validateFollowUpCopy enforces ich-form and guard output shape", () => {
const result = validateFollowUpCopy({
message: "Hier ist der Inhalt für das Follow-up.",
});
assert.equal(result.passed, false);
assert.equal(result.issues.length > 0, true);
assert.equal(
result.issues.some(
(issue) =>
issue.field === "followUp" && issue.rule === "missing_ich_form",
),
true,
);
});