Files
pitchfast/tests/ai-schemas.test.ts

501 lines
16 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
callScriptSchema,
emailDraftSchema,
emailSubjectSchema,
followUpDraftSchema,
auditSummarySchema,
qualityReviewSchema,
publicAuditTextSchema,
auditClassificationSchema,
internalFindingsSchema,
auditGenerationResultSchema,
type CallScript,
type EmailDraft,
type EmailSubject,
type FollowUpDraft,
type AuditSummary,
type PublicAuditText,
type AuditClassification,
type QualityReview,
type InternalFindings,
type AuditGenerationResult,
} from "../lib/ai/schemas";
test("internal findings schema accepts task-focused evidence", () => {
const parsed = internalFindingsSchema.parse({
findings: [
{
section: "UX",
finding: "Landingpage is not responsive on mobile viewport.",
suggestion: "Add responsive breakpoints for cards and typography.",
},
],
summary: "One high-priority UX gap was found.",
});
assert.equal(parsed.findings.length, 1);
assert.equal(parsed.findings[0].section, "UX");
});
test("audit generation result schema accepts v3 findings and aggregate outreach fields", () => {
const parsed = auditGenerationResultSchema.parse({
findings: [
{
skill_id: "contact-conversion",
observation: "Die Telefonnummer ist mobil erst nach langem Scrollen sichtbar.",
customer_benefit: "Ein sichtbarer Kontaktweg senkt Reibung und erhöht Anfragen.",
public_phrasing:
"Mir ist aufgefallen, dass der Kontaktweg am Smartphone noch schneller erreichbar sein könnte.",
severity: 3,
evidence: "screenshot_mobile",
applies: true,
},
],
usedSkills: ["contact-conversion", "mobile-usability"],
publicAuditText:
"Mir ist aufgefallen, dass der Kontaktweg am Smartphone noch schneller erreichbar sein könnte.",
finalSummary: "Hohe Priorität: mobile Kontaktaufnahme sichtbarer machen.",
emailSubject: "Kurzer Blick auf euren Webauftritt",
emailBody: "Hallo, ich habe mir eure Website angesehen...",
phoneScript: "Ich habe mir kurz eure mobile Kontaktstrecke angesehen.",
ctaType: "anruf",
});
assert.equal(parsed.findings[0].skill_id, "contact-conversion");
assert.equal(parsed.findings[0].severity, 3);
assert.equal(parsed.findings[0].applies, true);
assert.deepEqual(parsed.usedSkills, ["contact-conversion", "mobile-usability"]);
});
test("audit classification schema accepts v3 findings and required used skills", () => {
const parsed = auditClassificationSchema.parse({
findings: [
{
skill_id: "contact-conversion",
observation: "Die Telefonnummer ist mobil erst nach langem Scrollen sichtbar.",
customer_benefit: "Ein sichtbarer Kontaktweg senkt Reibung und erhöht Anfragen.",
public_phrasing:
"Mir ist aufgefallen, dass der Kontaktweg am Smartphone noch schneller erreichbar sein könnte.",
severity: 3,
evidence: "screenshot_mobile",
applies: true,
},
],
summary: "Kontaktaufnahme hat die höchste Priorität.",
usedSkills: ["contact-conversion"],
});
assert.equal(parsed.findings[0].skill_id, "contact-conversion");
assert.deepEqual(parsed.usedSkills, ["contact-conversion"]);
});
test("structured output schemas avoid optional top-level fields for OpenAI strict mode", () => {
const classificationPayload = {
findings: [
{
skill_id: "contact-conversion",
observation: "Die Telefonnummer ist mobil erst nach langem Scrollen sichtbar.",
customer_benefit: "Ein sichtbarer Kontaktweg senkt Reibung und erhöht Anfragen.",
public_phrasing:
"Mir ist aufgefallen, dass der Kontaktweg am Smartphone noch schneller erreichbar sein könnte.",
severity: 3,
evidence: "screenshot_mobile",
applies: true,
},
],
summary: "Kontaktaufnahme hat die höchste Priorität.",
} as const;
assert.throws(
() => auditClassificationSchema.parse(classificationPayload),
/usedSkills|invalid|required/i,
);
assert.equal(
auditClassificationSchema.parse({
...classificationPayload,
usedSkills: null,
}).usedSkills,
null,
);
assert.throws(
() =>
followUpDraftSchema.parse({
message: "Kurzer Follow-up-Hinweis für nächste Woche.",
}),
/followInDays|goals|invalid|required/i,
);
const followParsed = followUpDraftSchema.parse({
message: "Kurzer Follow-up-Hinweis für nächste Woche.",
followInDays: null,
goals: null,
});
assert.equal(followParsed.followInDays, null);
assert.equal(followParsed.goals, null);
assert.throws(
() =>
qualityReviewSchema.parse({
isValid: true,
severity: "ok",
issues: [],
suggestions: [],
rewriteRequired: false,
revisedCopy: null,
}),
/notes|invalid|required/i,
);
assert.equal(
qualityReviewSchema.parse({
isValid: true,
severity: "ok",
issues: [],
suggestions: [],
rewriteRequired: false,
revisedCopy: null,
notes: null,
}).notes,
null,
);
});
test("audit classification schema rejects legacy-only finding payloads", () => {
assert.throws(
() =>
auditClassificationSchema.parse({
findings: [
{
section: "UX",
finding: "Landingpage is not responsive on mobile viewport.",
suggestion: "Add responsive breakpoints for cards and typography.",
},
],
summary: "Legacy payload.",
}),
/invalid|expected|required/i,
);
});
test("v3 finding severity only accepts internal priority levels 1 through 3", () => {
assert.throws(
() =>
auditGenerationResultSchema.parse({
findings: [
{
skill_id: "visual-design",
observation: "Kontrast ist gering.",
customer_benefit: "Bessere Lesbarkeit stärkt den ersten Eindruck.",
public_phrasing: "Ein staerkerer Kontrast wuerde die Lesbarkeit verbessern.",
severity: 4,
evidence: "screenshot_desktop",
applies: true,
},
],
usedSkills: ["visual-design"],
publicAuditText: "Ein staerkerer Kontrast wuerde die Lesbarkeit verbessern.",
finalSummary: "Kontrast priorisieren.",
emailSubject: "Kurzer Website-Hinweis",
emailBody: "Hallo...",
phoneScript: "Kurzer Gespraechseinstieg.",
ctaType: "anruf",
}),
/invalid input/i,
);
});
test("audit generation result schema rejects blank text fields and empty collections", () => {
const validPayload = {
findings: [
{
skill_id: "contact-conversion",
observation: "Die Telefonnummer ist mobil erst nach langem Scrollen sichtbar.",
customer_benefit: "Ein sichtbarer Kontaktweg senkt Reibung und erhöht Anfragen.",
public_phrasing:
"Mir ist aufgefallen, dass der Kontaktweg am Smartphone noch schneller erreichbar sein könnte.",
severity: 2,
evidence: "screenshot_mobile",
applies: true,
},
],
usedSkills: ["contact-conversion"],
publicAuditText:
"Mir ist aufgefallen, dass der Kontaktweg am Smartphone noch schneller erreichbar sein könnte.",
finalSummary: "Mobile Kontaktaufnahme sichtbarer machen.",
emailSubject: "Kurzer Blick auf euren Webauftritt",
emailBody: "Hallo, ich habe mir eure Website angesehen...",
phoneScript: "Ich habe mir kurz eure mobile Kontaktstrecke angesehen.",
ctaType: "termin",
};
assert.throws(
() =>
auditGenerationResultSchema.parse({
...validPayload,
publicAuditText: " ",
}),
/too small|invalid/i,
);
assert.throws(
() =>
auditGenerationResultSchema.parse({
...validPayload,
findings: [],
}),
/too small|invalid/i,
);
assert.throws(
() =>
auditGenerationResultSchema.parse({
...validPayload,
usedSkills: [],
}),
/too small|invalid/i,
);
assert.throws(
() =>
auditGenerationResultSchema.parse({
...validPayload,
findings: [
{
...validPayload.findings[0],
observation: "",
},
],
}),
/too small|invalid/i,
);
});
test("audit generation result schema only accepts documented cta types", () => {
const basePayload = {
findings: [
{
skill_id: "visual-design",
observation: "Die Schrift ist mobil klein.",
customer_benefit: "Lesbare Inhalte halten Besucher laenger auf der Seite.",
public_phrasing: "Die mobile Schrift koennte an einigen Stellen lesbarer sein.",
severity: 1,
evidence: "screenshot_mobile",
applies: true,
},
],
usedSkills: ["visual-design"],
publicAuditText: "Die mobile Schrift koennte an einigen Stellen lesbarer sein.",
finalSummary: "Mobile Lesbarkeit verbessern.",
emailSubject: "Kurzer Website-Hinweis",
emailBody: "Hallo...",
phoneScript: "Kurzer Gespraechseinstieg.",
};
for (const ctaType of ["anruf", "termin", "rueckruf"] as const) {
assert.equal(
auditGenerationResultSchema.parse({
...basePayload,
ctaType,
}).ctaType,
ctaType,
);
}
assert.throws(
() =>
auditGenerationResultSchema.parse({
...basePayload,
ctaType: "angebot",
}),
/invalid/i,
);
});
test("audit summary and public text schemas remain intentionally lightweight", () => {
const summaryParsed = auditSummarySchema.parse({
summary: "Kurze Zusammenfassung mit den wichtigsten Verbesserungen.",
keyFindings: ["Fehlende Kontaktmöglichkeit.", "Langsame Ladezeiten."],
});
const publicParsed = publicAuditTextSchema.parse({
publicText: "Dein Shop ist sauber, aber der erste Eindruck lässt Potenzial erkennen.",
});
assert.equal(summaryParsed.keyFindings.length, 2);
assert.equal(typeof publicParsed.publicText, "string");
});
test("outreach schemas parse German customer-facing payloads", () => {
const emailDraftParsed = emailDraftSchema.parse({
body: "Hallo, ich habe mir euer Angebot angesehen...",
});
const subjectParsed = emailSubjectSchema.parse({
subject: "Kurznotiz zu eurem Webauftritt",
});
const callParsed = callScriptSchema.parse({
openingLine: "Guten Tag, ich bin ...",
callScript: [
"Euer Fokus auf Terminbuchung ist stark.",
"Wie läuft eure aktuelle Lead-Generierung?",
],
closeLine: "Ich schicke im Anschluss kurz die wichtigsten Beobachtungen.",
});
const followParsed = followUpDraftSchema.parse({
message: "Kurzer Follow-up-Hinweis für nächste Woche.",
followInDays: 4,
goals: ["Antwort auf Rückmeldung erhalten"],
});
const qualityParsed = qualityReviewSchema.parse({
isValid: true,
severity: "ok",
issues: [],
suggestions: ["Mehr Kundennutzen konkret beschreiben."],
rewriteRequired: false,
revisedCopy: null,
notes: null,
});
assert.equal(typeof emailDraftParsed.body, "string");
assert.equal(typeof subjectParsed.subject, "string");
assert.equal(Array.isArray(callParsed.callScript), true);
assert.equal(typeof followParsed.message, "string");
assert.equal(Array.isArray(qualityParsed.suggestions), true);
});
test("quality review schema accepts one-shot revised copy payloads", () => {
const parsed: QualityReview = qualityReviewSchema.parse({
isValid: false,
severity: "warning",
issues: ["Betreff klingt noch etwas generisch."],
suggestions: ["Betreff konkreter machen."],
rewriteRequired: true,
revisedCopy: {
publicSummary: "Mir ist aufgefallen, dass die mobile Seite etwas traege wirkt.",
publicBody:
"Mein Vorschlag waere, zuerst die sichtbaren Ladebremsen der Startseite zu pruefen.",
emailSubject: "Kurzer Hinweis zur mobilen Seite",
emailBody:
"Guten Tag, mir ist beim Blick auf Ihre Website aufgefallen, dass die mobile Seite etwas traege wirkt.",
phoneScript: {
openingLine: "Guten Tag, hier ist Matthias Meister.",
callScript: [
"Mir ist bei Ihrer mobilen Website ein konkreter Ladezeitpunkt aufgefallen.",
"Mein Vorschlag waere, diesen Punkt kurz zu priorisieren.",
],
closeLine: "Soll ich Ihnen den Hinweis kurz per E-Mail senden?",
},
followUpDraft: {
message:
"Ich wollte kurz nachfassen, ob der Hinweis zur mobilen Seite fuer Sie relevant ist.",
followInDays: 7,
goals: ["kurze Rueckmeldung", "Interesse klaeren"],
},
},
notes: ["Ein Rewrite ist sinnvoll."],
});
assert.equal(parsed.rewriteRequired, true);
assert.equal(parsed.revisedCopy?.emailSubject, "Kurzer Hinweis zur mobilen Seite");
assert.deepEqual(parsed.revisedCopy?.followUpDraft.goals, [
"kurze Rueckmeldung",
"Interesse klaeren",
]);
});
test("schema-inferred types are exported for Convex action wiring", () => {
const typedFindings: InternalFindings = {
findings: [
{
section: "Homepage",
finding: "No visible Datenschutzhinweis.",
suggestion: "Bitte Hinweis ergänzen.",
},
],
summary: "One finding identified.",
};
const typedSummary: AuditSummary = {
summary: "Kernbefund mit 2 Punkten.",
keyFindings: ["Kontaktseite fehlt."],
};
const typedPublicText: PublicAuditText = {
publicText: "Starker Start, aber optimierungsfähig.",
};
const typedEmail: EmailDraft = {
body: "Text mit Ich-Perspektive und konkretem Vorschlag.",
};
const typedSubject: EmailSubject = {
subject: "Kurzer Betreff",
};
const typedCall: CallScript = {
openingLine: "Hallo, ich habe euer Shop geprüft.",
callScript: ["Wie gehen die Leads aktuell rein?"],
closeLine: "Ich melde mich nach der Rückfrage erneut.",
};
const typedFollowUp: FollowUpDraft = {
message: "Kurzes Follow-up ohne harte Floskel.",
followInDays: null,
goals: null,
};
const typedQuality: QualityReview = {
isValid: true,
severity: "ok",
issues: [],
suggestions: [],
rewriteRequired: false,
revisedCopy: null,
notes: null,
};
const typedAuditGeneration: AuditGenerationResult = {
findings: [
{
skill_id: "visual-design",
observation: "Schrift ist mobil klein.",
customer_benefit: "Lesbare Inhalte halten Besucher laenger auf der Seite.",
public_phrasing: "Die mobile Schrift koennte an einigen Stellen lesbarer sein.",
severity: 2,
evidence: "screenshot_mobile",
applies: true,
},
],
usedSkills: ["visual-design"],
publicAuditText: "Die mobile Schrift koennte an einigen Stellen lesbarer sein.",
finalSummary: "Mobile Lesbarkeit verbessern.",
emailSubject: "Kurzer Website-Hinweis",
emailBody: "Hallo...",
phoneScript: "Kurzer Gespraechseinstieg.",
ctaType: "anruf",
};
const typedClassification: AuditClassification = {
findings: [
{
skill_id: "contact-conversion",
observation: "Kontakt ist mobil spaet sichtbar.",
customer_benefit: "Schneller Kontakt senkt Reibung.",
public_phrasing: "Der Kontaktweg koennte mobil schneller sichtbar sein.",
severity: 2,
evidence: "screenshot_mobile",
applies: true,
},
],
summary: "Kontaktweg priorisieren.",
usedSkills: ["contact-conversion"],
};
assert.equal(typedFindings.findings.length, 1);
assert.equal(typedSummary.keyFindings.length, 1);
assert.equal(typedPublicText.publicText.length > 0, true);
assert.equal(typeof typedEmail.body, "string");
assert.equal(typeof typedSubject.subject, "string");
assert.equal(typedCall.callScript.length, 1);
assert.equal(typedFollowUp.message.length > 0, true);
assert.equal(typedQuality.isValid, true);
assert.equal(typedAuditGeneration.usedSkills.length, 1);
assert.equal(typedClassification.findings.length, 1);
});