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", ); type ExactSetEquality = [ Exclude, ] extends [never] ? [Exclude] extends [never] ? true : false : false; type AssertPageSpeedStrategy = "mobile" | "desktop"; type AssertPageSpeedResultStatus = "succeeded" | "failed"; type AssertPageSpeedErrorType = | "quota" | "timeout" | "unavailable" | "invalid_url" | "api_error" | "unknown"; type PageSpeedStrategyParity = ExactSetEquality< AssertPageSpeedStrategy, ("mobile" | "desktop") >; type PageSpeedResultStatusParity = ExactSetEquality< AssertPageSpeedResultStatus, "succeeded" | "failed" >; type PageSpeedErrorTypeParity = ExactSetEquality< AssertPageSpeedErrorType, "quota" | "timeout" | "unavailable" | "invalid_url" | "api_error" | "unknown" >; const _assertPageSpeedStrategyParity: PageSpeedStrategyParity = true; const _assertPageSpeedResultStatusParity: PageSpeedResultStatusParity = true; const _assertPageSpeedErrorTypeParity: PageSpeedErrorTypeParity = 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 i = objectStart; i < schemaSource.length; i += 1) { if (schemaSource[i] === "{") { depth += 1; } else if (schemaSource[i] === "}") { depth -= 1; if (depth === 0) { objectEnd = i; 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("PageSpeed validator unions are declared", () => { assert.equal(_assertPageSpeedStrategyParity, true); assert.equal(_assertPageSpeedResultStatusParity, true); assert.equal(_assertPageSpeedErrorTypeParity, true); assertHas( /const\s+pageSpeedStrategy\s*=\s*v\.union\(\s*[\s\S]*v\.literal\(\s*["']mobile["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']desktop["']\s*\)[\s\S]*\)/, schemaSource, "Schema should define pageSpeedStrategy union with mobile and desktop.", ); assertHas( /const\s+pageSpeedResultStatus\s*=\s*v\.union\(\s*[\s\S]*v\.literal\(\s*["']succeeded["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']failed["']\s*\)[\s\S]*\)/, schemaSource, "Schema should define pageSpeedResultStatus union with succeeded and failed.", ); assertHas( /const\s+pageSpeedErrorType\s*=\s*v\.union\(\s*[\s\S]*v\.literal\(\s*["']quota["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']timeout["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']unavailable["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']invalid_url["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']api_error["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']unknown["']\s*\)[\s\S]*\)/, schemaSource, "Schema should define pageSpeedErrorType union with all declared values.", ); }); test("pageSpeedResults table has contract fields and indexes", () => { const { section, objectBlock } = extractTableSection("pageSpeedResults"); assertHas( /leadId:\s*v\.id\(["']leads["']\)/, objectBlock, "pageSpeedResults.leadId should be required lead id.", ); assertHas( /auditId:\s*v\.optional\(\s*v\.id\(["']audits["']\)\s*\)/, objectBlock, "pageSpeedResults.auditId should be optional audit id.", ); assertHas( /runId:\s*v\.optional\(\s*v\.id\(["']agentRuns["']\)\s*\)/, objectBlock, "pageSpeedResults.runId should be optional run id.", ); assertHas( /strategy:\s*pageSpeedStrategy/, objectBlock, "pageSpeedResults.strategy should use pageSpeedStrategy validator.", ); assertHas( /status:\s*pageSpeedResultStatus/, objectBlock, "pageSpeedResults.status should use pageSpeedResultStatus validator.", ); assertHas( /sourceUrl:\s*v\.string\(\)/, objectBlock, "pageSpeedResults.sourceUrl should be required.", ); assertHas( /finalUrl:\s*v\.optional\(\s*v\.string\(\)\s*\)/, objectBlock, "pageSpeedResults.finalUrl should be optional string.", ); assertHas( /rawStorageId:\s*v\.optional\(\s*v\.id\(["']_storage["']\)\s*\)/, objectBlock, "pageSpeedResults.rawStorageId should be optional storage id.", ); assertHas( /errorType:\s*v\.optional\(\s*pageSpeedErrorType\s*\)/, objectBlock, "pageSpeedResults.errorType should be optional error type.", ); assertHas( /errorSummary:\s*v\.optional\(\s*v\.string\(\)\s*\)/, objectBlock, "pageSpeedResults.errorSummary should be optional.", ); assertHas( /fetchedAt:\s*v\.number\(\)/, objectBlock, "pageSpeedResults.fetchedAt should be required.", ); assertHas( /createdAt:\s*v\.number\(\)/, objectBlock, "pageSpeedResults.createdAt should be required.", ); assertHas( /scores:\s*v\.optional\(\s*v\.object\([\s\S]*?performance:\s*v\.optional\(v\.number\(\)\)[\s\S]*?accessibility:\s*v\.optional\(v\.number\(\)\)[\s\S]*?bestPractices:\s*v\.optional\(v\.number\(\)\)[\s\S]*?seo:\s*v\.optional\(v\.number\(\)\)[\s\S]*?\)\s*\)/, objectBlock, "pageSpeedResults.normalized.scores should include expected keys.", ); assertHas( /metrics:\s*v\.optional\(\s*v\.object\([\s\S]*?firstContentfulPaintMs:\s*v\.optional\(v\.number\(\)\)[\s\S]*?largestContentfulPaintMs:\s*v\.optional\(v\.number\(\)\)[\s\S]*?cumulativeLayoutShift:\s*v\.optional\(v\.number\(\)\)[\s\S]*?totalBlockingTimeMs:\s*v\.optional\(v\.number\(\)\)[\s\S]*?speedIndexMs:\s*v\.optional\(v\.number\(\)\)[\s\S]*?\)\s*\)/, objectBlock, "pageSpeedResults.normalized.metrics should include expected keys.", ); assertHas( /opportunities:\s*v\.optional\(\s*v\.array\(v\.string\(\)\)\s*\)/, objectBlock, "pageSpeedResults.normalized.opportunities should be optional string array.", ); assertHas( /implications:\s*v\.optional\(\s*v\.array\(v\.string\(\)\)\s*\)/, objectBlock, "pageSpeedResults.normalized.implications should be optional string array.", ); assertHas( /index\("by_leadId",\s*\["leadId"\]\)/, section, "pageSpeedResults should have by_leadId index.", ); assertHas( /index\("by_runId",\s*\["runId"\]\)/, section, "pageSpeedResults should have by_runId index.", ); assertHas( /index\("by_auditId",\s*\["auditId"\]\)/, section, "pageSpeedResults should have by_auditId index.", ); assertHas( /index\("by_leadId_and_strategy",\s*\["leadId",\s*"strategy"\]\)/, section, "pageSpeedResults should have by_leadId_and_strategy index.", ); }); test("audits should not include public raw PageSpeed/Lighthouse JSON fields", () => { const { objectBlock } = extractTableSection("audits"); const hasPublicRawJson = /raw.*pagespeed|pagespeed.*raw|raw.*lighthouse|lighthouse.*raw/i.test( objectBlock, ); assert.equal( hasPublicRawJson, false, "audits should not expose raw PageSpeed/Lighthouse JSON fields.", ); });