Fix audit generation and enrichment fallback

This commit is contained in:
2026-06-07 23:03:57 +02:00
parent e9463e8ef2
commit 470fb0f348
10 changed files with 2190 additions and 138 deletions

View File

@@ -8,15 +8,19 @@ import {
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", () => {
@@ -35,6 +39,270 @@ test("internal findings schema accepts task-focused evidence", () => {
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,
issues: [],
suggestions: [],
}),
/notes|invalid|required/i,
);
assert.equal(
qualityReviewSchema.parse({
isValid: true,
issues: [],
suggestions: [],
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.",
@@ -72,6 +340,7 @@ test("outreach schemas parse German customer-facing payloads", () => {
isValid: true,
issues: [],
suggestions: ["Mehr Kundennutzen konkret beschreiben."],
notes: null,
});
assert.equal(typeof emailDraftParsed.body, "string");
@@ -118,12 +387,52 @@ test("schema-inferred types are exported for Convex action wiring", () => {
const typedFollowUp: FollowUpDraft = {
message: "Kurzes Follow-up ohne harte Floskel.",
followInDays: null,
goals: null,
};
const typedQuality: QualityReview = {
isValid: true,
issues: [],
suggestions: [],
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);
@@ -134,4 +443,6 @@ test("schema-inferred types are exported for Convex action wiring", () => {
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);
});