import assert from "node:assert/strict"; import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import test from "node:test"; import ts from "typescript"; const pageSpeedPath = path.join(process.cwd(), "convex", "pageSpeed.ts"); const pageSpeedSource = existsSync(pageSpeedPath) ? readFileSync(pageSpeedPath, "utf8") : ""; const sourceFile = ts.createSourceFile( "pageSpeed.ts", pageSpeedSource, 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 = pageSpeedSource.indexOf(marker); assert.notEqual(declarationIndex, -1, `Expected declaration for ${name}`); const openBraceIndex = pageSpeedSource.indexOf("{", declarationIndex); let depth = 0; let end = -1; for (let index = openBraceIndex; index < pageSpeedSource.length; index += 1) { const char = pageSpeedSource[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 pageSpeedSource.slice(openBraceIndex, end + 1); } test("pageSpeed module exports mutation contracts", () => { assert.equal(existsSync(pageSpeedPath), true, "pageSpeed.ts should be present"); const exports = getExportedConstNames(sourceFile); const required = [ "queueLeadPageSpeedAudit", "startPageSpeedAuditRun", "persistPageSpeedResult", "finishPageSpeedAuditRun", ]; for (const exportName of required) { assert.equal(exports.has(exportName), true, `Expected export: ${exportName}`); } }); test("pageSpeed module uses internalMutation for queue/start/persist/finish", () => { for (const name of [ "queueLeadPageSpeedAudit", "startPageSpeedAuditRun", "persistPageSpeedResult", "finishPageSpeedAuditRun", ]) { assert.equal( hasPattern(pageSpeedSource, new RegExp(`export const ${name} = internalMutation\\s*\\(`)), true, `${name} should be registered as internalMutation.`, ); } }); test("queueLeadPageSpeedAudit dedupes per lead and schedules pagespeed action", () => { const queueSource = extractExportSource("queueLeadPageSpeedAudit"); assert.equal( hasPattern( queueSource, /withIndex\(\s*"by_type_and_status_and_leadId"[\s\S]*?eq\("type",\s*"audit"\)[\s\S]*?eq\("status",\s*"pending"\)[\s\S]*?eq\("leadId",\s*args\.leadId\)/, ), true, "Queue should dedupe pending audit runs by type+status+leadId.", ); assert.equal( hasPattern( queueSource, /withIndex\(\s*"by_type_and_status_and_leadId"[\s\S]*?eq\("type",\s*"audit"\)[\s\S]*?eq\("status",\s*"running"\)[\s\S]*?eq\("leadId",\s*args\.leadId\)/, ), true, "Queue should dedupe running audit runs by type+status+leadId.", ); assert.equal( hasPattern( queueSource, /currentStep:\s*["']pagespeed_insights["']/, ), true, "Queued page speed runs should use currentStep pagespeed_insights.", ); assert.equal( hasPattern( queueSource, /ctx\.scheduler\.runAfter\(\s*0,\s*internal\.pageSpeedAction\.processPageSpeedAudit,\s*\{[\s\S]*?runId/, ), true, "queueLeadPageSpeedAudit must schedule internal.pageSpeedAction.processPageSpeedAudit with runAfter(0, ...).", ); assert.equal( hasPattern( queueSource, /PageSpeed-Analyse wurde in die Warteschlange gesetzt\./, ), true, "queueLeadPageSpeedAudit should emit queue-start event message.", ); }); test("startPageSpeedAuditRun marks run as running and handles clear failures", () => { const startSource = extractExportSource("startPageSpeedAuditRun"); assert.equal( hasPattern( startSource, /run\.type\s*!==?\s*["']audit["']/, ), true, "start function should require audit run type.", ); assert.equal( hasPattern(startSource, /run\.status\s*!==?\s*["']pending["']/), true, "start function should require pending status.", ); assert.equal( hasPattern( startSource, /ctx\.db\.patch\(\s*args\.runId,\s*\{[\s\S]*status:\s*["']running["']/, ), true, "start function should set status running.", ); assert.equal( hasPattern(startSource, /currentStep:\s*["']pagespeed_insights["']/), true, "start function should set currentStep pagespeed_insights.", ); assert.equal( hasPattern( startSource, /!run\.leadId[\s\S]*status:\s*["']failed["']/, ), true, "start should fail and record missing leadId.", ); assert.equal( hasPattern( startSource, /!lead\.websiteUrl[\s\S]*status:\s*["']failed["']/, ), true, "start should fail and record missing website URL.", ); assert.equal( hasPattern( startSource, /message:\s*["'][^"']*konnte nicht gestartet werden[^"']*["']/i, ), true, "start should add clear failure events.", ); }); test("persistPageSpeedResult writes pageSpeedResults table", () => { const persistSource = pageSpeedSource ? extractExportSource("persistPageSpeedResult") : "export const persistPageSpeedResult = {}"; assert.equal( hasPattern(persistSource, /ctx\.db\.insert\(\s*["']pageSpeedResults["']/), true, "persistPageSpeedResult should insert into pageSpeedResults.", ); }); test("finishPageSpeedAuditRun writes completion status and finishedAt", () => { const finishSource = extractExportSource("finishPageSpeedAuditRun"); assert.equal( hasPattern(finishSource, /ctx\.db\.patch\(\s*args\.runId,[\s\S]*?finishedAt:\s*now/), true, "finish function should set finishedAt.", ); assert.equal( hasPattern( finishSource, /counters:\s*\{\s*[\s\S]*?errors:\s*args\.errors\s*\?\?/, ), true, "finish function should update counters.", ); assert.equal( hasPattern(finishSource, /currentStep:\s*["']pagespeed_insights["']/), true, "finish function should set currentStep pagespeed_insights.", ); });