Add SMTP send flow for approved outreach
This commit is contained in:
@@ -6,12 +6,16 @@ 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",
|
||||
@@ -20,6 +24,13 @@ const sourceFile = ts.createSourceFile(
|
||||
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<string>();
|
||||
@@ -73,6 +84,37 @@ function extractExportSource(name: string) {
|
||||
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);
|
||||
}
|
||||
@@ -110,6 +152,67 @@ test("outreach review module exports authenticated review contracts", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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}`;
|
||||
@@ -246,7 +349,7 @@ test("upsertFromAuditGeneration preserves review boundaries for generated copy",
|
||||
);
|
||||
hasPattern(
|
||||
upsertSource,
|
||||
/current\.sendStatus\s*===\s*"sent"[\s\S]*?ctx\.db\.insert\(\s*"outreachRecords"/,
|
||||
/current\.sendStatus\s*===\s*"sent"[\s\S]*?buildOutreachRecordsInsertPayload/,
|
||||
"upsert should create a new draft record instead of patching a sent outreach record.",
|
||||
);
|
||||
hasPattern(
|
||||
@@ -255,8 +358,8 @@ test("upsertFromAuditGeneration preserves review boundaries for generated copy",
|
||||
"Generated copy changes should reset existing unsent outreach records to draft.",
|
||||
);
|
||||
hasPattern(
|
||||
upsertSource,
|
||||
/approvalStatus:\s*"draft"[\s\S]*sendStatus:\s*"not_sent"/,
|
||||
outreachSource,
|
||||
/buildOutreachRecordsInsertPayload[\s\S]*approvalStatus:\s*"draft"[\s\S]*sendStatus:\s*"not_sent"/,
|
||||
"New generated outreach records should start as unsent drafts.",
|
||||
);
|
||||
});
|
||||
@@ -302,6 +405,7 @@ test("saveReviewDraft validates editable fields and never edits sent records", (
|
||||
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.");
|
||||
@@ -316,16 +420,324 @@ test("approveEmailDraft validates approval prerequisites and preserves send sepa
|
||||
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, /ctx\.scheduler|runAfter|runMutation|nodemailer|smtp|smpp|sendEmail/i, "Approval must not queue or send email/SMPP/Nodemailer work.");
|
||||
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.",
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user