292 lines
8.2 KiB
TypeScript
292 lines
8.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
|
|
const schemaSource = readFileSync(
|
|
join(process.cwd(), "convex", "schema.ts"),
|
|
"utf8",
|
|
);
|
|
const domainSource = readFileSync(
|
|
join(process.cwd(), "convex", "domain.ts"),
|
|
"utf8",
|
|
);
|
|
|
|
function extractTableSection(tableName: string) {
|
|
const marker = `${tableName}: defineTable({`;
|
|
const markerIndex = schemaSource.indexOf(marker);
|
|
assert.notEqual(
|
|
markerIndex,
|
|
-1,
|
|
`Expected schema table definition for ${tableName}.`,
|
|
);
|
|
|
|
const objectStart = schemaSource.indexOf("{", markerIndex);
|
|
let depth = 0;
|
|
let objectEnd = -1;
|
|
|
|
for (let index = objectStart; index < schemaSource.length; index += 1) {
|
|
if (schemaSource[index] === "{") {
|
|
depth += 1;
|
|
} else if (schemaSource[index] === "}") {
|
|
depth -= 1;
|
|
if (depth === 0) {
|
|
objectEnd = index;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
assert.notEqual(objectEnd, -1, `Could not parse schema object for ${tableName}.`);
|
|
|
|
const remainder = schemaSource.slice(objectEnd + 1);
|
|
const nextTableMatch = remainder.match(
|
|
/^\s*[a-zA-Z_][\w]*:\s*defineTable\(/m,
|
|
);
|
|
|
|
const sectionEnd =
|
|
nextTableMatch === null
|
|
? schemaSource.length
|
|
: objectEnd + 1 + nextTableMatch.index!;
|
|
|
|
const section = schemaSource.slice(markerIndex, sectionEnd);
|
|
const objectBlock = schemaSource.slice(markerIndex, objectEnd + 1);
|
|
|
|
return { section, objectBlock };
|
|
}
|
|
|
|
function assertHas(pattern: RegExp, source: string, message: string) {
|
|
assert.equal(pattern.test(source), true, message);
|
|
}
|
|
|
|
test("auditGenerations table has contract fields", () => {
|
|
const { section, objectBlock } = extractTableSection("auditGenerations");
|
|
|
|
assertHas(
|
|
/leadId:\s*v\.id\(["']leads["']\)/,
|
|
objectBlock,
|
|
"auditGenerations.leadId must be required lead id.",
|
|
);
|
|
assertHas(
|
|
/auditId:\s*v\.optional\(\s*v\.id\(["']audits["']\)\s*\)/,
|
|
objectBlock,
|
|
"auditGenerations.auditId should be optional audit id.",
|
|
);
|
|
assertHas(
|
|
/runId:\s*v\.id\(["']agentRuns["']\)/,
|
|
objectBlock,
|
|
"auditGenerations.runId should be required agent run id.",
|
|
);
|
|
assertHas(
|
|
/stage:\s*auditGenerationStage/,
|
|
objectBlock,
|
|
"auditGenerations.stage should use auditGenerationStage validator.",
|
|
);
|
|
assertHas(
|
|
/modelProfile:\s*v\.string\(\)/,
|
|
objectBlock,
|
|
"auditGenerations.modelProfile should be required string.",
|
|
);
|
|
assertHas(
|
|
/modelId:\s*v\.string\(\)/,
|
|
objectBlock,
|
|
"auditGenerations.modelId should be required string.",
|
|
);
|
|
assertHas(
|
|
/prompt:\s*v\.string\(\)/,
|
|
objectBlock,
|
|
"auditGenerations.prompt should be required string.",
|
|
);
|
|
assertHas(
|
|
/systemPrompt:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
|
|
objectBlock,
|
|
"auditGenerations.systemPrompt should be optional string.",
|
|
);
|
|
assertHas(
|
|
/rawResponse:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
|
|
objectBlock,
|
|
"auditGenerations.rawResponse should be optional string.",
|
|
);
|
|
assertHas(
|
|
/parsedJson:\s*v\.optional\(\s*auditGenerationParsedJson\s*\)/,
|
|
objectBlock,
|
|
"auditGenerations.parsedJson should allow string or structured object.",
|
|
);
|
|
assertHas(
|
|
/usage:\s*v\.optional\(\s*auditGenerationUsage\s*\)/,
|
|
objectBlock,
|
|
"auditGenerations.usage should be optional token usage object.",
|
|
);
|
|
assertHas(
|
|
/finishReason:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
|
|
objectBlock,
|
|
"auditGenerations.finishReason should be optional string.",
|
|
);
|
|
assertHas(
|
|
/status:\s*auditGenerationStatus/,
|
|
objectBlock,
|
|
"auditGenerations.status should use auditGenerationStatus validator.",
|
|
);
|
|
assertHas(
|
|
/errorSummary:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
|
|
objectBlock,
|
|
"auditGenerations.errorSummary should be optional string.",
|
|
);
|
|
assertHas(
|
|
/createdAt:\s*v\.number\(\)/,
|
|
objectBlock,
|
|
"auditGenerations.createdAt should be required number.",
|
|
);
|
|
assertHas(
|
|
/updatedAt:\s*v\.number\(\)/,
|
|
objectBlock,
|
|
"auditGenerations.updatedAt should be required number.",
|
|
);
|
|
|
|
assertHas(
|
|
/index\("by_leadId",\s*\["leadId"\]\)/,
|
|
section,
|
|
"auditGenerations should have by_leadId index.",
|
|
);
|
|
assertHas(
|
|
/index\("by_auditId",\s*\["auditId"\]\)/,
|
|
section,
|
|
"auditGenerations should have by_auditId index.",
|
|
);
|
|
assertHas(
|
|
/index\("by_runId",\s*\["runId"\]\)/,
|
|
section,
|
|
"auditGenerations should have by_runId index.",
|
|
);
|
|
assertHas(
|
|
/index\("by_stage",\s*\["stage"\]\)/,
|
|
section,
|
|
"auditGenerations should have by_stage index.",
|
|
);
|
|
assertHas(
|
|
/index\("by_leadId_and_stage",\s*\["leadId",\s*"stage"\]\)/,
|
|
section,
|
|
"auditGenerations should have by_leadId_and_stage index.",
|
|
);
|
|
});
|
|
|
|
test("audit-generation validators are declared", () => {
|
|
assertHas(
|
|
/const\s+auditGenerationStage\s*=\s*v\.union\([\s\S]*\)/,
|
|
schemaSource,
|
|
"schema should define auditGenerationStage union.",
|
|
);
|
|
assertHas(
|
|
/const\s+auditGenerationStatus\s*=\s*v\.union\([\s\S]*\)/,
|
|
schemaSource,
|
|
"schema should define auditGenerationStatus union.",
|
|
);
|
|
assertHas(
|
|
/AUDIT_GENERATION_STAGES\s*=\s*\[[\s\S]*["']classification["'][\s\S]*\]/,
|
|
domainSource,
|
|
"auditGenerationStage should include classification.",
|
|
);
|
|
assertHas(
|
|
/AUDIT_GENERATION_STAGES\s*=\s*\[[\s\S]*["']multimodalAudit["'][\s\S]*\]/,
|
|
domainSource,
|
|
"auditGenerationStage should include multimodalAudit.",
|
|
);
|
|
assertHas(
|
|
/AUDIT_GENERATION_STAGES\s*=\s*\[[\s\S]*["']germanCopy["'][\s\S]*\]/,
|
|
domainSource,
|
|
"auditGenerationStage should include germanCopy.",
|
|
);
|
|
assertHas(
|
|
/AUDIT_GENERATION_STAGES\s*=\s*\[[\s\S]*["']qualityReview["'][\s\S]*\]/,
|
|
domainSource,
|
|
"auditGenerationStage should include qualityReview.",
|
|
);
|
|
});
|
|
|
|
test("auditFindings table stores verified specialist findings with evidence refs", () => {
|
|
const { section, objectBlock } = extractTableSection("auditFindings");
|
|
|
|
assertHas(
|
|
/auditId:\s*v\.id\(["']audits["']\)/,
|
|
objectBlock,
|
|
"auditFindings.auditId must be required audit id.",
|
|
);
|
|
assertHas(
|
|
/runId:\s*v\.id\(["']agentRuns["']\)/,
|
|
objectBlock,
|
|
"auditFindings.runId must be required run id.",
|
|
);
|
|
assertHas(
|
|
/skillId:\s*v\.string\(\)/,
|
|
objectBlock,
|
|
"auditFindings.skillId must identify the source specialist skill.",
|
|
);
|
|
assertHas(
|
|
/claim:\s*v\.string\(\)/,
|
|
objectBlock,
|
|
"auditFindings.claim must store the verified claim.",
|
|
);
|
|
assertHas(
|
|
/recommendation:\s*v\.string\(\)/,
|
|
objectBlock,
|
|
"auditFindings.recommendation must store the concrete fix.",
|
|
);
|
|
assertHas(
|
|
/customerBenefit:\s*v\.string\(\)/,
|
|
objectBlock,
|
|
"auditFindings.customerBenefit must store customer-facing impact.",
|
|
);
|
|
assertHas(
|
|
/severity:\s*v\.union\(\s*v\.literal\(1\),\s*v\.literal\(2\),\s*v\.literal\(3\)\s*\)/,
|
|
objectBlock,
|
|
"auditFindings.severity should be a 1-3 literal union.",
|
|
);
|
|
assertHas(
|
|
/confidence:\s*v\.number\(\)/,
|
|
objectBlock,
|
|
"auditFindings.confidence must be persisted for review calibration.",
|
|
);
|
|
assertHas(
|
|
/evidenceRefs:\s*v\.array\(\s*auditFindingEvidenceRef\s*\)/,
|
|
objectBlock,
|
|
"auditFindings.evidenceRefs must persist typed evidence refs.",
|
|
);
|
|
assertHas(
|
|
/reviewStatus:\s*auditFindingReviewStatus/,
|
|
objectBlock,
|
|
"auditFindings.reviewStatus should use a review-status validator.",
|
|
);
|
|
assertHas(
|
|
/index\("by_auditId",\s*\["auditId"\]\)/,
|
|
section,
|
|
"auditFindings should have by_auditId index.",
|
|
);
|
|
assertHas(
|
|
/index\("by_runId",\s*\["runId"\]\)/,
|
|
section,
|
|
"auditFindings should have by_runId index.",
|
|
);
|
|
assertHas(
|
|
/index\("by_auditId_and_reviewStatus",\s*\["auditId",\s*"reviewStatus"\]\)/,
|
|
section,
|
|
"auditFindings should support review-status filtering per audit.",
|
|
);
|
|
});
|
|
|
|
test("specialist fan-out audit stages are declared in domain", () => {
|
|
for (const stage of [
|
|
"localSeoSpecialist",
|
|
"conversionUxSpecialist",
|
|
"visualTrustSpecialist",
|
|
"critiqueSpecialist",
|
|
"performanceAccessibilitySpecialist",
|
|
"evidenceVerifier",
|
|
]) {
|
|
assertHas(
|
|
new RegExp(`AUDIT_GENERATION_STAGES\\s*=\\s*\\[[\\s\\S]*["']${stage}["'][\\s\\S]*\\]`),
|
|
domainSource,
|
|
`auditGenerationStage should include ${stage}.`,
|
|
);
|
|
}
|
|
});
|