import assert from "node:assert/strict"; import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import test from "node:test"; import ts from "typescript"; const outreachPath = join(process.cwd(), "convex", "outreach.ts"); const schemaPath = join(process.cwd(), "convex", "schema.ts"); const actionPath = join(process.cwd(), "convex", "outreachSendAction.ts"); const outreachSource = existsSync(outreachPath) ? readFileSync(outreachPath, "utf8") : ""; const schemaSource = existsSync(schemaPath) ? readFileSync(schemaPath, "utf8") : ""; const outreachSendActionSource = existsSync(actionPath) ? readFileSync(actionPath, "utf8") : ""; const sourceFile = ts.createSourceFile( "outreach.ts", outreachSource, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TS, ); const actionSourceFile = ts.createSourceFile( "outreachSendAction.ts", outreachSendActionSource, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TS, ); function getExportedConstNames(file: ts.SourceFile) { const names = new Set(); const visit = (node: ts.Node) => { if (ts.isVariableStatement(node)) { const isExported = node.modifiers?.some( (mod) => mod.kind === ts.SyntaxKind.ExportKeyword, ); const isConst = Boolean(node.declarationList.flags & ts.NodeFlags.Const); if (isExported && isConst) { for (const declaration of node.declarationList.declarations) { if (ts.isIdentifier(declaration.name)) { names.add(declaration.name.text); } } } } ts.forEachChild(node, visit); }; ts.forEachChild(file, visit); return names; } function extractExportSource(name: string) { const marker = `export const ${name} = `; const declarationIndex = outreachSource.indexOf(marker); assert.notEqual(declarationIndex, -1, `Expected declaration for ${name}.`); const openBraceIndex = outreachSource.indexOf("{", declarationIndex); let depth = 0; let end = -1; for (let index = openBraceIndex; index < outreachSource.length; index += 1) { const char = outreachSource[index]; if (char === "{") { depth += 1; } else if (char === "}") { depth -= 1; if (depth === 0) { end = index; break; } } } assert.notEqual(end, -1, `Expected balanced braces for ${name}.`); return outreachSource.slice(openBraceIndex, end + 1); } function extractTableSource(tableName: string) { const marker = `${tableName}: defineTable({`; const start = schemaSource.indexOf(marker); if (start === -1) { return ""; } const openBraceIndex = schemaSource.indexOf("{", start + marker.length - 1); let depth = 0; let end = -1; for (let index = openBraceIndex; index < schemaSource.length; index += 1) { const char = schemaSource[index]; if (char === "{") { depth += 1; } else if (char === "}") { depth -= 1; if (depth === 0) { end = index; break; } } } if (end === -1) { return ""; } return schemaSource.slice(openBraceIndex, end + 1); } function hasPattern(source: string, pattern: RegExp, message: string) { assert.equal(pattern.test(source), true, message); } function lacksPattern(source: string, pattern: RegExp, message: string) { assert.equal(pattern.test(source), false, message); } test("outreach review module exports authenticated review contracts", () => { assert.equal(existsSync(outreachPath), true, "outreach.ts should be present."); const exports = getExportedConstNames(sourceFile); for (const exportName of [ "listReviewWorkspace", "saveReviewDraft", "approveEmailDraft", ]) { assert.equal(exports.has(exportName), true, `Expected export: ${exportName}`); } hasPattern( outreachSource, /const requireOperator\s*=\s*async\s*\(\s*ctx:\s*(?:QueryCtx\s*\|\s*MutationCtx|MutationCtx\s*\|\s*QueryCtx)\s*\)/, "Module should define a local requireOperator helper usable from queries and mutations.", ); hasPattern( outreachSource, /ctx\.auth\.getUserIdentity\(\)/, "requireOperator should derive the operator identity from Convex auth.", ); hasPattern( outreachSource, /Nicht autorisiert/, "Unauthenticated review calls should fail clearly.", ); }); test("outreach record inserts never spread args directly", () => { const createSource = extractExportSource("create"); const upsertSource = extractExportSource("upsertFromAuditGeneration"); hasPattern( createSource, /buildOutreachRecordsInsertPayload/, "create should delegate outreachRecords insert payload construction to a local helper.", ); hasPattern( upsertSource, /buildOutreachRecordsInsertPayload/, "upsertFromAuditGeneration should delegate outreachRecords insert payload construction to a local helper.", ); lacksPattern(createSource, /\.\.\.args/, "create should not spread raw args into db insert payloads."); lacksPattern( createSource, /ctx\.db\.insert\(\s*"outreachRecords"[\s\S]*\.\.\./, "create should build explicit insert object fields.", ); lacksPattern(upsertSource, /\.\.\.args/, "upsertFromAuditGeneration should not spread raw args into db insert payloads."); lacksPattern( upsertSource, /ctx\.db\.insert\(\s*"outreachRecords"[\s\S]*\.\.\.args/, "upsertFromAuditGeneration should build explicit insert object fields.", ); }); test("outreach record payload builder keeps optional fields explicit", () => { hasPattern( outreachSource, /const buildOutreachRecordsInsertPayload = /, "create/upsert should use a dedicated insert payload helper.", ); hasPattern( outreachSource, /if \(args\.auditId !== undefined\) \{[\s\S]*auditId:/, "Insert payload should include optional auditId only when it is set.", ); hasPattern( outreachSource, /if \(args\.phoneScript !== undefined\) \{[\s\S]*phoneScript:/, "Insert payload should include optional phoneScript only when it is set.", ); hasPattern( outreachSource, /if \(args\.emailSubject !== undefined\) \{[\s\S]*emailSubject:/, "Insert payload should include optional emailSubject only when it is set.", ); hasPattern( outreachSource, /if \(args\.emailBody !== undefined\) \{[\s\S]*emailBody:/, "Insert payload should include optional emailBody only when it is set.", ); hasPattern( outreachSource, /if \(args\.followUpDraft !== undefined\) \{[\s\S]*followUpDraft:/, "Insert payload should include optional followUpDraft only when it is set.", ); }); test("listReviewWorkspace is bounded, authenticated, and joins review context", () => { const listSource = extractExportSource("listReviewWorkspace"); const reviewSource = `${listSource}\n${outreachSource}`; hasPattern(outreachSource, /export const listReviewWorkspace = query\(/, "Review workspace should be a public query."); hasPattern(listSource, /requireOperator\(ctx\)/, "Review workspace should require auth."); hasPattern(listSource, /limit:\s*v\.optional\(v\.number\(\)\)/, "Review workspace should accept optional limit."); hasPattern(listSource, /normalizeListLimit\(args\.limit\)/, "Review workspace should normalize the requested limit."); lacksPattern(listSource, /\.collect\(/, "Review workspace must not use unbounded collect()."); hasPattern( listSource, /query\("leads"\)[\s\S]*?withIndex\("by_contactStatus_and_updatedAt"[\s\S]*?eq\("contactStatus",\s*"outreach_ready"\)[\s\S]*?\.order\("desc"\)[\s\S]*?\.take\(candidateLimit\)/, "Review workspace should include newest outreach-ready leads via contactStatus+updatedAt.", ); for (const [approvalStatus, sendStatus] of [ ["draft", "not_sent"], ["draft", "queued"], ["draft", "failed"], ["approved", "not_sent"], ["approved", "queued"], ["approved", "failed"], ]) { hasPattern( listSource, new RegExp( `query\\("outreachRecords"\\)[\\s\\S]*?withIndex\\("by_approvalStatus_and_sendStatus_and_updatedAt"[\\s\\S]*?eq\\("approvalStatus",\\s*"${approvalStatus}"\\)[\\s\\S]*?eq\\("sendStatus",\\s*"${sendStatus}"\\)[\\s\\S]*?\\.order\\("desc"\\)[\\s\\S]*?\\.take\\(candidateLimit\\)`, ), `Review workspace should fetch newest ${approvalStatus}/${sendStatus} outreach via combined eligibility+updatedAt index.`, ); } lacksPattern( listSource, /withIndex\("by_approvalStatus_and_updatedAt"/, "Review workspace should not depend on approval-only bounded windows for outreach eligibility.", ); lacksPattern( listSource, /withIndex\("by_sendStatus_and_updatedAt"/, "Review workspace should not depend on send-only bounded windows for outreach eligibility.", ); hasPattern( listSource, /\.\.\.draftNotSentOutreach[\s\S]*\.\.\.draftQueuedOutreach[\s\S]*\.\.\.draftFailedOutreach[\s\S]*\.\.\.approvedNotSentOutreach[\s\S]*\.\.\.approvedQueuedOutreach[\s\S]*\.\.\.approvedFailedOutreach/, "Review workspace should combine only eligible approval/send-status candidate windows.", ); hasPattern( listSource, /approvalStatus\s*===\s*"draft"[\s\S]*?approvalStatus\s*===\s*"approved"/, "Review workspace should include draft and approved unsent outreach records.", ); hasPattern( listSource, /sendStatus\s*!==\s*"sent"/, "Review workspace should exclude sent outreach records.", ); hasPattern( listSource, /sort\(\(\s*a,\s*b\s*\)\s*=>\s*b\.sortAt\s*-\s*a\.sortAt\s*\)/, "Review rows should be newest first.", ); hasPattern(listSource, /slice\(0,\s*limit\)/, "Review rows should be capped to the normalized limit."); for (const tableName of [ "audits", "auditGenerations", "pageSpeedResults", "websiteCrawlPages", "websiteEmailCandidates", ]) { hasPattern( reviewSource, new RegExp(`query\\("${tableName}"\\)[\\s\\S]*?\\.take\\(\\s*\\d+\\s*\\)`), `${tableName} join should be bounded with take(n).`, ); } for (const fieldName of [ "lead", "latestOutreach", "audit", "auditGenerations", "usedSkills", "skillSummaries", "sourceSummaries", "pageSpeedResults", "crawlPages", "emailCandidates", ]) { hasPattern( reviewSource, new RegExp(`${fieldName}:`), `Review rows should include ${fieldName}.`, ); } }); test("schema defines recency indexes for outreach review bounded reads", () => { assert.equal(existsSync(schemaPath), true, "schema.ts should be present."); for (const [indexName, fieldsPattern] of [ ["by_contactStatus_and_updatedAt", String.raw`\[\s*"contactStatus",\s*"updatedAt",?\s*\]`], ["by_approvalStatus_and_updatedAt", String.raw`\[\s*"approvalStatus",\s*"updatedAt",?\s*\]`], ["by_sendStatus_and_updatedAt", String.raw`\[\s*"sendStatus",\s*"updatedAt",?\s*\]`], [ "by_approvalStatus_and_sendStatus_and_updatedAt", String.raw`\[\s*"approvalStatus",\s*"sendStatus",\s*"updatedAt",?\s*\]`, ], ]) { hasPattern( schemaSource, new RegExp(`\\.index\\("${indexName}",\\s*${fieldsPattern}`), `Schema should define ${indexName} for newest-first bounded review reads.`, ); } }); test("upsertFromAuditGeneration preserves review boundaries for generated copy", () => { const upsertSource = extractExportSource("upsertFromAuditGeneration"); hasPattern( outreachSource, /export const upsertFromAuditGeneration = internalMutation\(/, "upsertFromAuditGeneration should remain an internal mutation.", ); hasPattern(upsertSource, /ctx\.db\.get\(args\.leadId\)/, "upsert should verify the lead exists."); hasPattern(upsertSource, /!lead/, "upsert should reject missing leads."); hasPattern(upsertSource, /ctx\.db\.get\(args\.auditId\)/, "upsert should load provided audits."); hasPattern(upsertSource, /!audit/, "upsert should reject missing audits."); hasPattern( upsertSource, /audit\.leadId\s*!==\s*args\.leadId/, "upsert should reject auditId values that belong to a different lead.", ); hasPattern( upsertSource, /current\.sendStatus\s*===\s*"sent"[\s\S]*?buildOutreachRecordsInsertPayload/, "upsert should create a new draft record instead of patching a sent outreach record.", ); hasPattern( upsertSource, /ctx\.db\.patch\(current\._id,[\s\S]*approvalStatus:\s*"draft"/, "Generated copy changes should reset existing unsent outreach records to draft.", ); hasPattern( outreachSource, /buildOutreachRecordsInsertPayload[\s\S]*approvalStatus:\s*"draft"[\s\S]*sendStatus:\s*"not_sent"/, "New generated outreach records should start as unsent drafts.", ); }); test("sensitive public outreach exports require operators and validate references", () => { const createSource = extractExportSource("create"); const listSource = extractExportSource("list"); hasPattern(createSource, /requireOperator\(ctx\)/, "create should require operator auth."); hasPattern(listSource, /requireOperator\(ctx\)/, "list should require operator auth."); hasPattern(createSource, /ctx\.db\.get\(args\.leadId\)/, "create should verify the lead exists."); hasPattern(createSource, /!lead/, "create should reject missing leads."); hasPattern(createSource, /ctx\.db\.get\(args\.auditId\)/, "create should load provided audits."); hasPattern(createSource, /!audit/, "create should reject missing audits."); hasPattern( createSource, /audit\.leadId\s*!==\s*args\.leadId/, "create should reject auditId values that belong to a different lead.", ); }); test("saveReviewDraft validates editable fields and never edits sent records", () => { const saveSource = extractExportSource("saveReviewDraft"); hasPattern(outreachSource, /export const saveReviewDraft = mutation\(/, "saveReviewDraft should be a mutation."); hasPattern(saveSource, /requireOperator\(ctx\)/, "saveReviewDraft should require auth."); hasPattern(saveSource, /id:\s*v\.id\("outreachRecords"\)/, "saveReviewDraft should validate the outreach id."); hasPattern(saveSource, /strategy:\s*strategy/, "saveReviewDraft should validate strategy with the shared strategy validator."); for (const fieldName of [ "phoneScript", "emailSubject", "emailBody", "followUpDraft", ]) { hasPattern( saveSource, new RegExp(`${fieldName}:\\s*v\\.optional\\(v\\.string\\(\\)\\)`), `${fieldName} should be optional editable copy.`, ); } hasPattern(saveSource, /ctx\.db\.get\(args\.id\)/, "saveReviewDraft should load the outreach record."); hasPattern(saveSource, /!outreach/, "saveReviewDraft should reject missing outreach."); hasPattern(saveSource, /outreach\.sendStatus\s*===\s*"sent"/, "saveReviewDraft should reject sent outreach records."); hasPattern(saveSource, /outreach\.sendStatus\s*===\s*"queued"/, "saveReviewDraft should reject queued outreach records."); hasPattern(saveSource, /approvalStatus:\s*"draft"/, "Saving edits should reset approval to draft."); hasPattern(saveSource, /updatedAt:\s*now/, "Saving edits should stamp updatedAt."); hasPattern(saveSource, /ctx\.db\.patch\(args\.id/, "saveReviewDraft should patch the existing outreach record."); }); test("approveEmailDraft validates approval prerequisites and preserves send separation", () => { const approveSource = extractExportSource("approveEmailDraft"); hasPattern(outreachSource, /export const approveEmailDraft = mutation\(/, "approveEmailDraft should be a mutation."); hasPattern(approveSource, /requireOperator\(ctx\)/, "approveEmailDraft should require auth."); hasPattern(approveSource, /id:\s*v\.id\("outreachRecords"\)/, "approveEmailDraft should validate the outreach id."); hasPattern(approveSource, /ctx\.db\.get\(args\.id\)/, "approveEmailDraft should load the outreach record."); hasPattern(approveSource, /!outreach/, "approveEmailDraft should reject missing outreach."); hasPattern(approveSource, /outreach\.sendStatus\s*===\s*"sent"/, "approveEmailDraft should reject sent outreach records."); hasPattern( approveSource, /outreach\.sendStatus\s*===\s*"queued"/, "approveEmailDraft should reject queued outreach records.", ); hasPattern(approveSource, /ctx\.db\.get\(outreach\.leadId\)/, "approveEmailDraft should load the linked lead."); hasPattern(approveSource, /lead\.email\?\.trim\(\)/, "approveEmailDraft should require a trimmed recipient email."); hasPattern(approveSource, /outreach\.emailSubject\?\.trim\(\)/, "approveEmailDraft should require a trimmed subject."); hasPattern(approveSource, /outreach\.emailBody\?\.trim\(\)/, "approveEmailDraft should require a trimmed body."); hasPattern( approveSource, /process\.env\.SMTP_FROM\?\.trim\(\)/, "approveEmailDraft should resolve the configured SMTP_FROM sender.", ); hasPattern( approveSource, /SMTP-Absender-Adresse fehlt\./, "approveEmailDraft should fail fast if SMTP_FROM is not configured.", ); hasPattern(approveSource, /approvalStatus:\s*"approved"/, "Approval should mark only the approval status."); hasPattern(approveSource, /updatedAt:\s*now/, "Approval should stamp updatedAt."); hasPattern(approveSource, /recipient:/, "Approval should return recipient context."); hasPattern(approveSource, /subject:/, "Approval should return subject context."); hasPattern(approveSource, /sender:/, "Approval should return sender context."); hasPattern(approveSource, /auditSlug:/, "Approval should return audit slug context when available."); lacksPattern(approveSource, /sendStatus\s*:/, "Approval must not alter sendStatus."); lacksPattern( approveSource, /SMTP_USER|SMTP_PASSWORD|SMTP_HOST/, "Approval should not expose SMTP credentials in returned context.", ); lacksPattern( approveSource, /ctx\.scheduler|runAfter|runMutation|nodemailer|smpp|sendEmail/i, "Approval must not queue or send email/SMPP/Nodemailer work.", ); }); test("schema defines outbound send attempt logging table", () => { assert.equal(existsSync(schemaPath), true, "schema.ts should be present."); const tableSource = extractTableSource("outreachSendAttempts"); assert.equal( tableSource.length > 0, true, "schema.ts should define outreachSendAttempts table.", ); hasPattern( tableSource, /outreachId:\s*v\.id\(\s*"outreachRecords"\s*\)/, "outreachSendAttempts must track outreachId.", ); hasPattern( tableSource, /leadId:\s*v\.id\(\s*"leads"\s*\)/, "outreachSendAttempts must track leadId.", ); hasPattern( tableSource, /auditId:\s*v\.optional\(v\.id\(\s*"audits"\s*\)\)/, "outreachSendAttempts should optionally track auditId.", ); hasPattern( tableSource, /recipient:\s*v\.string\(\)/, "outreachSendAttempts should persist recipient.", ); hasPattern( tableSource, /subject:\s*v\.string\(\)/, "outreachSendAttempts should persist subject.", ); hasPattern( tableSource, /body:\s*v\.string\(\)/, "outreachSendAttempts should persist body.", ); hasPattern( tableSource, /sender:\s*v\.string\(\)/, "outreachSendAttempts should persist sender.", ); hasPattern( tableSource, /status:\s*(?:v\.union\(\s*v\.literal\(\s*"success"\s*\),\s*v\.literal\(\s*"failed"\s*\)\s*\)|outreachSendAttemptStatus)/, "outreachSendAttempts status must be success or failed.", ); hasPattern( tableSource, /sentAt:\s*v\.optional\(v\.number\(\)\)/, "outreachSendAttempts should persist optional sentAt.", ); hasPattern( tableSource, /smtpMessageId:\s*v\.optional\(v\.string\(\)\)/, "outreachSendAttempts should persist optional smtpMessageId.", ); hasPattern( tableSource, /smtpResponse:\s*v\.optional\(v\.string\(\)\)/, "outreachSendAttempts should persist optional smtpResponse.", ); hasPattern( tableSource, /smtpAccepted:\s*v\.optional\(v\.array\(v\.string\(\)\)\)/, "outreachSendAttempts should persist optional smtpAccepted.", ); hasPattern( tableSource, /smtpRejected:\s*v\.optional\(v\.array\(v\.string\(\)\)\)/, "outreachSendAttempts should persist optional smtpRejected.", ); hasPattern( tableSource, /errorMessage:\s*v\.optional\(v\.string\(\)\)/, "outreachSendAttempts should persist optional errorMessage.", ); hasPattern( tableSource, /errorCode:\s*v\.optional\(v\.string\(\)\)/, "outreachSendAttempts should persist optional errorCode.", ); hasPattern( tableSource, /errorResponseCode:\s*v\.optional\(v\.number\(\)\)/, "outreachSendAttempts should persist optional errorResponseCode.", ); hasPattern( tableSource, /errorResponse:\s*v\.optional\(v\.string\(\)\)/, "outreachSendAttempts should persist optional errorResponse.", ); hasPattern( tableSource, /auditLink:\s*v\.optional\(v\.union\(v\.string\(\),\s*v\.null\(\)\)\)/, "outreachSendAttempts should persist optional auditLink.", ); hasPattern( tableSource, /createdAt:\s*v\.number\(\)/, "outreachSendAttempts should persist createdAt.", ); hasPattern( tableSource, /updatedAt:\s*v\.number\(\)/, "outreachSendAttempts should persist updatedAt.", ); hasPattern( schemaSource, /defineTable\(\{\s*[\s\S]*\}\)\s*\.index\("by_outreachId",\s*\["outreachId"\]\)/, "outreachSendAttempts should be queryable by outreach id.", ); hasPattern( schemaSource, /"by_status",\s*\["status"\]/, "outreachSendAttempts should include status indexing.", ); }); test("outreach module exports internal send claim and logging mutations", () => { const exports = getExportedConstNames(sourceFile); for (const exportName of [ "claimApprovedEmailForSend", "recordEmailSendSuccess", "recordEmailSendFailure", ]) { assert.equal( exports.has(exportName), true, `Expected export: ${exportName}`, ); } const claimSource = extractExportSource("claimApprovedEmailForSend"); const successSource = extractExportSource("recordEmailSendSuccess"); const failureSource = extractExportSource("recordEmailSendFailure"); hasPattern( claimSource, /outreach\.approvalStatus\s*!==\s*"approved"/, "claimApprovedEmailForSend must reject non-approved or already sent outreach records.", ); hasPattern( claimSource, /outreach\.sendStatus\s*===\s*"sent"\s*\|\|\s*outreach\.sendStatus\s*===\s*"queued"/, "claimApprovedEmailForSend should only claim not_sent or failed records.", ); hasPattern( claimSource, /ctx\.db\.patch\(args\.id,\s*{[\s\S]*sendStatus:\s*"queued"[\s\S]*updatedAt:\s*now[\s\S]*}\)/, "claimApprovedEmailForSend must set sendStatus queued and update updatedAt.", ); hasPattern( claimSource, /sender/, "claimApprovedEmailForSend should include sender in its snapshot return.", ); hasPattern( claimSource, /recipient/, "claimApprovedEmailForSend should include recipient in its snapshot return.", ); hasPattern( claimSource, /body/, "claimApprovedEmailForSend should include body in its snapshot return.", ); hasPattern( claimSource, /auditLink/, "claimApprovedEmailForSend should include optional auditLink in its snapshot return.", ); hasPattern( successSource, /ctx\.db\.patch\(args\.id,\s*{[\s\S]*sendStatus:\s*"sent"[\s\S]*sentAt:\s*args\.sentAt[\s\S]*updatedAt:\s*now[\s\S]*}\)/, "recordEmailSendSuccess should mark status sent, sentAt, and updatedAt.", ); hasPattern( successSource, /ctx\.db\.patch\(lead\._id,[\s\S]*contactStatus:\s*"contacted"[\s\S]*updatedAt:\s*now[\s\S]*}\)/, "recordEmailSendSuccess should mark outreach lead as contacted.", ); hasPattern( successSource, /ctx\.db\.insert\(\s*"outreachSendAttempts"/, "recordEmailSendSuccess should insert an outreachSendAttempts row.", ); hasPattern( failureSource, /ctx\.db\.patch\(args\.id,\s*{[\s\S]*sendStatus:\s*"failed"[\s\S]*updatedAt:\s*now[\s\S]*}\)/, "recordEmailSendFailure should mark status failed and update updatedAt.", ); hasPattern( failureSource, /ctx\.db\.insert\(\s*"outreachSendAttempts"/, "recordEmailSendFailure should insert an outreachSendAttempts row.", ); hasPattern( successSource, /status:\s*outreachSendAttemptSuccessStatus/, "recordEmailSendSuccess should send a typed success status.", ); hasPattern( failureSource, /status:\s*outreachSendAttemptFailedStatus/, "recordEmailSendFailure should send a typed failed status.", ); lacksPattern( failureSource, /contactStatus:\s*"contacted"/, "recordEmailSendFailure should not update lead to contacted.", ); }); test("outreachSendAction exists as Node action and orchestrates claim/send/log flow", () => { assert.equal(existsSync(actionPath), true, "outreachSendAction.ts should be present."); const actionExports = getExportedConstNames(actionSourceFile); hasPattern( outreachSendActionSource, /^"use node";/m, "outreachSendAction.ts should declare Node runtime.", ); assert.equal( actionExports.has("sendApprovedEmail"), true, "outreachSendAction.ts should export sendApprovedEmail.", ); hasPattern( outreachSendActionSource, /sendApprovedEmail\s*=\s*action\(\s*{\s*args:\s*{[\s\S]*id:\s*v\.id\(\s*"outreachRecords"\s*\)[\s\S]*}\s*,/, "sendApprovedEmail should accept outreachRecords id.", ); hasPattern( outreachSendActionSource, /internal\.outreach\.claimApprovedEmailForSend/, "sendApprovedEmail must call claimApprovedEmailForSend before sending.", ); hasPattern( outreachSendActionSource, /nodemailer\.createTransport\(\s*{[\s\S]*host:\s*smtpHost[\s\S]*port:\s*smtpPort[\s\S]*secure:\s*isSecureSmtp/, "sendApprovedEmail should configure Nodemailer from SMTP env vars.", ); hasPattern( outreachSendActionSource, /sendMail\(\s*{[\s\S]*from:\s*snapshot\.sender[\s\S]*to:\s*snapshot\.recipient[\s\S]*subject:\s*snapshot\.subject[\s\S]*text:\s*snapshot\.body[\s\S]*}\s*\)/, "sendApprovedEmail should send with snapshot sender, recipient, subject, body.", ); hasPattern( outreachSendActionSource, /internal\.outreach\.recordEmailSendSuccess/, "sendApprovedEmail should delegate successful sends to recordEmailSendSuccess.", ); hasPattern( outreachSendActionSource, /internal\.outreach\.recordEmailSendFailure/, "sendApprovedEmail should delegate failed sends to recordEmailSendFailure.", ); hasPattern( outreachSendActionSource, /console\.error\(|console\.warn\(/, "sendApprovedEmail should log failed sends with a concrete error path.", ); hasPattern( outreachSendActionSource, /sanitizeSmtpError/, "sendApprovedEmail should sanitize SMTP error details before logging.", ); hasPattern( outreachSendActionSource, /const successPayload: \{[\s\S]*auditId\?:[\s\S]*auditLink\?:[\s\S]*smtpMessageId\?:[\s\S]*smtpAccepted\?:[\s\S]*smtpRejected\?:/, "Success logging payload should be assembled with optional fields, not spread directly.", ); hasPattern( outreachSendActionSource, /const failurePayload: \{[\s\S]*auditId\?:[\s\S]*auditLink\?:[\s\S]*errorMessage\?:[\s\S]*errorCode\?:[\s\S]*errorResponseCode\?:[\s\S]*errorResponse\?:/, "Failure logging payload should be assembled with optional fields, not spread directly.", ); });