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\S]*audit,[\s\S]*lead,[\s\S]*sourceSummaries:[\s\S]*}/, "getDetail should return audit, lead, and sourceSummaries.", ); hasPattern( getDetailSource, /query\("auditFindings"\)[\s\S]*withIndex\("by_auditId"[\s\S]*eq\("auditId",\s*audit\._id\)[\s\S]*take\(DETAIL_EVIDENCE_LIMIT\)/, "getDetail should load persisted findings by auditId.", ); hasPattern( getDetailSource, /return\s*{[\s\S]*audit,[\s\S]*lead,[\s\S]*findings,[\s\S]*sourceSummaries:[\s\S]*}/, "getDetail should return top-level findings for the detail UI.", ); hasPattern( sourceFile.getFullText(), /export const getDetail = query\(/, "audits.ts should export a getDetail query.", ); }); test("audits.getDetail joins compact checked-page evidence from latest successful enrichment", () => { const getDetailSource = extractExportSource("getDetail"); hasPattern( getDetailSource, /query\("agentRuns"\)[\s\S]*withIndex\("by_type_and_status_and_leadId"[\s\S]*eq\("type",\s*"website_enrichment"\)[\s\S]*eq\("status",\s*"succeeded"\)[\s\S]*eq\("leadId",\s*audit\.leadId\)[\s\S]*order\("desc"\)[\s\S]*take\(1\)/, "getDetail should locate the latest successful website_enrichment run for the audit lead.", ); for (const table of [ "websiteCrawlPages", "websiteTechnicalChecks", "websiteCrawlScreenshots", ]) { hasPattern( getDetailSource, new RegExp( `query\\("${table}"\\)[\\s\\S]*withIndex\\("by_runId"[\\s\\S]*eq\\("runId",\\s*enrichmentRunId\\)[\\s\\S]*take\\(DETAIL_EVIDENCE_LIMIT\\)`, ), `${table} should be loaded from the bounded enrichment run evidence window.`, ); } hasPattern( getDetailSource, /audit\.checkedPages\.map\(/, "getDetail should preserve audit.checkedPages as the canonical display order.", ); hasPattern( getDetailSource, /fallbackCheckedPageEvidence/, "getDetail should return checked-page fallback rows when enrichment evidence is missing.", ); hasPattern( getDetailSource, /ctx\.storage\.getUrl\(screenshot\.storageId\)/, "getDetail should resolve screenshot storage ids to display URLs.", ); hasPattern( getDetailSource, /sourceSummaries:\s*{\s*checkedPages/, "getDetail should expose checked page summaries under sourceSummaries.checkedPages.", ); });