719 lines
23 KiB
TypeScript
719 lines
23 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
|
|
const actionPath = path.join(process.cwd(), "convex", "auditGenerationAction.ts");
|
|
const actionSource = existsSync(actionPath) ? readFileSync(actionPath, "utf8") : "";
|
|
const toneGuidelinesPath = path.join(
|
|
process.cwd(),
|
|
"lib",
|
|
"ai",
|
|
"customer-tone-guidelines.ts",
|
|
);
|
|
const toneGuidelinesSource = existsSync(toneGuidelinesPath)
|
|
? readFileSync(toneGuidelinesPath, "utf8")
|
|
: "";
|
|
const generationSourcePath = path.join(process.cwd(), "convex", "auditGeneration.ts");
|
|
const generationSource = existsSync(generationSourcePath)
|
|
? readFileSync(generationSourcePath, "utf8")
|
|
: "";
|
|
|
|
function hasPattern(source: string, pattern: RegExp) {
|
|
return pattern.test(source);
|
|
}
|
|
|
|
function hasExportedInternalAction(exportName: string) {
|
|
const pattern = new RegExp(
|
|
`export const ${exportName}\\s*=\\s*internalAction\\s*\\(`,
|
|
);
|
|
return hasPattern(actionSource, pattern);
|
|
}
|
|
|
|
function hasStageCall(schema: string) {
|
|
const escaped = schema.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
return hasPattern(
|
|
actionSource,
|
|
new RegExp(
|
|
`generateObject\\([\\s\\S]*schema:\\s*${escaped}[\\s\\S]*\\)`,
|
|
"m",
|
|
),
|
|
);
|
|
}
|
|
|
|
function extractFunctionSource(functionName: string) {
|
|
const marker = `function ${functionName}`;
|
|
const asyncMarker = `async function ${functionName}`;
|
|
const declarationIndex = actionSource.indexOf(marker) === -1
|
|
? actionSource.indexOf(asyncMarker)
|
|
: actionSource.indexOf(marker);
|
|
assert.notEqual(
|
|
declarationIndex,
|
|
-1,
|
|
`Expected function ${functionName} to exist.`,
|
|
);
|
|
|
|
const openBraceIndex = actionSource.indexOf("{", declarationIndex);
|
|
let depth = 0;
|
|
let end = -1;
|
|
|
|
for (let index = openBraceIndex; index < actionSource.length; index += 1) {
|
|
const char = actionSource[index];
|
|
if (char === "{") {
|
|
depth += 1;
|
|
} else if (char === "}") {
|
|
depth -= 1;
|
|
if (depth === 0) {
|
|
end = index;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
assert.notEqual(end, -1, `Expected balanced braces for ${functionName}.`);
|
|
return actionSource.slice(declarationIndex, end + 1);
|
|
}
|
|
|
|
test("auditGenerationAction module exists and is a Node action file", () => {
|
|
assert.equal(existsSync(actionPath), true, "auditGenerationAction.ts should exist");
|
|
assert.equal(
|
|
hasPattern(actionSource, /^"use node";/m),
|
|
true,
|
|
"auditGenerationAction.ts should start with \"use node\"",
|
|
);
|
|
});
|
|
|
|
test("auditGenerationAction exports processAuditGeneration with runId validator", () => {
|
|
assert.equal(
|
|
hasExportedInternalAction("processAuditGeneration"),
|
|
true,
|
|
"processAuditGeneration should be an internalAction",
|
|
);
|
|
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/processAuditGeneration\s*=\s*internalAction\(\s*{\s*args:\s*{[\s\S]*?runId:\s*v\.id\(\s*["']agentRuns["']\s*\)[\s\S]*?rootRunId:\s*v\.optional\(v\.id\(\s*["']agentRuns["']\s*\)\)/,
|
|
),
|
|
true,
|
|
"processAuditGeneration should validate runId and optional rootRunId as agentRuns IDs",
|
|
);
|
|
});
|
|
|
|
test("action starts, queries evidence, and runs stage pipeline", () => {
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/internal\.auditGeneration\.startAuditGenerationRun/,
|
|
),
|
|
true,
|
|
"Action should start the run via internal.auditGeneration.startAuditGenerationRun",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/internal\.auditGeneration\.getAuditGenerationEvidence/,
|
|
),
|
|
true,
|
|
"Action should load evidence via internal.auditGeneration.getAuditGenerationEvidence",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/internal\.auditGeneration\.persistAuditGenerationResult/,
|
|
),
|
|
true,
|
|
"Action should persist each stage result",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/internal\.auditGeneration\.finishAuditGenerationRun/,
|
|
),
|
|
true,
|
|
"Action should finish run via internal.auditGeneration.finishAuditGenerationRun",
|
|
);
|
|
});
|
|
|
|
test("action includes all required audit stages", () => {
|
|
for (const stage of [
|
|
"classification",
|
|
"localSeoSpecialist",
|
|
"conversionUxSpecialist",
|
|
"visualTrustSpecialist",
|
|
"critiqueSpecialist",
|
|
"performanceAccessibilitySpecialist",
|
|
"evidenceVerifier",
|
|
"multimodalAudit",
|
|
"germanCopy",
|
|
"qualityReview",
|
|
]) {
|
|
const token = new RegExp(`stage:\\s*["']${stage}["']`);
|
|
assert.equal(
|
|
hasPattern(actionSource, token),
|
|
true,
|
|
`Action should reference ${stage} stage`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("specialist fan-out runs after evidence input and before German copy", () => {
|
|
const evidenceInputIndex = actionSource.indexOf("const evidenceInput = buildAuditEvidenceInput");
|
|
const fanOutIndex = actionSource.indexOf("Promise.all(\n specialistStageConfigs.map");
|
|
const verifierIndex = actionSource.indexOf('currentStep = "evidenceVerifier"');
|
|
const germanCopyIndex = actionSource.indexOf('currentStep = "germanCopy"');
|
|
|
|
assert.notEqual(evidenceInputIndex, -1, "Action should build evidence input.");
|
|
assert.notEqual(germanCopyIndex, -1, "Action should still run German copy.");
|
|
assert.notEqual(fanOutIndex, -1, "Action should fan out specialist stage configs.");
|
|
assert.notEqual(verifierIndex, -1, "Action should run the evidence verifier.");
|
|
assert.equal(
|
|
fanOutIndex > evidenceInputIndex && fanOutIndex < germanCopyIndex,
|
|
true,
|
|
"Specialist fan-out should run after evidence input and before German copy.",
|
|
);
|
|
assert.equal(
|
|
verifierIndex > fanOutIndex && verifierIndex < germanCopyIndex,
|
|
true,
|
|
"Evidence verifier should run after specialist fan-out and before German copy.",
|
|
);
|
|
});
|
|
|
|
test("specialist stages use specialist schemas and verified findings feed German copy", () => {
|
|
assert.equal(
|
|
hasStageCall("auditSpecialistResultSchema"),
|
|
true,
|
|
"Specialist stages should call generateObject with auditSpecialistResultSchema.",
|
|
);
|
|
assert.equal(
|
|
hasStageCall("auditEvidenceVerificationSchema"),
|
|
true,
|
|
"Verifier stage should call generateObject with auditEvidenceVerificationSchema.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/(?:const|let)\s+verifiedFindings\s*[:=]/,
|
|
"Action should derive verifiedFindings before synthesis.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/verifiedResult?\.?object|verifiedFindingIds/,
|
|
"Verifier output should use compact finding IDs instead of echoing full findings.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/verifiedFindingIds\.has\(candidate\.findingId\)/,
|
|
"Action should map verifier-approved IDs back to original specialist findings.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/buildGermanCopyPrompt\(\s*verifiedFindingsText/,
|
|
"German copy should be generated from verified findings text.",
|
|
);
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/buildGermanCopyPrompt\(\s*classificationSummary\s*,/,
|
|
"German copy should no longer use raw classification summary as its primary finding input.",
|
|
);
|
|
});
|
|
|
|
test("critique specialist translates impeccable critique guidance into the audit fan-out", () => {
|
|
assert.match(
|
|
actionSource,
|
|
/stage:\s*["']critiqueSpecialist["']/,
|
|
"Action should include a dedicated critique specialist stage.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/impeccable-critique/,
|
|
"Critique specialist should anchor findings to the impeccable critique skill id.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/kognitive Last|Nielsen|AI-Slop|Informationsarchitektur/,
|
|
"Critique specialist should include critique guidance beyond generic visual trust.",
|
|
);
|
|
});
|
|
|
|
test("German copy prompt uses first-contact email tone guidelines without a new AI stage", () => {
|
|
const buildPromptSource = extractFunctionSource("buildGermanCopyPrompt");
|
|
|
|
assert.doesNotMatch(
|
|
buildPromptSource,
|
|
/Ich-Ich Kontext/,
|
|
"German copy prompt should not force formulaic Ich-Ich copy.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/buildCustomerTonePromptSection/,
|
|
"German copy prompt should inject shared customer tone guidelines.",
|
|
);
|
|
assert.match(
|
|
buildPromptSource,
|
|
/evidence:\s*AuditEvidence/,
|
|
"German copy prompt should accept explicit evidence context.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/buildGermanCopyPrompt\([\s\S]*verifiedFindingsText[\s\S]*multimodalSummary[\s\S]*evidenceInput[\s\S]*\)/,
|
|
"German copy prompt should receive the explicit evidence context at the callsite.",
|
|
);
|
|
assert.match(
|
|
toneGuidelinesSource,
|
|
/kollegial direkt/,
|
|
"Tone guidelines should lock the selected sender posture.",
|
|
);
|
|
assert.match(
|
|
toneGuidelinesSource,
|
|
/maximal zwei verifizierte Befunde|max\. zwei verifizierte Befunde/,
|
|
"Tone guidelines should keep outreach emails to at most two verified findings.",
|
|
);
|
|
assert.match(
|
|
toneGuidelinesSource,
|
|
/kein Mini-Audit/,
|
|
"Tone guidelines should explicitly forbid mini-audit emails.",
|
|
);
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/tone(?:Review|Rewrite|Specialist)|emailToneSpecialist|copyToneSpecialist/,
|
|
"Tone work should not add another model-backed generation stage.",
|
|
);
|
|
});
|
|
|
|
test("quality review can rewrite copy once without making copy feedback a hard failure", () => {
|
|
const qualityPromptSource = extractFunctionSource("buildQualityReviewPrompt");
|
|
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/qualityPassed\s*=\s*qualityResult\.object\.isValid\s*&&\s*guardResult\.passed/,
|
|
"Copy quality feedback should not be a hard AND-gate with the deterministic German copy guard.",
|
|
);
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/qualityPassed\s*=\s*guardResult\.passed\s*;/,
|
|
"The deterministic German copy guard should not be the quality pass condition.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/rewriteRequired[\s\S]*revisedCopy[\s\S]*applyRevisedCopy/,
|
|
"Quality review should be able to request one revised copy and apply it before persistence.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/copyReviewAttempts\s*<\s*2/,
|
|
"Quality review should run at most the initial review plus one rewrite review.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/message:\s*["']Copy-Review hat korrigiert\.["']/,
|
|
"A successful rewrite should be visible as a warning event.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/message:\s*["']Copy-Review mit Hinweisen abgeschlossen\.["']/,
|
|
"Remaining copy feedback should be stored as warning telemetry.",
|
|
);
|
|
assert.match(
|
|
qualityPromptSource,
|
|
/echte Erstmail von Matthias/,
|
|
"Quality review should apply the selected first-contact email rubric.",
|
|
);
|
|
assert.match(
|
|
qualityPromptSource,
|
|
/KI-Verkaufstext/,
|
|
"Quality review should reject AI-like sales copy.",
|
|
);
|
|
assert.match(
|
|
qualityPromptSource,
|
|
/verified findings|verifizierte Befunde/i,
|
|
"Quality review should keep concrete claims tied to verified findings.",
|
|
);
|
|
assert.match(
|
|
qualityPromptSource,
|
|
/revisedCopy|rewriteRequired/,
|
|
"Quality review prompt should ask for revised copy when rewrite is needed.",
|
|
);
|
|
});
|
|
|
|
test("action handles post-start failure paths in action-level catch", () => {
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/try\s*{[\s\S]*internal\.auditGeneration\.getAuditGenerationEvidence[\s\S]*const provider = createOpenRouterProvider\(\)/,
|
|
),
|
|
true,
|
|
"Action should include evidence query and provider init inside catch-covered flow.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/catch\s*\(error\)\s*{[\s\S]*appendRunEvent[\s\S]*finishAuditGenerationRun[\s\S]*"failed"/,
|
|
),
|
|
true,
|
|
"Action-level error handler should emit run events.",
|
|
);
|
|
});
|
|
|
|
test("action calls generateObject with required schemas", () => {
|
|
const requiredSchemas = [
|
|
"auditClassificationSchema",
|
|
"auditSummarySchema",
|
|
"publicAuditTextSchema",
|
|
"emailDraftSchema",
|
|
"emailSubjectSchema",
|
|
"callScriptSchema",
|
|
"followUpDraftSchema",
|
|
"qualityReviewSchema",
|
|
];
|
|
|
|
for (const requiredSchema of requiredSchemas) {
|
|
assert.equal(
|
|
hasStageCall(requiredSchema),
|
|
true,
|
|
`Action should call generateObject with schema ${requiredSchema}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("action loads v3 skill registry from bundled MVP source for evidence input", () => {
|
|
assert.equal(
|
|
hasPattern(actionSource, /import\s*{[\s\S]*loadLocalAuditSkillRegistry[\s\S]*}\s*from\s*["']\.\.\/lib\/ai\/local-audit-skill-registry["']/),
|
|
true,
|
|
"Action should import the bundled MVP skill registry loader.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(actionSource, /loadLocalAuditSkillRegistry\(\s*\)/),
|
|
true,
|
|
"Action should load the v3 registry from a bundled MVP module.",
|
|
);
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/v2_elemente|process\.cwd\(\)|loadSkillsRegistry\(|node:path/,
|
|
"Action should not read v2 reference files or filesystem paths at runtime.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(actionSource, /skillRegistry:\s*\[\s*\]/),
|
|
false,
|
|
"Action should not pass an always-empty skillRegistry to buildAuditEvidenceInput.",
|
|
);
|
|
});
|
|
|
|
test("registry load warning logging is isolated from fallback return", () => {
|
|
const loadRegistrySource = extractFunctionSource("loadAuditSkillRegistry");
|
|
|
|
assert.equal(
|
|
hasPattern(
|
|
loadRegistrySource,
|
|
/catch\s*\(error\)\s*{[\s\S]*try\s*{[\s\S]*appendRunEvent[\s\S]*}\s*catch\s*{[\s\S]*}\s*return\s*\[\s*\]/,
|
|
),
|
|
true,
|
|
"Registry load fallback should return [] even when warning event logging fails.",
|
|
);
|
|
});
|
|
|
|
test("persistAuditStage omits undefined fields from Convex mutation args", () => {
|
|
const persistSource = extractFunctionSource("persistAuditStage");
|
|
const mutationPayloadSource = persistSource.slice(
|
|
persistSource.indexOf("await ctx.runMutation"),
|
|
);
|
|
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/persistAuditStage\(\s*{(?:(?!\n\s*}\s*\);)[\s\S])*(?:parsedJson|rawResponse|usage|finishReason|errorSummary):\s*undefined/,
|
|
"Call sites should not pass explicit undefined stage payload fields.",
|
|
);
|
|
assert.doesNotMatch(
|
|
persistSource,
|
|
/usage:\s*usage\s*\?\s*toPersistedUsage\(usage\)\s*:\s*undefined/,
|
|
"persistAuditStage should not emit usage: undefined.",
|
|
);
|
|
|
|
for (const field of [
|
|
"systemPrompt",
|
|
"rawResponse",
|
|
"parsedJson",
|
|
"finishReason",
|
|
"errorSummary",
|
|
]) {
|
|
assert.doesNotMatch(
|
|
mutationPayloadSource,
|
|
new RegExp(`\\n\\s*${field},`),
|
|
`persistAuditStage should conditionally spread ${field}.`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("OpenRouter usage payloads omit undefined token fields", () => {
|
|
const recordUsageSource = extractFunctionSource("recordOpenRouterUsage");
|
|
|
|
assert.match(
|
|
actionSource,
|
|
/function toPersistedUsage[\s\S]*usage\.inputTokens\s*!==\s*undefined[\s\S]*promptTokens:\s*usage\.inputTokens/,
|
|
"toPersistedUsage should omit promptTokens when inputTokens is undefined.",
|
|
);
|
|
assert.doesNotMatch(
|
|
recordUsageSource,
|
|
/tokens:\s*{[\s\S]*inputTokens:\s*args\.usage\.inputTokens/,
|
|
"recordOpenRouterUsage should not build token payloads with undefined properties.",
|
|
);
|
|
});
|
|
|
|
test("appendRunEvent omits undefined details from Convex mutation args", () => {
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/ctx\.runMutation\(internal\.runs\.appendEventInternal,\s*{[\s\S]*\n\s*details:\s*args\.details,\n/,
|
|
"appendRunEvent should conditionally include details only when defined.",
|
|
);
|
|
});
|
|
|
|
test("success finishAuditGenerationRun omits undefined errorSummary", () => {
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/finishAuditGenerationRun,\s*{[\s\S]*status:\s*["']succeeded["'][\s\S]*errorSummary:\s*qualityPassed\s*\?\s*undefined/,
|
|
"Succeeded finishAuditGenerationRun payload should not send errorSummary: undefined.",
|
|
);
|
|
});
|
|
|
|
test("quality review stage does not pass explicit undefined optional fields", () => {
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/persistAuditStage\(\s*{[\s\S]*stage:\s*["']qualityReview["'][\s\S]*errorSummary:\s*qualityPassed\s*\?\s*undefined/,
|
|
"Quality persistAuditStage callsite should conditionally include errorSummary.",
|
|
);
|
|
});
|
|
|
|
test("persistAuditStage callsites conditionally include optional auditId", () => {
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/await\s+persistAuditStage\(\s*{(?:(?!\n\s*}\s*\);)[\s\S])*\n\s*auditId,\n/,
|
|
"persistAuditStage callsites should spread auditId only when defined.",
|
|
);
|
|
});
|
|
|
|
test("audit generation helper callsites conditionally include optional auditId", () => {
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/(?:recordOpenRouterUsage|captureExternalAuditArtifacts)\(\s*ctx,\s*{(?:(?!\n\s*}\s*\);)[\s\S])*\n\s*auditId,\n/,
|
|
"Helper callsites should spread auditId only when defined.",
|
|
);
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/recordAuditUsageEvent\(\s*ctx,\s*{(?:(?!\n\s*}\s*\);)[\s\S])*\n\s*auditId:\s*args\.auditId,\n/,
|
|
"recordAuditUsageEvent callsites should spread args.auditId only when defined.",
|
|
);
|
|
});
|
|
|
|
test("persistAuditStage callsites avoid nested maybe-undefined usage objects", () => {
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/persistAuditStage\(\s*{(?:(?!\n\s*}\s*\);)[\s\S])*usage:\s*{[\s\S]*?(?:inputTokens|outputTokens|totalTokens|cacheReadTokens):/,
|
|
"persistAuditStage callsites should use a usage helper or conditional spreads, not inline maybe-undefined usage objects.",
|
|
);
|
|
});
|
|
|
|
test("classification stage uses v3 audit classification schema", () => {
|
|
assert.equal(
|
|
hasPattern(actionSource, /auditClassificationSchema/),
|
|
true,
|
|
"Action should reference the v3 auditClassificationSchema.",
|
|
);
|
|
assert.equal(
|
|
hasStageCall("auditClassificationSchema"),
|
|
true,
|
|
"Classification generateObject call should validate v3 finding payloads.",
|
|
);
|
|
assert.equal(
|
|
hasStageCall("internalFindingsSchema"),
|
|
false,
|
|
"Classification should no longer validate against legacy-only internalFindingsSchema.",
|
|
);
|
|
});
|
|
|
|
test("action uses multimodal file parts with mediaType image/* when screenshots are available", () => {
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/type:\s*["']file["'][\s\S]*mediaType:\s*(?:getValidMediaType|["']image\/)/,
|
|
),
|
|
true,
|
|
"Multimodal call should include AI file parts with image mediaType",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/ctx\.storage\.(get|getUrl)\(/,
|
|
),
|
|
true,
|
|
"Multimodal call should try to fetch screenshots from Convex storage",
|
|
);
|
|
});
|
|
|
|
test("action handles missing screenshots with warning event fallback", () => {
|
|
assert.equal(
|
|
hasPattern(actionSource, /level:\s*["']warning["'][\s\S]*Screenshot|Vorschaubild/),
|
|
true,
|
|
"Action should append warning event when multimodal screenshot input is unavailable",
|
|
);
|
|
assert.equal(
|
|
hasPattern(actionSource, /messages:\s*\[[\s\S]*type:\s*["']text["'][\s\S]*\]/),
|
|
true,
|
|
"Action should fall back to text-only multimodal calls when required parts are missing",
|
|
);
|
|
});
|
|
|
|
test("action keeps German copy guard as telemetry without blocking outreach-ready", () => {
|
|
assert.equal(
|
|
hasPattern(actionSource, /validateCustomerFacingCopy/),
|
|
true,
|
|
"Action should still run German copy validation for telemetry.",
|
|
);
|
|
assert.doesNotMatch(
|
|
actionSource,
|
|
/guardResult\.passed[\s\S]{0,500}finishAuditGenerationRun[\s\S]{0,250}status:\s*["']failed["']/,
|
|
"German copy guard findings should not finish the audit generation as failed.",
|
|
);
|
|
assert.match(
|
|
actionSource,
|
|
/guardTelemetry|deterministicGuard/,
|
|
"German copy guard output should be persisted as telemetry in the quality payload.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(actionSource, /internal\.leads\.reviewUpdateInternal/),
|
|
true,
|
|
"Action should patch lead via internal.leads.reviewUpdateInternal",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/isTerminalLeadContactStatus/,
|
|
),
|
|
true,
|
|
"Action should set contactStatus to outreach_ready only when terminal guard allows it.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/do_not_contact|contacted|replied/i,
|
|
),
|
|
true,
|
|
"Action should explicitly guard against terminal lead statuses before outreach-ready.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/Lead-Status wurde nicht auf outreach_ready gesetzt/,
|
|
),
|
|
true,
|
|
"Action should emit warning event when outreach-ready cannot be set.",
|
|
);
|
|
});
|
|
|
|
test("action persists audit and outreach outputs before finishing succeeded run", () => {
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/internal\.audits\.upsertFromAuditGeneration/,
|
|
),
|
|
true,
|
|
"Action should persist audit output via internal.audits.upsertFromAuditGeneration",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/internal\.outreach\.upsertFromAuditGeneration/,
|
|
),
|
|
true,
|
|
"Action should persist outreach output via internal.outreach.upsertFromAuditGeneration",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/internal\.audits\.upsertFromAuditGeneration[\s\S]*internal\.outreach\.upsertFromAuditGeneration[\s\S]*internal\.auditGeneration\.finishAuditGenerationRun[\s\S]*status:\s*["']succeeded["']/,
|
|
),
|
|
true,
|
|
"Action should finish success after persisted outputs",
|
|
);
|
|
});
|
|
|
|
test("action uses model profiles for generation parameters", () => {
|
|
assert.equal(
|
|
hasPattern(actionSource, /resolveModelProfile\("classification"\)/),
|
|
true,
|
|
"classification generation should use resolveModelProfile.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(actionSource, /resolveModelProfile\("multimodalAudit"\)/),
|
|
true,
|
|
"multimodal generation should use resolveModelProfile.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(actionSource, /resolveModelProfile\("germanCopy"\)/),
|
|
true,
|
|
"german copy generation should use resolveModelProfile.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(actionSource, /resolveModelProfile\("qualityReview"\)/),
|
|
true,
|
|
"quality review generation should use resolveModelProfile.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/temperature:\s*classificationProfile\.temperature[\s\S]*maxOutputTokens:\s*classificationProfile\.maxTokens/,
|
|
),
|
|
true,
|
|
"classification stage should use profile temperature/maxTokens.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/temperature:\s*germanCopyProfile\.temperature[\s\S]*maxOutputTokens:\s*germanCopyProfile\.maxTokens/,
|
|
),
|
|
true,
|
|
"german copy stages should use profile temperature/maxTokens.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/temperature:\s*qualityReviewProfile\.temperature[\s\S]*maxOutputTokens:\s*qualityReviewProfile\.maxTokens/,
|
|
),
|
|
true,
|
|
"quality review stage should use profile temperature/maxTokens.",
|
|
);
|
|
});
|
|
|
|
test("action sanitization masks env-backed secrets", () => {
|
|
assert.equal(
|
|
hasPattern(
|
|
actionSource,
|
|
/sanitizeSecretCandidates\([\s\S]*process\.env/,
|
|
),
|
|
true,
|
|
"sanitize logic should include env-backed secret masking.",
|
|
);
|
|
assert.equal(
|
|
hasPattern(actionSource, /OPENROUTER_API_KEY/),
|
|
true,
|
|
"sanitizer should include OPENROUTER_API_KEY in secret hints.",
|
|
);
|
|
});
|
|
|
|
test("auditGeneration scheduler reference in queueLeadAuditGeneration is typed", () => {
|
|
assert.equal(
|
|
hasPattern(
|
|
generationSource,
|
|
/internal\.auditGenerationAction\.processAuditGeneration/,
|
|
),
|
|
true,
|
|
"queueLeadAuditGeneration should reference internal.auditGenerationAction.processAuditGeneration",
|
|
);
|
|
assert.equal(
|
|
hasPattern(
|
|
generationSource,
|
|
/internal as any/,
|
|
),
|
|
false,
|
|
"No temporary internal cast should remain for the processAuditGeneration schedule",
|
|
);
|
|
});
|