Files
pitchfast/tests/audit-skills-schema.test.ts

237 lines
6.4 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import test from "node:test";
import ts from "typescript";
const schemaPath = join(process.cwd(), "convex", "schema.ts");
const auditsPath = join(process.cwd(), "convex", "audits.ts");
const schemaSource = readFileSync(schemaPath, "utf8");
const auditsSource = readFileSync(auditsPath, "utf8");
const sourceFile = ts.createSourceFile(
"audits.ts",
auditsSource,
ts.ScriptTarget.ES2022,
true,
);
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 objectBlock = schemaSource.slice(objectStart, objectEnd + 1);
return { objectBlock };
}
function extractExportSource(name: string) {
const marker = `export const ${name} = `;
const declarationIndex = auditsSource.indexOf(marker);
assert.notEqual(
declarationIndex,
-1,
`Expected declaration for ${name}.`,
);
const openBraceIndex = auditsSource.indexOf("{", declarationIndex);
let depth = 0;
let end = -1;
for (let index = openBraceIndex; index < auditsSource.length; index += 1) {
const char = auditsSource[index];
if (char === "{") {
depth += 1;
} else if (char === "}") {
depth -= 1;
if (depth === 0) {
end = index;
break;
}
}
}
assert.notEqual(
end,
-1,
`Expected balanced braces for export ${name}.`,
);
return auditsSource.slice(openBraceIndex, end + 1);
}
function extractFieldSection(source: string, fieldName: string, nextFieldName: string) {
const match = source.match(
new RegExp(
`${fieldName}:\\s*v\\.optional\\([\\s\\S]*?(?=\\s*${nextFieldName}:)`,
),
);
assert.notEqual(
match,
null,
`Expected ${fieldName} field with expected object structure in schema.`,
);
return match![0];
}
function hasPattern(source: string, pattern: RegExp, message: string) {
assert.equal(pattern.test(source), true, message);
}
test("audits schema stores compact usedSkills metadata", () => {
const { objectBlock } = extractTableSection("audits");
const usedSkillsSection = extractFieldSection(
objectBlock,
"usedSkills",
"skillSummaries",
);
const skillSummariesSection = extractFieldSection(
objectBlock,
"skillSummaries",
"multimodalSummary",
);
hasPattern(usedSkillsSection, /usedSkills:\s*v\.optional\(/, "usedSkills should be optional.");
hasPattern(
usedSkillsSection,
/name:\s*v\.string\(\)/,
"usedSkills.name should be string.",
);
hasPattern(
usedSkillsSection,
/id:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
"usedSkills.id should be optional string.",
);
hasPattern(
usedSkillsSection,
/category:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
"usedSkills.category should be optional string.",
);
hasPattern(
usedSkillsSection,
/version:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
"usedSkills.version should be optional string.",
);
hasPattern(
usedSkillsSection,
/source:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
"usedSkills.source should be optional string.",
);
hasPattern(
usedSkillsSection,
/v\.array\(/,
"usedSkills should be an optional array of objects.",
);
hasPattern(
usedSkillsSection,
/v\.object\(/,
"usedSkills should be defined with v.object fields.",
);
hasPattern(skillSummariesSection, /skillSummaries:/, "skillSummaries should still exist.");
hasPattern(
skillSummariesSection,
/name:\s*v\.string\(\)/,
"skillSummaries.name should stay string.",
);
hasPattern(
skillSummariesSection,
/purpose:\s*v\.string\(\)/,
"skillSummaries.purpose should stay string.",
);
hasPattern(
skillSummariesSection,
/summary:\s*v\.string\(\)/,
"skillSummaries.summary should stay string.",
);
});
test("audits.create accepts usedSkills validator and persists metadata payloads", () => {
const createSource = extractExportSource("create");
hasPattern(
auditsSource,
/const usedSkillsValidator\s*=\s*v\.array\(/,
"audits.ts should define a reusable usedSkillsValidator.",
);
hasPattern(
auditsSource,
/v\.object\([\s\S]*?id:\s*v\.optional\(\s*v\.string\(\)\s*\)[\s\S]*?name:\s*v\.string\(\)[\s\S]*?category:\s*v\.optional\(\s*v\.string\(\)\s*\)[\s\S]*?version:\s*v\.optional\(\s*v\.string\(\)\s*\)[\s\S]*?source:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
"audits.ts should define reusable v3-compatible usedSkillsValidator fields.",
);
hasPattern(
createSource,
/usedSkills:\s*v\.optional\(usedSkillsValidator\)/,
"create args should include optional usedSkills field.",
);
hasPattern(
createSource,
/ctx\.db\.insert\(\s*["']audits["'][\s\S]*?args[\s\S]*\}/,
"create should persist audit payload from args (so usedSkills is stored when provided).",
);
});
test("audits.getDetail returns audit + lead context with null-safe lead lookup", () => {
const getDetailSource = extractExportSource("getDetail");
hasPattern(
getDetailSource,
/args:\s*{[\s\S]*id:\s*v\.id\(["']audits["']\)[\s\S]*}/,
"getDetail should require id argument for audits.",
);
hasPattern(
getDetailSource,
/const\s+audit\s*=\s*await\s+ctx\.db\.get\s*\(\s*args\.id\s*\)/,
"getDetail should load audit by id.",
);
hasPattern(
getDetailSource,
/if\s*\(\s*!audit\s*\)\s*{\s*return null;\s*}/,
"getDetail should return null when audit is missing.",
);
hasPattern(
getDetailSource,
/const\s+lead\s*=\s*await\s+ctx\.db\.get\s*\(\s*audit\.leadId\s*\)/,
"getDetail should load lead by leadId from the audit.",
);
hasPattern(
getDetailSource,
/return\s*{\s*audit,\s*lead\s*}/,
"getDetail should return { audit, lead }.",
);
hasPattern(
sourceFile.getFullText(),
/export const getDetail = query\(/,
"audits.ts should export a getDetail query.",
);
});