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, 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.", 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, issues: [], suggestions: ["Mehr Kundennutzen konkret beschreiben."], 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("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, 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); 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); });