273 lines
8.0 KiB
TypeScript
273 lines
8.0 KiB
TypeScript
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<string>();
|
|
|
|
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 = [
|
|
"getLeadAuditStartStates",
|
|
"requestLeadAudit",
|
|
"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("requestLeadAudit is a public authenticated mutation that queues PageSpeed only after user intent", () => {
|
|
const source = extractExportSource("requestLeadAudit");
|
|
|
|
assert.equal(
|
|
hasPattern(pageSpeedSource, /export const requestLeadAudit = mutation\s*\(/),
|
|
true,
|
|
"requestLeadAudit should be a public mutation for UI-triggered audit starts.",
|
|
);
|
|
assert.match(source, /requireOperator\(ctx\)/);
|
|
assert.match(source, /queueLeadPageSpeedAuditForLead/);
|
|
assert.match(source, /triggeredBy:\s*"manual"/);
|
|
assert.match(source, /Audit-Start wurde manuell angefordert\./);
|
|
});
|
|
|
|
test("getLeadAuditStartStates exposes active audit run status for lead review buttons", () => {
|
|
const source = extractExportSource("getLeadAuditStartStates");
|
|
|
|
assert.equal(
|
|
hasPattern(pageSpeedSource, /export const getLeadAuditStartStates = query\s*\(/),
|
|
true,
|
|
"getLeadAuditStartStates should be a public query.",
|
|
);
|
|
assert.match(source, /requireOperator\(ctx\)/);
|
|
assert.match(source, /leadIds:\s*v\.array\(v\.id\("leads"\)\)/);
|
|
assert.match(pageSpeedSource, /by_type_and_status_and_leadId/);
|
|
assert.match(source, /canStart/);
|
|
});
|
|
|
|
test("queueLeadPageSpeedAudit dedupes per lead and schedules audit workflow", () => {
|
|
const queueSource = pageSpeedSource;
|
|
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\.auditWorkflow\.startLeadAuditWorkflow,\s*\{[\s\S]*?runId/,
|
|
),
|
|
true,
|
|
"queueLeadPageSpeedAudit must schedule internal.auditWorkflow.startLeadAuditWorkflow 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.",
|
|
);
|
|
});
|