import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; import test from "node:test"; const outreachPagePath = join( process.cwd(), "app", "dashboard", "outreach", "page.tsx", ); const outreachWorkspacePath = join( process.cwd(), "components", "outreach", "outreach-review-workspace.tsx", ); function extractConstFunction(source: string, name: string) { const declaration = new RegExp(`const ${name}\\s*=\\s*(?:async\\s+)?\\(`); const match = source.match(declaration); assert.ok(match, `${name} handler should exist.`); const start = match.index ?? -1; assert.ok(start >= 0, `${name} handler should exist.`); const firstBrace = source.indexOf("{", start); assert.ok(firstBrace >= 0, `${name} handler should have a body.`); let depth = 0; for (let index = firstBrace; index < source.length; index += 1) { const char = source[index]; if (char === "{") { depth += 1; } if (char === "}") { depth -= 1; if (depth === 0) { return source.slice(start, index + 1); } } } assert.fail(`${name} handler body should close.`); } test("/dashboard/outreach mounts the outreach review workspace", async () => { const source = await readFile(outreachPagePath, "utf8"); assert.doesNotMatch(source, /DashboardPlaceholderPage/); assert.match(source, /OutreachReviewWorkspace/); assert.match( source, /@\/components\/outreach\/outreach-review-workspace/, ); }); test("OutreachReviewWorkspace uses the review workspace API and required controls", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); assert.match(source, /api\.outreach(?:\s+as\s+OutreachApi\))?\.listReviewWorkspace/); assert.match(source, /limit:\s*100/); assert.match(source, /api\.outreach(?:\s+as\s+OutreachApi\))?\.saveReviewDraft/); assert.match(source, /api\.outreach(?:\s+as\s+OutreachApi\))?\.approveEmailDraft/); assert.match(source, /api\.audits\.savePublicAuditContent/); assert.match(source, /api\.audits\.publishPublicAudit/); [ "Lead-Details", "Kontaktquellen", "Prioritätsgrund", "Kontaktstrategie", "Audit-Zusammenfassung", "Public-Audit", "Verwendete Skills", "Quellen anzeigen", "Raw anzeigen", "E-Mail-Betreff", "E-Mail-Text", "Telefon-Skript", "Follow-up-Draft", ].forEach((label) => assert.match(source, new RegExp(label))); }); test("OutreachReviewWorkspace renders a compact queue with one selected detail editor", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); assert.match(source, /selectedRecordId/); assert.match(source, /selectedRecord/); assert.match(source, /Details prüfen/); assert.match(source, /Review-Queue/); assert.match(source, /reviewStatusFilters/); assert.match(source, /setActiveFilter/); assert.match(source, /Bereit zum Versand/); assert.match(source, /Mail offen/); assert.match(source, /role="status"/); assert.match(source, /aria-pressed=\{selectedRecord\?\.id === record\.id\}/); }); test("OutreachReviewWorkspace keeps exactly one recommended email subject and body editor", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); assert.equal((source.match(/aria-label="E-Mail-Betreff"/g) ?? []).length, 1); assert.equal((source.match(/aria-label="E-Mail-Text"/g) ?? []).length, 1); assert.equal((source.match(/ { const source = await readFile(outreachWorkspacePath, "utf8"); assert.match(source, /Audit veröffentlichen/); assert.match(source, /Änderungen speichern/); assert.match(source, /E-Mail freigeben und senden/); assert.match(source, /useAction/); assert.match(source, /outreachSendAction[\s\S]*sendApprovedEmail/); const auditPublishIndex = source.indexOf("Audit veröffentlichen"); const auditSaveIndex = source.indexOf("Änderungen speichern"); const emailApprovalIndex = source.indexOf("E-Mail freigeben und senden"); assert.ok(auditPublishIndex >= 0); assert.ok(auditSaveIndex >= 0); assert.ok(emailApprovalIndex >= 0); assert.ok( Math.abs(auditPublishIndex - auditSaveIndex) < Math.abs(auditPublishIndex - emailApprovalIndex), "Audit actions should be grouped closer to each other than to email approval.", ); }); test("OutreachReviewWorkspace disables draft/approval/final controls for queued send", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); assert.match(source, /outreach\.sendStatus\s*===\s*"queued"/); assert.match(source, /const isQueuedSend = outreach\?\.sendStatus === "queued"/); assert.match( source, /disabled=\{[\s\S]*busyAction === `\$?\{record\.id\}:outreach-save`[\s\S]*\|\|\s*isQueuedSend[\s\S]*\}/, "Outreach save control should be disabled while outreach is queued.", ); assert.match( source, /disabled=\{[\s\S]*busyAction === `\$?\{record\.id\}:email-approval`[\s\S]*\|\|\s*isQueuedSend[\s\S]*\}/, "Email approval control should be disabled while outreach is queued.", ); assert.match( source, /disabled=\{[\s\S]*busyAction === `\$?\{pendingEmailConfirmation\.id\}:email-send`[\s\S]*\|\|\s*isQueuedSendForConfirmation[\s\S]*\}/, "Final send control should be disabled while confirmed outreach is queued.", ); }); test("OutreachReviewWorkspace prevents draft mutation handlers for queued outreach", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); const saveOutreachHandler = extractConstFunction(source, "saveOutreach"); const approveEmailHandler = extractConstFunction(source, "approveEmail"); assert.match(saveOutreachHandler, /outreach\.sendStatus\s*===\s*"queued"/); assert.match(saveOutreachHandler, /[aA]ufgrund des laufenden Sendevorgangs/); assert.match(approveEmailHandler, /outreach\.sendStatus\s*===\s*"queued"/); assert.match(approveEmailHandler, /[aA]ufgrund des laufenden Sendevorgangs/); }); test("OutreachReviewWorkspace prevents final send when confirmed outreach is queued", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); const sendHandler = extractConstFunction(source, "sendApprovedEmailFromConfirmation"); assert.match( sendHandler, /const isQueuedSend = rows\.some\(/, "Final send handler should check current list records for queued status before sending.", ); assert.match( sendHandler, /\.sendStatus\s*===\s*"queued"/, "Final send handler should guard queued status.", ); assert.match( sendHandler, /setNotice\(\s*"(?:.*(?:[aA]ufgrund[^"\r\n]*Sendevorgang|.*bereits[^"\r\n]*Vorgang|.*bereits[^"\r\n]*im Gange)[^"]*)"/, ); }); test("OutreachReviewWorkspace useAction receives a typed send action ref", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); assert.match(source, /useAction\(api\.outreachSendAction\.sendApprovedEmail\)/); assert.doesNotMatch( source, /as \{\s*outreachSendAction:\s*\{\s*sendApprovedEmail:\s*unknown/, ); }); test("OutreachReviewWorkspace includes final confirmation UI fields", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); assert.match(source, /Dialog/); assert.match(source, /Empfänger/); assert.match(source, /Betreff/); assert.match(source, /Absender/); assert.match(source, /Audit-Link/); assert.match(source, /sender/); assert.doesNotMatch(source, /Konfigurierter SMTP-Absender/); }); test("approveEmail opens confirmation after save and approval", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); const handler = extractConstFunction(source, "approveEmail"); const draftIndex = handler.indexOf("const draft = drafts[record.id] ?? getDraft(record)"); const saveIndex = handler.indexOf("await saveReviewDraft"); const approveIndex = handler.indexOf("await approveEmailDraft"); const confirmationIndex = handler.indexOf("setPendingEmailConfirmation"); assert.ok(draftIndex >= 0, "Approval should read the current local draft."); assert.ok(saveIndex >= 0, "Approval should persist the current draft first."); assert.ok(approveIndex >= 0, "Approval should still call approveEmailDraft."); assert.ok(confirmationIndex >= 0, "Approval should open a confirmation dialog."); assert.ok(draftIndex < saveIndex, "Approval should read draft before save."); assert.ok(saveIndex < approveIndex, "Approval should save before approve."); assert.ok(approveIndex < confirmationIndex, "Confirmation should open after approval."); assert.equal( /sendApprovedEmail/.test(handler), false, "Approval should not call sendApprovedEmail.", ); assert.match(handler, /emailSubject:\s*draft\.emailSubject/); assert.match(handler, /emailBody:\s*draft\.emailBody/); assert.match(handler, /followUpDraft:\s*draft\.followUpDraft/); assert.match( handler, /approvalData\.sender/, ); assert.ok( !/approvalData\.sender\s*\?\?\s*SMTP_SENDER_PLACEHOLDER/.test(handler), "Sender should come from approvalResult without placeholder fallback.", ); }); test("OutreachReviewWorkspace gates phone scripts to call-first or missing-contact leads with phone numbers", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); assert.match(source, /hasCallablePhone/); assert.match(source, /strategy\s*===\s*"call_first"/); assert.match(source, /lead\?\.contactStatus\s*===\s*"missing_contact"/); assert.match(source, /Kein Telefon-Skript erforderlich/); }); test("approveEmail saves the visible outreach draft before approving it", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); const handler = extractConstFunction(source, "approveEmail"); const draftIndex = handler.indexOf("const draft = drafts[record.id] ?? getDraft(record)"); const saveIndex = handler.indexOf("await saveReviewDraft"); const approveIndex = handler.indexOf("await approveEmailDraft"); assert.ok(draftIndex >= 0, "Approval should read the current local draft."); assert.ok(saveIndex >= 0, "Approval should persist the current draft first."); assert.ok(approveIndex >= 0, "Approval should still call approveEmailDraft."); assert.ok( draftIndex < saveIndex && saveIndex < approveIndex, "Approval should read draft, save it, then approve.", ); assert.match(handler, /emailSubject:\s*draft\.emailSubject/); assert.match(handler, /emailBody:\s*draft\.emailBody/); assert.match(handler, /followUpDraft:\s*draft\.followUpDraft/); }); test("final email send handler calls sendApprovedEmail with outreach id", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); const handler = extractConstFunction(source, "sendApprovedEmailFromConfirmation"); assert.match(handler, /await sendApprovedEmail\(\{\s*id:\s*confirmation\.id/); assert.ok( /await sendApprovedEmail\([\s\S]*id:\s*confirmation\.id[\s\S]*\}/.test(handler), "Final handler should pass the currently confirmed outreach id.", ); assert.ok( handler.indexOf("E-Mail gesendet.") >= 0, "Final send should show a success notice.", ); assert.ok( /Retry|erneut|nochmal|nicht versendet|nicht gesendet/.test(handler), "Final send should surface a retry-oriented failure notice.", ); assert.equal( handler.indexOf("setPendingEmailConfirmation(null)") >= 0, true, "Final handler should clear confirmation on success.", ); }); test("canceling confirmation does not send", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); const handler = extractConstFunction(source, "closeEmailConfirmation"); assert.ok(handler.includes("setPendingEmailConfirmation(null)")); assert.equal(/sendApprovedEmail/.test(handler), false); }); test("publishAudit saves the visible audit draft before publishing it", async () => { const source = await readFile(outreachWorkspacePath, "utf8"); const handler = extractConstFunction(source, "publishAudit"); const draftIndex = handler.indexOf("const draft = drafts[record.id] ?? getDraft(record)"); const saveIndex = handler.indexOf("await savePublicAuditContent"); const publishIndex = handler.indexOf("await publishPublicAudit"); assert.ok(draftIndex >= 0, "Publishing should read the current local audit draft."); assert.ok(saveIndex >= 0, "Publishing should save the public audit content first."); assert.ok(publishIndex >= 0, "Publishing should still call publishPublicAudit."); assert.ok( draftIndex < saveIndex && saveIndex < publishIndex, "Publishing should read draft, save it, then publish.", ); assert.match(handler, /publicSummary:\s*draft\.auditSummary/); assert.match(handler, /publicBody:\s*draft\.auditBody/); });