Fix audit generation and enrichment fallback
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user