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 auditGenerationPath = join(process.cwd(), "convex", "auditGeneration.ts"); const auditGenerationSource = existsSync(auditGenerationPath) ? readFileSync(auditGenerationPath, "utf8") : ""; const sourceFile = ts.createSourceFile( "auditGeneration.ts", auditGenerationSource, 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, ); if (!isExported) { ts.forEachChild(node, visit); return; } const isConst = node.declarationList.flags & ts.NodeFlags.Const; if (!isConst) { ts.forEachChild(node, visit); return; } 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 hasPattern(source: string, pattern: RegExp) { return pattern.test(source); } function extractExportSource(name: string) { const marker = `export const ${name} = `; const declarationIndex = auditGenerationSource.indexOf(marker); assert.notEqual( declarationIndex, -1, `Expected declaration for ${name}`, ); const openBraceIndex = auditGenerationSource.indexOf("{", declarationIndex); let depth = 0; let end = -1; for (let index = openBraceIndex; index < auditGenerationSource.length; index += 1) { const char = auditGenerationSource[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 auditGenerationSource.slice(openBraceIndex, end + 1); } test("auditGeneration module exports required mutation contracts", () => { assert.equal( existsSync(auditGenerationPath), true, "auditGeneration.ts should be present", ); const exports = getExportedConstNames(sourceFile); const required = [ "queueLeadAuditGeneration", "startAuditGenerationRun", "persistAuditGenerationResult", "finishAuditGenerationRun", ]; for (const exportName of required) { assert.equal( exports.has(exportName), true, `Expected export: ${exportName}`, ); } }); test("auditGeneration module registers internalMutation contracts", () => { for (const name of [ "queueLeadAuditGeneration", "startAuditGenerationRun", "persistAuditGenerationResult", "finishAuditGenerationRun", ]) { assert.equal( hasPattern( auditGenerationSource, new RegExp(`export const ${name} = internalMutation\\s*\\(`), ), true, `${name} should be registered as internalMutation.`, ); } }); test("queueLeadAuditGeneration dedupes pending/running runs and schedules action", () => { const queueSource = extractExportSource("queueLeadAuditGeneration"); assert.equal( hasPattern( queueSource, /withIndex\("by_type_and_status_and_leadId"[\s\S]*?eq\("type",\s*"audit_generation"\)[\s\S]*?eq\("status",\s*"pending"\)[\s\S]*?eq\("leadId",\s*args\.leadId\)/, ), true, "Queue should dedupe pending runs with by_type_and_status_and_leadId for type audit_generation.", ); assert.equal( hasPattern( queueSource, /withIndex\("by_type_and_status_and_leadId"[\s\S]*?eq\("type",\s*"audit_generation"\)[\s\S]*?eq\("status",\s*"running"\)[\s\S]*?eq\("leadId",\s*args\.leadId\)/, ), true, "Queue should dedupe running runs with by_type_and_status_and_leadId for type audit_generation.", ); assert.equal( hasPattern( queueSource, /ctx\.scheduler\.runAfter\(\s*0,\s*internal\.auditGenerationAction\.processAuditGeneration,[\s\S]*?runId/, ), true, "Queue should schedule internal.auditGenerationAction.processAuditGeneration.", ); assert.equal( hasPattern(queueSource, /Audit-Generierung wurde in die Warteschlange gesetzt\./), true, "Queue should emit a queue event message.", ); }); test("startAuditGenerationRun validates and marks run as running", () => { const startSource = extractExportSource("startAuditGenerationRun"); assert.equal( hasPattern(startSource, /run\.type\s*!==\s*"audit_generation"/), true, "start should validate audit_generation run type.", ); assert.equal( hasPattern(startSource, /run\.status\s*!==\s*"pending"/), true, "start should require pending status.", ); assert.equal( hasPattern(startSource, /!run\.leadId[\s\S]*status:\s*"failed"/), true, "start should fail clearly when leadId missing.", ); assert.equal( hasPattern(startSource, /!lead[\s\S]*status:\s*"failed"/), true, "start should fail clearly when lead cannot be loaded.", ); assert.equal( hasPattern( startSource, /ctx\.db\.patch\(\s*args\.runId,[\s\S]*status:\s*"running"/, ), true, "start should set run status running.", ); assert.equal( hasPattern(startSource, /message:\s*"[^"]*konnte nicht gestartet werden[^"]*"/i), true, "start should emit clear failure events when starting fails.", ); }); test("persistAuditGenerationResult inserts into auditGenerations", () => { const persistSource = extractExportSource("persistAuditGenerationResult"); assert.equal( hasPattern(persistSource, /ctx\.db\.insert\(\s*"auditGenerations"/), true, "persistAuditGenerationResult should insert into auditGenerations.", ); assert.equal( hasPattern( persistSource, /prompt:\s*sanitizeAndCapString\(args\.prompt,\s*MAX_PROMPT_BYTES\)/, ), true, "persist function should sanitize prompt before persisting to avoid secrets.", ); assert.equal( hasPattern( persistSource, /rawResponse:\s*sanitizeAndCapString\(args\.rawResponse,\s*MAX_RAW_RESPONSE_BYTES\)/, ), true, "persist function should sanitize rawResponse before persisting to avoid secrets.", ); }); test("truncateWithMarker is byte-capped and marker-safe in persistence", () => { assert.equal( hasPattern(auditGenerationSource, /const markerBytes = byteLength\(TRUNCATION_MARKER\);/), true, "truncateWithMarker should calculate marker bytes explicitly.", ); assert.equal( hasPattern( auditGenerationSource, /if\s*\(byteLength\(value\)\s*<=\s*maxBytes\)\s*\{\s*return\s*value;\s*\}/, ), true, "truncateWithMarker should return early when already within byte limit.", ); assert.equal( hasPattern( auditGenerationSource, /if\s*\(markerBytes\s*>=\s*maxBytes\)/, ), true, "truncateWithMarker should handle marker length edge cases.", ); assert.equal( hasPattern( auditGenerationSource, /new TextDecoder\(\)\.decode\(markerBytesBuffer\.slice\(0,\s*maxBytes\)\)/, ), true, "truncateWithMarker should trim marker bytes with decoder slice fallback.", ); assert.equal( hasPattern( auditGenerationSource, /TRUNCATION_MARKER\\.slice\(0,\s*maxBytes\)/, ), false, "truncateWithMarker should not use unbounded marker slicing by bytes.", ); }); test("sanitizer masks env-backed secret values in persistence", () => { assert.equal( hasPattern(auditGenerationSource, /function\s+sanitizeSecretCandidates/), true, "Persistence should expose secret candidate sanitizer.", ); assert.equal( hasPattern(auditGenerationSource, /OPENROUTER_API_KEY/), true, "Persistence sanitizer should know OPENROUTER_API_KEY.", ); assert.equal( hasPattern( auditGenerationSource, /return\s+sanitized\s*\r?\n\s*\.replace\(/, ), true, "Persistence sanitizer should apply regex secret-masking patterns.", ); }); test("finishAuditGenerationRun updates run status/counters/currentStep", () => { const finishSource = extractExportSource("finishAuditGenerationRun"); assert.equal( hasPattern( finishSource, /ctx\.db\.patch\(\s*args\.runId,[\s\S]*?status:\s*args\.status/, ), true, "finish should set run status.", ); assert.equal( hasPattern( finishSource, /status:\s*args\.status[\s\S]*finishedAt:\s*now/, ), true, "finish should set finishedAt.", ); assert.equal( hasPattern( finishSource, /counters:\s*\{[\s\S]*errors:\s*args\.errors/, ), true, "finish should update counters with errors.", ); assert.equal( hasPattern( finishSource, /currentStep:\s*args\.currentStep\s*(\|\||\?\?)\s*"audit_generation"/, ), true, "finish should update currentStep.", ); });