From 1ee3b3694c5b3a364df6e1f0a45edd9acf8a2233 Mon Sep 17 00:00:00 2001 From: youzini Date: Sun, 31 May 2026 16:41:46 +0000 Subject: [PATCH] test: guard runtime dependency injection completeness --- package.json | 1 + sync/graph-persistence-io.js | 47 +++-- tests/runtime-deps-completeness.mjs | 296 ++++++++++++++++++++++++++++ 3 files changed, 329 insertions(+), 15 deletions(-) create mode 100644 tests/runtime-deps-completeness.mjs diff --git a/package.json b/package.json index 9b18914..41f9414 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "test:runtime-history": "node tests/runtime-history.mjs", "test:graph-persistence": "node tests/graph-persistence.mjs", "test:index-slicing-ratchet": "node tests/index-slicing-ratchet.mjs", + "test:runtime-deps": "node tests/runtime-deps-completeness.mjs", "test:identity-resolver": "node tests/identity-resolver.mjs", "test:persistence-reducer": "node tests/persistence-reducer.mjs", "test:graph-snapshot-schema": "node tests/graph-snapshot-schema.mjs", diff --git a/sync/graph-persistence-io.js b/sync/graph-persistence-io.js index b1f115c..ad55ce4 100644 --- a/sync/graph-persistence-io.js +++ b/sync/graph-persistence-io.js @@ -21,15 +21,32 @@ function createGraphPersistenceStateProxy(runtime = {}) { }); } -function createRuntimeRef(runtime = {}, name) { - const getter = runtime[`get${name}`]; - const setter = runtime[`set${name}`]; +function createNativeHydrateInstallPromiseRef(runtime = {}) { return { get value() { - return typeof getter === "function" ? getter() : undefined; + return typeof runtime.getNativeHydrateInstallPromise === "function" + ? runtime.getNativeHydrateInstallPromise() + : undefined; }, set value(nextValue) { - if (typeof setter === "function") setter(nextValue); + if (typeof runtime.setNativeHydrateInstallPromise === "function") { + runtime.setNativeHydrateInstallPromise(nextValue); + } + }, + }; +} + +function createNativePersistDeltaInstallPromiseRef(runtime = {}) { + return { + get value() { + return typeof runtime.getNativePersistDeltaInstallPromise === "function" + ? runtime.getNativePersistDeltaInstallPromise() + : undefined; + }, + set value(nextValue) { + if (typeof runtime.setNativePersistDeltaInstallPromise === "function") { + runtime.setNativePersistDeltaInstallPromise(nextValue); + } }, }; } @@ -51,8 +68,8 @@ export async function loadGraphFromIndexedDbImpl(runtime, ) { const graphPersistenceState = createGraphPersistenceStateProxy(runtime); const currentGraph = runtime.getCurrentGraph?.() || null; - const nativeHydrateInstallPromiseRef = createRuntimeRef(runtime, "NativeHydrateInstallPromise"); - const nativePersistDeltaInstallPromiseRef = createRuntimeRef(runtime, "NativePersistDeltaInstallPromise"); + const nativeHydrateInstallPromiseRef = createNativeHydrateInstallPromiseRef(runtime); + const nativePersistDeltaInstallPromiseRef = createNativePersistDeltaInstallPromiseRef(runtime); const bmeIndexedDbLatestQueuedRevisionByChatId = runtime.bmeIndexedDbLatestQueuedRevisionByChatId; const bmeIndexedDbWriteInFlightByChatId = runtime.bmeIndexedDbWriteInFlightByChatId; const updateGraphPersistenceState = runtime.updateGraphPersistenceState || ((patch = {}) => runtime.setGraphPersistenceState?.({ ...(runtime.getGraphPersistenceState?.() || {}), ...(patch || {}) })); @@ -869,8 +886,8 @@ export async function loadGraphFromIndexedDbImpl(runtime, export function maybeFlushQueuedGraphPersistImpl(runtime, reason = "queued-graph-persist") { const graphPersistenceState = createGraphPersistenceStateProxy(runtime); const currentGraph = runtime.getCurrentGraph?.() || null; - const nativeHydrateInstallPromiseRef = createRuntimeRef(runtime, "NativeHydrateInstallPromise"); - const nativePersistDeltaInstallPromiseRef = createRuntimeRef(runtime, "NativePersistDeltaInstallPromise"); + const nativeHydrateInstallPromiseRef = createNativeHydrateInstallPromiseRef(runtime); + const nativePersistDeltaInstallPromiseRef = createNativePersistDeltaInstallPromiseRef(runtime); const bmeIndexedDbLatestQueuedRevisionByChatId = runtime.bmeIndexedDbLatestQueuedRevisionByChatId; const bmeIndexedDbWriteInFlightByChatId = runtime.bmeIndexedDbWriteInFlightByChatId; const updateGraphPersistenceState = runtime.updateGraphPersistenceState || ((patch = {}) => runtime.setGraphPersistenceState?.({ ...(runtime.getGraphPersistenceState?.() || {}), ...(patch || {}) })); @@ -1020,8 +1037,8 @@ export async function retryPendingGraphPersistImpl(runtime, { } = {}) { const graphPersistenceState = createGraphPersistenceStateProxy(runtime); const currentGraph = runtime.getCurrentGraph?.() || null; - const nativeHydrateInstallPromiseRef = createRuntimeRef(runtime, "NativeHydrateInstallPromise"); - const nativePersistDeltaInstallPromiseRef = createRuntimeRef(runtime, "NativePersistDeltaInstallPromise"); + const nativeHydrateInstallPromiseRef = createNativeHydrateInstallPromiseRef(runtime); + const nativePersistDeltaInstallPromiseRef = createNativePersistDeltaInstallPromiseRef(runtime); const bmeIndexedDbLatestQueuedRevisionByChatId = runtime.bmeIndexedDbLatestQueuedRevisionByChatId; const bmeIndexedDbWriteInFlightByChatId = runtime.bmeIndexedDbWriteInFlightByChatId; const updateGraphPersistenceState = runtime.updateGraphPersistenceState || ((patch = {}) => runtime.setGraphPersistenceState?.({ ...(runtime.getGraphPersistenceState?.() || {}), ...(patch || {}) })); @@ -1342,8 +1359,8 @@ export async function saveGraphToIndexedDbImpl(runtime, ) { const graphPersistenceState = createGraphPersistenceStateProxy(runtime); const currentGraph = runtime.getCurrentGraph?.() || null; - const nativeHydrateInstallPromiseRef = createRuntimeRef(runtime, "NativeHydrateInstallPromise"); - const nativePersistDeltaInstallPromiseRef = createRuntimeRef(runtime, "NativePersistDeltaInstallPromise"); + const nativeHydrateInstallPromiseRef = createNativeHydrateInstallPromiseRef(runtime); + const nativePersistDeltaInstallPromiseRef = createNativePersistDeltaInstallPromiseRef(runtime); const bmeIndexedDbLatestQueuedRevisionByChatId = runtime.bmeIndexedDbLatestQueuedRevisionByChatId; const bmeIndexedDbWriteInFlightByChatId = runtime.bmeIndexedDbWriteInFlightByChatId; const updateGraphPersistenceState = runtime.updateGraphPersistenceState || ((patch = {}) => runtime.setGraphPersistenceState?.({ ...(runtime.getGraphPersistenceState?.() || {}), ...(patch || {}) })); @@ -2399,8 +2416,8 @@ export function queueGraphPersistToIndexedDbImpl(runtime, ) { const graphPersistenceState = createGraphPersistenceStateProxy(runtime); const currentGraph = runtime.getCurrentGraph?.() || null; - const nativeHydrateInstallPromiseRef = createRuntimeRef(runtime, "NativeHydrateInstallPromise"); - const nativePersistDeltaInstallPromiseRef = createRuntimeRef(runtime, "NativePersistDeltaInstallPromise"); + const nativeHydrateInstallPromiseRef = createNativeHydrateInstallPromiseRef(runtime); + const nativePersistDeltaInstallPromiseRef = createNativePersistDeltaInstallPromiseRef(runtime); const bmeIndexedDbLatestQueuedRevisionByChatId = runtime.bmeIndexedDbLatestQueuedRevisionByChatId; const bmeIndexedDbWriteInFlightByChatId = runtime.bmeIndexedDbWriteInFlightByChatId; const updateGraphPersistenceState = runtime.updateGraphPersistenceState || ((patch = {}) => runtime.setGraphPersistenceState?.({ ...(runtime.getGraphPersistenceState?.() || {}), ...(patch || {}) })); diff --git a/tests/runtime-deps-completeness.mjs b/tests/runtime-deps-completeness.mjs new file mode 100644 index 0000000..9c841d8 --- /dev/null +++ b/tests/runtime-deps-completeness.mjs @@ -0,0 +1,296 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +const CHECKS = [ + { + modulePath: "sync/graph-persistence-io.js", + builderName: "createGraphPersistenceIoRuntime", + }, + { + modulePath: "sync/graph-load-persist.js", + builderName: "createGraphLoadPersistRuntime", + }, + { + modulePath: "sync/graph-mutation-gate.js", + builderName: "createGraphMutationGateRuntime", + }, +]; + +function readProjectFile(relativePath) { + return readFileSync(new URL(`../${relativePath}`, import.meta.url), "utf8"); +} + +function stripCommentsAndStrings(source) { + let output = ""; + let i = 0; + + const appendSpacePreservingNewlines = (text) => { + output += text.replace(/[^\n\r]/g, " "); + }; + + while (i < source.length) { + const char = source[i]; + const next = source[i + 1]; + + if (char === "/" && next === "/") { + const start = i; + i += 2; + while (i < source.length && source[i] !== "\n" && source[i] !== "\r") i += 1; + appendSpacePreservingNewlines(source.slice(start, i)); + continue; + } + + if (char === "/" && next === "*") { + const start = i; + i += 2; + while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i += 1; + i = Math.min(source.length, i + 2); + appendSpacePreservingNewlines(source.slice(start, i)); + continue; + } + + if (char === '"' || char === "'") { + const quote = char; + const start = i; + i += 1; + while (i < source.length) { + if (source[i] === "\\") { + i += 2; + continue; + } + if (source[i] === quote) { + i += 1; + break; + } + i += 1; + } + appendSpacePreservingNewlines(source.slice(start, i)); + continue; + } + + if (char === "`") { + const start = i; + i += 1; + while (i < source.length) { + if (source[i] === "\\") { + i += 2; + continue; + } + if (source[i] === "`") { + i += 1; + break; + } + i += 1; + } + appendSpacePreservingNewlines(source.slice(start, i)); + continue; + } + + output += char; + i += 1; + } + + return output; +} + +function findMatchingBrace(strippedSource, openIndex) { + if (strippedSource[openIndex] !== "{") { + throw new Error(`Expected opening brace at offset ${openIndex}`); + } + + let depth = 0; + for (let i = openIndex; i < strippedSource.length; i += 1) { + if (strippedSource[i] === "{") depth += 1; + if (strippedSource[i] === "}") { + depth -= 1; + if (depth === 0) return i; + } + } + + throw new Error(`No matching closing brace for offset ${openIndex}`); +} + +function extractRuntimeKeys(moduleSource) { + const stripped = stripCommentsAndStrings(moduleSource); + const unsupportedComputedAccess = /\bruntime\s*(?:\?\.)?\s*\[/.exec(stripped); + if (unsupportedComputedAccess) { + throw new Error( + `Unsupported computed runtime dependency access near offset ${unsupportedComputedAccess.index}. ` + + "Use direct runtime.someDependency access so completeness can be checked.", + ); + } + const keys = new Set(); + const runtimePropertyPattern = /\bruntime\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)/g; + let match = null; + + while ((match = runtimePropertyPattern.exec(stripped))) { + keys.add(match[1]); + } + + return keys; +} + +function extractBuilderReturnObjectRange(indexSource, builderName) { + const stripped = stripCommentsAndStrings(indexSource); + const functionPattern = new RegExp(`\\bfunction\\s+${builderName}\\s*\\(`); + const functionMatch = functionPattern.exec(stripped); + if (!functionMatch) { + throw new Error(`Could not locate builder function ${builderName}`); + } + + const functionBodyOpen = stripped.indexOf("{", functionMatch.index + functionMatch[0].length); + if (functionBodyOpen < 0) { + throw new Error(`Could not locate function body for ${builderName}`); + } + const functionBodyClose = findMatchingBrace(stripped, functionBodyOpen); + + const returnPattern = /\breturn\b/g; + returnPattern.lastIndex = functionBodyOpen + 1; + let returnMatch = null; + + while ((returnMatch = returnPattern.exec(stripped)) && returnMatch.index < functionBodyClose) { + let cursor = returnMatch.index + returnMatch[0].length; + while (cursor < functionBodyClose && /\s/.test(stripped[cursor])) cursor += 1; + if (stripped[cursor] === "{") { + const objectOpen = cursor; + const objectClose = findMatchingBrace(stripped, objectOpen); + if (objectClose > functionBodyClose) { + throw new Error(`Return object for ${builderName} extends beyond function body`); + } + return { open: objectOpen, close: objectClose, stripped }; + } + } + + throw new Error(`Could not locate returned object literal for ${builderName}`); +} + +function splitTopLevelObjectEntries(strippedSource, open, close) { + const entries = []; + let depth = 0; + let entryStart = open + 1; + + for (let i = open + 1; i < close; i += 1) { + const char = strippedSource[i]; + if (char === "{" || char === "(" || char === "[") depth += 1; + if (char === "}" || char === ")" || char === "]") depth -= 1; + if (char === "," && depth === 0) { + entries.push(strippedSource.slice(entryStart, i)); + entryStart = i + 1; + } + } + + entries.push(strippedSource.slice(entryStart, close)); + return entries; +} + +function extractBuilderProvidedKeys(indexSource, builderName) { + const { open, close, stripped } = extractBuilderReturnObjectRange(indexSource, builderName); + const entries = splitTopLevelObjectEntries(stripped, open, close); + const keys = new Set(); + + for (const rawEntry of entries) { + const entry = rawEntry.trim(); + if (!entry || entry.startsWith("...")) continue; + + const keyedEntryMatch = /^([A-Za-z_$][\w$]*)\s*:/.exec(entry); + if (keyedEntryMatch) { + keys.add(keyedEntryMatch[1]); + continue; + } + + const methodEntryMatch = /^([A-Za-z_$][\w$]*)\s*\(/.exec(entry); + if (methodEntryMatch) { + keys.add(methodEntryMatch[1]); + continue; + } + + const shorthandEntryMatch = /^([A-Za-z_$][\w$]*)$/.exec(entry); + if (shorthandEntryMatch) { + keys.add(shorthandEntryMatch[1]); + } + } + + return keys; +} + +function diffSets(left, right) { + return [...left].filter((value) => !right.has(value)).sort(); +} + +function assertRuntimeDepsComplete({ modulePath, builderName, moduleSource, indexSource }) { + const requiredKeys = extractRuntimeKeys(moduleSource); + const providedKeys = extractBuilderProvidedKeys(indexSource, builderName); + const missingKeys = diffSets(requiredKeys, providedKeys); + + if (missingKeys.length > 0) { + throw new Error( + `${modulePath} requires runtime deps missing from ${builderName}: ${missingKeys.join(", ")}`, + ); + } + + return { + requiredKeys, + providedKeys, + unusedProvidedKeys: diffSets(providedKeys, requiredKeys), + }; +} + +function runExtractorSelfChecks() { + const runtimeKeys = extractRuntimeKeys(` + runtime.used(); + runtime.value?.(); + runtime?.optional; + // runtime.commentOnly(); + const stringOnly = "runtime.stringOnly"; + const templateOnly = ` + "`runtime.templateOnly`" + `; + `); + assert.deepEqual([...runtimeKeys].sort(), ["optional", "used", "value"]); + assert.throws( + () => extractRuntimeKeys("runtime[dynamicKey]();"), + /Unsupported computed runtime dependency access/, + ); + + const syntheticIndex = ` + function createSyntheticRuntime() { + return { + used, + value: () => ({ nested: true }), + extra: (input) => { + return { input }; + }, + }; + } + `; + const providedKeys = extractBuilderProvidedKeys(syntheticIndex, "createSyntheticRuntime"); + assert.deepEqual([...providedKeys].sort(), ["extra", "used", "value"]); + + assert.throws( + () => + assertRuntimeDepsComplete({ + modulePath: "synthetic.js", + builderName: "createSyntheticRuntime", + moduleSource: "runtime.used(); runtime.missing();", + indexSource: syntheticIndex, + }), + /synthetic\.js requires runtime deps missing from createSyntheticRuntime: missing/, + ); +} + +runExtractorSelfChecks(); + +const indexSource = readProjectFile("index.js"); +for (const check of CHECKS) { + const result = assertRuntimeDepsComplete({ + ...check, + moduleSource: readProjectFile(check.modulePath), + indexSource, + }); + + if (result.unusedProvidedKeys.length > 0) { + console.info( + `[runtime-deps] ${check.builderName} provides unused keys for ${check.modulePath}: ${result.unusedProvidedKeys.join(", ")}`, + ); + } +} + +console.log("runtime dependency completeness checks passed");