From f4b2ba13133b6897c45a2c86d507fd4f5ef80e43 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:55:02 +0800 Subject: [PATCH] refactor(javascript): own runtime reconciliation failures Return expected source and semantic failures from the runtime input owner as tagged Results. Bind source schema validation to concrete schemas, preserve nested active-capture paths, and let unexpected interpretation and derivation failures reach the adapter boundary. Remove broad protocol-error wrappers, duplicate active identity validation, and redundant JSON result parsing. Migrate parsed-fact consumers and retain CLI/MCP contracts. Relevant source and public-boundary tests pass; six injected implementation-fault controls fail on the prior revision. Live Electron behavior was not exercised. --- ...JavaScriptRuntimeReconciliationEvidence.ts | 3 +- .../JavaScriptRuntimeReconciliationService.ts | 43 ++--- .../javascriptApplicationWorkflowExamples.ts | 16 +- .../javascriptRuntimeReconciliation.ts | 25 ++- .../javascriptRuntimeReconciliationActive.ts | 62 ++++--- ...riptRuntimeReconciliationCaptureParsing.ts | 69 ++++---- ...iptRuntimeReconciliationInputValidation.ts | 59 +++++-- .../javascriptRuntimeReconciliationParsing.ts | 11 +- .../javascriptRuntimeContentPriority.test.ts | 20 ++- .../javascriptRuntimeReconciliation.test.ts | 18 ++- .../javascriptRuntimeUnresolvedTarget.test.ts | 13 +- ...riptRuntimeReconciliationAmbiguity.test.ts | 39 +++-- .../derivedOperationFailures.test.ts | 151 ++++++++++++++++++ 13 files changed, 358 insertions(+), 171 deletions(-) diff --git a/src/application/javascript/JavaScriptRuntimeReconciliationEvidence.ts b/src/application/javascript/JavaScriptRuntimeReconciliationEvidence.ts index afbc2d07cc..20b1b3dfd0 100644 --- a/src/application/javascript/JavaScriptRuntimeReconciliationEvidence.ts +++ b/src/application/javascript/JavaScriptRuntimeReconciliationEvidence.ts @@ -7,7 +7,6 @@ import type { JavaScriptRuntimeReconciliationResult, ReconcileJavaScriptRuntimeInput, } from "../../domain/javascript/javascriptRuntimeReconciliationSchemas.js"; -import { jsonValueSchema } from "../../domain/jsonValue.js"; import { JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER } from "../InvestigationProviders.js"; /** Create derived Evidence for one exact set of static/runtime inputs. */ @@ -19,7 +18,7 @@ export const createJavaScriptRuntimeReconciliationEvidence = ( predicateType: "rea.javascript-runtime-reconciliation", operation: "reconcile_javascript_runtime", parameters: parameters(input), - result: jsonValueSchema.parse(result), + result, rawResult: null, confidence: "inferred", authority: "analyst-inference", diff --git a/src/application/javascript/JavaScriptRuntimeReconciliationService.ts b/src/application/javascript/JavaScriptRuntimeReconciliationService.ts index 6c2d0dbcfe..923d8b9b92 100644 --- a/src/application/javascript/JavaScriptRuntimeReconciliationService.ts +++ b/src/application/javascript/JavaScriptRuntimeReconciliationService.ts @@ -1,14 +1,12 @@ import { resolve } from "node:path"; import { z } from "zod"; -import { AnalysisProtocolError } from "../../domain/analysisErrorCore.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { Evidence } from "../../domain/evidence.js"; import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { - parseRuntimeReconciliationInput, + readRuntimeReconciliationInput, reconcileJavaScriptRuntime, - type ParsedRuntimeReconciliationInput, } from "../../domain/javascript/javascriptRuntimeReconciliation.js"; import { reconcileJavaScriptRuntimeInputSchema } from "../../domain/javascript/javascriptRuntimeReconciliationSchemas.js"; import { err, ok, type Result } from "../../domain/result.js"; @@ -32,37 +30,14 @@ export const reconcileJavaScriptRuntimeEvidence = ( export const reconcileJavaScriptRuntimeEvidenceValidated = ( parsedInput: z.output, ): Result => { - let parsed: ParsedRuntimeReconciliationInput; - try { - parsed = parseRuntimeReconciliationInput( - normalizeRuntimeMappingRoots(parsedInput), - ); - } catch (cause: unknown) { - if (cause instanceof z.ZodError) - return err( - analysisInputErrorFromIssues(OPERATION, cause.issues, parsedInput), - ); - return err( - new AnalysisProtocolError( - "JavaScript runtime reconciliation input parsing failed", - { cause }, - ), - ); - } - - try { - const result = reconcileJavaScriptRuntime(parsed); - return ok( - createJavaScriptRuntimeReconciliationEvidence(parsed.input, result), - ); - } catch (cause: unknown) { - return err( - new AnalysisProtocolError( - "JavaScript runtime reconciliation produced an invalid result", - { cause }, - ), - ); - } + const parsed = readRuntimeReconciliationInput( + normalizeRuntimeMappingRoots(parsedInput), + ); + if (!parsed.ok) return parsed; + const result = reconcileJavaScriptRuntime(parsed.value); + return ok( + createJavaScriptRuntimeReconciliationEvidence(parsed.value.input, result), + ); }; const normalizeRuntimeMappingRoots = ( diff --git a/src/contracts/javascript/javascriptApplicationWorkflowExamples.ts b/src/contracts/javascript/javascriptApplicationWorkflowExamples.ts index 29d5b1bf24..95217bc81d 100644 --- a/src/contracts/javascript/javascriptApplicationWorkflowExamples.ts +++ b/src/contracts/javascript/javascriptApplicationWorkflowExamples.ts @@ -1,6 +1,6 @@ import { createEvidence } from "../../domain/evidence.js"; import { - parseRuntimeReconciliationInput, + readRuntimeReconciliationInput, reconcileJavaScriptRuntime, } from "../../domain/javascript/javascriptRuntimeReconciliation.js"; import { @@ -10,13 +10,17 @@ import { import { createHistoricalSourceGraph } from "../../domain/referenceSourceGraph.js"; import { reconcileJavaScriptRuntimeInputSchema } from "../../domain/javascript/javascriptRuntimeReconciliationSchemas.js"; -const reconciliation = reconcileJavaScriptRuntime( - parseRuntimeReconciliationInput( - reconcileJavaScriptRuntimeInputSchema.parse( - JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE, - ), +const reconciliationInput = readRuntimeReconciliationInput( + reconcileJavaScriptRuntimeInputSchema.parse( + JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE, ), ); +if (!reconciliationInput.ok) + throw new Error( + "The JavaScript runtime reconciliation contract example is invalid", + { cause: reconciliationInput.error }, + ); +const reconciliation = reconcileJavaScriptRuntime(reconciliationInput.value); const reconciliationEvidence = createEvidence( undefined, { diff --git a/src/domain/javascript/javascriptRuntimeReconciliation.ts b/src/domain/javascript/javascriptRuntimeReconciliation.ts index 6ad77385a5..35b7478e7e 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliation.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliation.ts @@ -14,6 +14,8 @@ import type { JavaScriptRuntimeReconciliationResult } from "./javascriptRuntimeR import type { ReconcileJavaScriptRuntimeInput } from "./javascriptRuntimeReconciliationSchemas.js"; import { projectRuntimeCaptures } from "./javascriptRuntimeReconciliationRuntime.js"; import { collectStaticRuntimeCandidates } from "./javascriptRuntimeStaticCandidates.js"; +import { AnalysisInputError } from "../analysisErrorCore.js"; +import { err, ok, type Result } from "../result.js"; /** Verified caller input and the static and runtime facts parsed from it. */ export interface ParsedRuntimeReconciliationInput { @@ -23,15 +25,22 @@ export interface ParsedRuntimeReconciliationInput { } /** Parse caller Evidence once into the owner consumed by reconciliation. */ -export const parseRuntimeReconciliationInput = ( +export const readRuntimeReconciliationInput = ( input: ReconcileJavaScriptRuntimeInput, -): ParsedRuntimeReconciliationInput => ({ - input, - layers: parseStaticLayers(input.static_layers, ["static_layers"]), - captures: parseRuntimeCaptures(input.runtime_observations, [ - "runtime_observations", - ]), -}); +): Result => { + try { + return ok({ + input, + layers: parseStaticLayers(input.static_layers, ["static_layers"]), + captures: parseRuntimeCaptures(input.runtime_observations, [ + "runtime_observations", + ]), + }); + } catch (cause: unknown) { + if (cause instanceof AnalysisInputError) return err(cause); + throw cause; + } +}; /** Reconcile static JAG layers with authorized passive CDP Evidence. */ export const reconcileJavaScriptRuntime = ( diff --git a/src/domain/javascript/javascriptRuntimeReconciliationActive.ts b/src/domain/javascript/javascriptRuntimeReconciliationActive.ts index 738cbc0644..30ed475fd7 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationActive.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationActive.ts @@ -4,8 +4,9 @@ import { digestCanonicalValue } from "../canonicalDigest.js"; import { isPathWithinRoot } from "../localPath.js"; import { z } from "zod"; import { + assertEvidenceIdentity, invalidInput, - parseAtPath, + parseInputAtPath, } from "./javascriptRuntimeReconciliationInputValidation.js"; import { classifyBrowserCompleteness } from "../browserCompleteness.js"; @@ -19,24 +20,35 @@ import type { ParsedRuntimeCapture } from "./javascriptRuntimeReconciliationCapt /** Parse active Electron Evidence as a bounded, target-only runtime capture. */ export const parseActiveElectronCapture = ( evidence: Evidence, + path: readonly (string | number)[], ): ParsedRuntimeCapture => { - assertIdentity(evidence); - const result = parseAtPath( - (value) => electronActiveObservationResultSchema.parse(value), - evidence.normalized_result, - ["normalized_result"], + assertEvidenceIdentity( + evidence, + { + operation: "capture_electron_scenario", + predicate: "rea.electron-active-scenario", + providerId: "rea-playwright-electron-active", + providerName: "REA Playwright active Electron observation provider", + providerVersion: "1", + authority: "controlled-replay", + confidence: "observed", + }, + path, ); - const parameters = parseAtPath( - (value) => - z - .object({ - application_path: absolutePathSchema, - application_root: absolutePathSchema, - }) - .passthrough() - .parse(value), + const result = parseInputAtPath( + electronActiveObservationResultSchema, + evidence.normalized_result, + [...path, "normalized_result"], + ); + const parameters = parseInputAtPath( + z + .object({ + application_path: absolutePathSchema, + application_root: absolutePathSchema, + }) + .passthrough(), evidence.parameters, - ["parameters"], + [...path, "parameters"], ); if ( result.application.application_path !== parameters.application_path || @@ -46,7 +58,7 @@ export const parseActiveElectronCapture = ( ) ) throw invalidInput( - ["normalized_result", "application", "application_path"], + [...path, "normalized_result", "application", "application_path"], "Active Electron Evidence application path disagrees with its configured root", ); return { @@ -60,22 +72,6 @@ export const parseActiveElectronCapture = ( const absolutePathSchema = z.string().min(1).refine(isAbsolute); -const assertIdentity = (evidence: Evidence): void => { - if ( - evidence.operation !== "capture_electron_scenario" || - evidence.predicate_type !== "rea.electron-active-scenario" || - evidence.provider.id !== "rea-playwright-electron-active" || - evidence.provider.name !== - "REA Playwright active Electron observation provider" || - evidence.provider.version !== "1" || - evidence.authority !== "controlled-replay" || - evidence.confidence !== "observed" - ) - throw invalidInput( - [], - "Evidence does not match the supported capture_electron_scenario contract", - ); -}; const normalizeInspection = ( result: ElectronActiveObservationResult, ): Extract< diff --git a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts index a384b2e35e..066f4acd00 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts @@ -26,12 +26,13 @@ import { } from "./javascriptRuntimeObservation.js"; import { digestCanonicalValue } from "../canonicalDigest.js"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; -import { parseEvidence, type Evidence } from "../evidence.js"; +import type { Evidence } from "../evidence.js"; import { parseActiveElectronCapture } from "./javascriptRuntimeReconciliationActive.js"; import { assertEvidenceIdentity, + authenticateRuntimeEvidence, invalidInput, - parseAtPath, + parseInputAtPath, } from "./javascriptRuntimeReconciliationInputValidation.js"; interface RuntimeCaptureBase { @@ -142,7 +143,7 @@ const parseRuntimeCapture = ( input: Evidence, evidencePath: readonly (string | number)[], ): ParsedRuntimeCapture => { - const evidence = parseAtPath(parseEvidence, input, evidencePath); + const evidence = authenticateRuntimeEvidence(input, evidencePath); if (evidence.operation === "inspect_web_page") return parseBrowserCapture(evidence, evidencePath); if (evidence.operation === "inspect_electron_page") @@ -150,11 +151,7 @@ const parseRuntimeCapture = ( if (evidence.operation === "observe_javascript_runtime") return parseV8Capture(evidence, evidencePath); if (evidence.operation === "capture_electron_scenario") { - return parseAtPath( - () => parseActiveElectronCapture(evidence), - evidence, - evidencePath, - ); + return parseActiveElectronCapture(evidence, evidencePath); } throw invalidInput( evidencePath, @@ -179,8 +176,8 @@ const parseBrowserCapture = ( }, path, ); - const inspection = parseAtPath( - (value) => webPageInspectionSchema.parse(value), + const inspection = parseInputAtPath( + webPageInspectionSchema, evidence.normalized_result, [...path, "normalized_result"], ); @@ -222,8 +219,8 @@ const parseElectronCapture = ( }, path, ); - const inspection = parseAtPath( - (value) => electronPageInspectionSchema.parse(value), + const inspection = parseInputAtPath( + electronPageInspectionSchema, evidence.normalized_result, [...path, "normalized_result"], ); @@ -264,8 +261,8 @@ const parseV8Capture = ( }, path, ); - const result = parseAtPath( - (value) => javascriptRuntimeObservationSchema.parse(value), + const result = parseInputAtPath( + javascriptRuntimeObservationSchema, evidence.normalized_result, [...path, "normalized_result"], ); @@ -284,16 +281,14 @@ const assertV8RuntimeParameters = ( result: JavaScriptRuntimeObservation, path: readonly (string | number)[], ): void => { - const parameters = parseAtPath( - (value) => - z - .object({ - inspector_endpoint: browserEndpointSchema, - target_id: z.string().trim().min(1), - runtime_kind: javascriptRuntimeKindSchema.optional(), - }) - .passthrough() - .parse(value), + const parameters = parseInputAtPath( + z + .object({ + inspector_endpoint: browserEndpointSchema, + target_id: z.string().trim().min(1), + runtime_kind: javascriptRuntimeKindSchema.optional(), + }) + .passthrough(), evidence.parameters, [...path, "parameters"], ); @@ -450,16 +445,14 @@ const assertRuntimeParameters = ( include_script_sources: z.boolean(), }; if (expected.kind === "browser") { - const parameters = parseAtPath( - (value) => - z - .object({ - ...common, - cdp_endpoint: recordedBrowserObservationEndpointSchema, - allowed_origins: browserAllowedOriginsSchema, - }) - .passthrough() - .parse(value), + const parameters = parseInputAtPath( + z + .object({ + ...common, + cdp_endpoint: recordedBrowserObservationEndpointSchema, + allowed_origins: browserAllowedOriginsSchema, + }) + .passthrough(), evidence.parameters, [...path, "parameters"], ); @@ -480,12 +473,8 @@ const assertRuntimeParameters = ( ); return; } - const parameters = parseAtPath( - (value) => - z - .object({ ...common, cdp_endpoint: browserEndpointSchema }) - .passthrough() - .parse(value), + const parameters = parseInputAtPath( + z.object({ ...common, cdp_endpoint: browserEndpointSchema }).passthrough(), evidence.parameters, [...path, "parameters"], ); diff --git a/src/domain/javascript/javascriptRuntimeReconciliationInputValidation.ts b/src/domain/javascript/javascriptRuntimeReconciliationInputValidation.ts index 7b303b3e44..2a1328f7c3 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationInputValidation.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationInputValidation.ts @@ -1,33 +1,47 @@ import { z } from "zod"; -import type { Evidence } from "../evidence.js"; -/** Prefix caller paths on a nested Zod validation boundary. */ -export const parseAtPath = ( - parse: (input: unknown) => Value, +import { AnalysisInputError } from "../analysisErrorCore.js"; +import { projectInputIssues } from "../inputIssueProjection.js"; +import { parseEvidence, type Evidence } from "../evidence.js"; + +const OPERATION = "reconcile_javascript_runtime"; + +/** Parse one caller-owned source value and prefix failures with its request path. */ +export const parseInputAtPath = ( + schema: Schema, input: unknown, path: readonly (string | number)[], -): Value => { +): z.output => { + const parsed = schema.safeParse(input); + if (!parsed.success) + throw inputErrorAtPath(parsed.error.issues, input, path, parsed.error); + return parsed.data; +}; + +/** Authenticate Evidence while preserving the reconciliation input contract. */ +export const authenticateRuntimeEvidence = ( + input: unknown, + path: readonly (string | number)[], +): Evidence => { try { - return parse(input); + return parseEvidence(input); } catch (cause: unknown) { if (cause instanceof z.ZodError) - throw new z.ZodError( - cause.issues.map((issue) => ({ - ...issue, - path: [...path, ...issue.path], - })), - ); + throw inputErrorAtPath(cause.issues, input, path, cause); throw cause; } }; -/** Represent one semantic caller constraint as a standard validation issue. */ +/** Represent one cross-field caller constraint as a standard input issue. */ export const invalidInput = ( path: readonly (string | number)[], message: string, -): z.ZodError => new z.ZodError([{ code: "custom", path: [...path], message }]); +): AnalysisInputError => + new AnalysisInputError(OPERATION, undefined, [ + { path: [...path], reason: "invalid_value", message }, + ]); -/** Verify an authenticated Evidence record against one runtime workflow. */ +/** Verify authenticated Evidence against one supported producer contract. */ export const assertEvidenceIdentity = ( evidence: Evidence, expected: { @@ -55,3 +69,18 @@ export const assertEvidenceIdentity = ( `Evidence does not match the supported ${expected.operation} contract`, ); }; + +const inputErrorAtPath = ( + issues: readonly z.core.$ZodIssue[], + input: unknown, + path: readonly (string | number)[], + cause: unknown, +): AnalysisInputError => + new AnalysisInputError( + OPERATION, + { cause }, + projectInputIssues(issues, input).map((issue) => ({ + ...issue, + path: [...path, ...issue.path], + })), + ); diff --git a/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts b/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts index afae449989..928e004459 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts @@ -1,6 +1,6 @@ import { digestCanonicalValue } from "../canonicalDigest.js"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; -import { parseEvidence, type Evidence } from "../evidence.js"; +import type { Evidence } from "../evidence.js"; import { javascriptApplicationAnalysisResultSchema, type JavaScriptApplicationAnalysisResult, @@ -9,8 +9,9 @@ import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js import type { ReconcileJavaScriptRuntimeInput } from "./javascriptRuntimeReconciliationSchemas.js"; import { assertEvidenceIdentity, + authenticateRuntimeEvidence, invalidInput, - parseAtPath, + parseInputAtPath, } from "./javascriptRuntimeReconciliationInputValidation.js"; type StaticLayerInput = @@ -43,9 +44,9 @@ const parseStaticLayer = ( layer: StaticLayerInput, evidencePath: readonly (string | number)[], ): ParsedStaticLayer => { - const evidence = parseAtPath(parseEvidence, layer.analysis, evidencePath); - const result = parseAtPath( - (value) => javascriptApplicationAnalysisResultSchema.parse(value), + const evidence = authenticateRuntimeEvidence(layer.analysis, evidencePath); + const result = parseInputAtPath( + javascriptApplicationAnalysisResultSchema, evidence.normalized_result, [...evidencePath, "normalized_result"], ); diff --git a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts index 867df5b05c..af9e5a50e8 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { - parseRuntimeReconciliationInput, + readRuntimeReconciliationInput, reconcileJavaScriptRuntime, } from "../../../../src/domain/javascript/javascriptRuntimeReconciliation.js"; import { reconcileRuntimeEntities } from "../../../../src/domain/javascript/javascriptRuntimeReconciliationMatching.js"; @@ -22,6 +22,14 @@ import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js const CAPTURED = "export const observed = 'different';\n"; +const readInput = ( + input: Parameters[0], +) => { + const result = readRuntimeReconciliationInput(input); + if (!result.ok) throw result.error; + return result.value; +}; + const collisionInput = async ( alternatives: number, explicitMapping: boolean, @@ -33,7 +41,7 @@ const collisionInput = async ( : root; const extra = await createTestTempDirectory("rea-runtime-content-extra-"); await writeFile(join(extra, "app.js"), CAPTURED); - return parseRuntimeReconciliationInput( + return readInput( reconcileJavaScriptRuntimeInputSchema.parse({ static_layers: [ { @@ -106,7 +114,7 @@ it("restricts content-and-location ambiguity to mapped candidates", async () => const live = await createTestTempDirectory("rea-runtime-content-mapped-"); const analysis = await analyzeFixture(application); const mapping = [{ kind: "file-root", root: live, artifact_prefix: "" }]; - const parsed = parseRuntimeReconciliationInput( + const parsed = readInput( reconcileJavaScriptRuntimeInputSchema.parse({ static_layers: [ { role: "application", analysis, runtime_mappings: mapping }, @@ -148,7 +156,7 @@ it.each([false, true])( "rea-runtime-content-unlocated-", ); const result = reconcileJavaScriptRuntime( - parseRuntimeReconciliationInput( + readInput( reconcileJavaScriptRuntimeInputSchema.parse({ static_layers: [ { @@ -245,7 +253,7 @@ const reconcileCapturedScript = async (input: { readonly includeWorker?: boolean; }) => { const result = reconcileJavaScriptRuntime( - parseRuntimeReconciliationInput( + readInput( reconcileJavaScriptRuntimeInputSchema.parse({ static_layers: await Promise.all( input.layers.map(async ({ root, mapping }, index) => ({ @@ -385,7 +393,7 @@ it.each([ "applies URL-prefix mapping presence to captured script %s", async (url, status) => { const root = await applicationFixture(); - const parsed = parseRuntimeReconciliationInput( + const parsed = readInput( reconcileJavaScriptRuntimeInputSchema.parse({ static_layers: [ { diff --git a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts index d6e25854a2..e0da9f01bf 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts @@ -13,7 +13,7 @@ import { mappedRuntimePaths } from "../../../../src/domain/javascript/javascript import { projectRuntimeCaptures } from "../../../../src/domain/javascript/javascriptRuntimeReconciliationRuntime.js"; import { electronPageInspectionSchema } from "../../../../src/domain/javascript/electronObservation.js"; import { - parseRuntimeReconciliationInput, + readRuntimeReconciliationInput, reconcileJavaScriptRuntime, } from "../../../../src/domain/javascript/javascriptRuntimeReconciliation.js"; import { reconcileJavaScriptRuntimeInputSchema } from "../../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js"; @@ -28,12 +28,16 @@ import { electronRuntimeEvidence, } from "../../../fixtures/javascriptRuntimeReconciliation.js"; -const reconcileInput = (input: unknown) => - reconcileJavaScriptRuntime( - parseRuntimeReconciliationInput( - reconcileJavaScriptRuntimeInputSchema.parse(input), - ), +const readInput = (input: unknown) => { + const result = readRuntimeReconciliationInput( + reconcileJavaScriptRuntimeInputSchema.parse(input), ); + if (!result.ok) throw result.error; + return result.value; +}; + +const reconcileInput = (input: unknown) => + reconcileJavaScriptRuntime(readInput(input)); it("preserves source-capture input paths for runtime Evidence", async () => { const root = await applicationFixture(); @@ -504,7 +508,7 @@ it.skipIf(process.platform === "win32")( "export const marker = 42;\n", ); const analysis = await analyzeFixture(root); - const parsed = parseRuntimeReconciliationInput( + const parsed = readInput( reconcileJavaScriptRuntimeInputSchema.parse({ static_layers: [ { diff --git a/tests/boundary/inspector/javascriptRuntimeUnresolvedTarget.test.ts b/tests/boundary/inspector/javascriptRuntimeUnresolvedTarget.test.ts index a5a91239f8..cc3755a35b 100644 --- a/tests/boundary/inspector/javascriptRuntimeUnresolvedTarget.test.ts +++ b/tests/boundary/inspector/javascriptRuntimeUnresolvedTarget.test.ts @@ -8,19 +8,20 @@ import { analyzeJavaScriptApplication } from "../../support/javascriptApplicatio import { createJavaScriptRuntimeObservationEvidence } from "../../../src/application/javascript/JavaScriptRuntimeObservationEvidence.js"; import { V8InspectorProvider } from "../../../src/inspector/V8InspectorProvider.js"; import { - parseRuntimeReconciliationInput, + readRuntimeReconciliationInput, reconcileJavaScriptRuntime, } from "../../../src/domain/javascript/javascriptRuntimeReconciliation.js"; import { reconcileJavaScriptRuntimeInputSchema } from "../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js"; import { startFakeV8Inspector } from "../../fixtures/inspector/fakeV8Inspector.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -const reconcileInput = (input: unknown) => - reconcileJavaScriptRuntime( - parseRuntimeReconciliationInput( - reconcileJavaScriptRuntimeInputSchema.parse(input), - ), +const reconcileInput = (input: unknown) => { + const result = readRuntimeReconciliationInput( + reconcileJavaScriptRuntimeInputSchema.parse(input), ); + if (!result.ok) throw result.error; + return reconcileJavaScriptRuntime(result.value); +}; test("keeps unresolved target identity unknown while matching verified loaded scripts", async () => { const root = await createTestTempDirectory("rea-unresolved-runtime-"); diff --git a/tests/boundary/process/javascriptRuntimeReconciliationAmbiguity.test.ts b/tests/boundary/process/javascriptRuntimeReconciliationAmbiguity.test.ts index 3604b862ad..81adfd6f88 100644 --- a/tests/boundary/process/javascriptRuntimeReconciliationAmbiguity.test.ts +++ b/tests/boundary/process/javascriptRuntimeReconciliationAmbiguity.test.ts @@ -8,7 +8,7 @@ import { afterEach, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { - parseRuntimeReconciliationInput, + readRuntimeReconciliationInput, reconcileJavaScriptRuntime, } from "../../../src/domain/javascript/javascriptRuntimeReconciliation.js"; import { reconcileJavaScriptRuntimeInputSchema } from "../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js"; @@ -20,12 +20,13 @@ import { electronRuntimeEvidence, } from "../../fixtures/javascriptRuntimeReconciliation.js"; -const reconcileInput = (input: unknown) => - reconcileJavaScriptRuntime( - parseRuntimeReconciliationInput( - reconcileJavaScriptRuntimeInputSchema.parse(input), - ), +const reconcileInput = (input: unknown) => { + const result = readRuntimeReconciliationInput( + reconcileJavaScriptRuntimeInputSchema.parse(input), ); + if (!result.ok) throw result.error; + return reconcileJavaScriptRuntime(result.value); +}; const execute = promisify(execFile); @@ -133,12 +134,32 @@ it("rejects runtime source bytes whose Evidence omits source-capture approval", sourceIncluded: false, }); - expect(() => - reconcileInput({ + const result = readRuntimeReconciliationInput( + reconcileJavaScriptRuntimeInputSchema.parse({ static_layers: [{ role: "application", analysis: staticEvidence }], runtime_observations: [contradictoryRuntime], }), - ).toThrow(/source-capture selection/u); + ); + expect(result).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisInputError", + operation: "reconcile_javascript_runtime", + issues: [ + { + path: [ + "runtime_observations", + 0, + "parameters", + "include_script_sources", + ], + reason: "invalid_value", + message: + "Runtime Evidence contains source without source-capture selection", + }, + ], + }, + }); }); it("keeps graph omission counts unknown when a runtime section is unavailable", async () => { diff --git a/tests/process-global/derivedOperationFailures.test.ts b/tests/process-global/derivedOperationFailures.test.ts index 0f03c86eb7..b42d1496ef 100644 --- a/tests/process-global/derivedOperationFailures.test.ts +++ b/tests/process-global/derivedOperationFailures.test.ts @@ -49,6 +49,12 @@ import { applicationFeatureTraceResultSchema } from "../../src/domain/javascript import { ownedApplicationVersionComparisonResultSchema } from "../../src/domain/javascript/javascriptApplicationVersionComparisonSchemas.js"; import { javaScriptExportShapeComparisonResultSchema } from "../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { sourceToBundleComparisonResultSchema } from "../../src/domain/javascript/sourceToBundleComparisonSchemas.js"; +import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js"; +import { javascriptRuntimeReconciliationResultSchema } from "../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js"; +import { browserCompletenessSchema } from "../../src/domain/browserCompleteness.js"; +import { createElectronActiveEvidence } from "../../src/application/javascript/ElectronActiveEvidence.js"; +import { electronActiveObservationInputSchema } from "../../src/domain/javascript/electronActiveObservation.js"; +import { createElectronActiveObservationFixtureResult } from "../../src/domain/javascript/electronActiveObservation.fixture.js"; import { nativeFunctionMatchingSchema } from "../../src/domain/native/functionMatching.js"; import { FunctionByteCoverage } from "../../src/domain/native/functionByteCoverage.js"; @@ -207,6 +213,151 @@ const withSdk = async ( } }; +const activeRuntimeFixture = () => { + const root = join(process.cwd(), "active-runtime-fixture"); + const applicationPath = join(root, "main.js"); + const result = createElectronActiveObservationFixtureResult(applicationPath); + const evidence = createElectronActiveEvidence( + { + ...electronActiveObservationInputSchema.parse({ + executable_path: process.execPath, + application_path: applicationPath, + application_root: root, + }), + application_root: root, + }, + result, + { + id: "rea-playwright-electron-active", + name: "REA Playwright active Electron observation provider", + version: "1", + }, + ); + return { evidence, result }; +}; + +describe("runtime reconciliation failure ownership through the public SDK", () => { + afterEach(() => vi.restoreAllMocks()); + + it.each(unexpectedResultFailures)( + "preserves an unexpected %s from generated reconciliation output", + async (_label, createFailure) => { + await withSdk(async ({ client, session }) => { + const failure = createFailure(); + vi.spyOn( + javascriptRuntimeReconciliationResultSchema, + "parse", + ).mockImplementation(() => { + throw failure; + }); + const response = await client.callTool({ + name: "reconcile_javascript_runtime", + arguments: JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE, + }); + expectUnexpectedToolFailure(response, failure); + expect(session.exportEvidenceBundle()).toMatchObject({ + records: [], + unknowns: [], + }); + }); + }, + ); + + it.each(unexpectedResultFailures)( + "preserves an unexpected %s while interpreting an authenticated active capture", + async (_label, createFailure) => { + await withSdk(async ({ client, session }) => { + const { evidence: runtime } = activeRuntimeFixture(); + const failure = createFailure(); + vi.spyOn(browserCompletenessSchema, "parse").mockImplementation(() => { + throw failure; + }); + const response = await client.callTool({ + name: "reconcile_javascript_runtime", + arguments: { + static_layers: + JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE.static_layers, + runtime_observations: [runtime], + }, + }); + expectUnexpectedToolFailure(response, failure); + expect(session.exportEvidenceBundle()).toMatchObject({ + records: [], + unknowns: [], + }); + }); + }, + ); + + it.each([ + { + field: "payload", + path: ["normalized_result", "application", "application_path"], + reason: "invalid_type", + }, + { + field: "parameters", + path: ["parameters", "application_root"], + reason: "invalid_type", + }, + { + field: "scope", + path: ["normalized_result", "application", "application_path"], + reason: "invalid_value", + }, + ])( + "keeps an invalid active $field at its caller path", + async ({ field, path, reason }) => { + await withSdk(async ({ client, session }) => { + const { evidence, result } = activeRuntimeFixture(); + const runtime = createEvidence(undefined, evidence.provider, { + operation: evidence.operation, + predicateType: evidence.predicate_type, + authority: evidence.authority, + confidence: evidence.confidence, + parameters: + field === "parameters" + ? { ...evidence.parameters, application_root: 3 } + : evidence.parameters, + result: + field === "parameters" + ? result + : { + ...result, + application: { + ...result.application, + application_path: + field === "payload" + ? 3 + : join(process.cwd(), "another-application", "main.js"), + }, + }, + }); + const response = await client.callTool({ + name: "reconcile_javascript_runtime", + arguments: { + static_layers: + JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE.static_layers, + runtime_observations: [runtime], + }, + }); + expect(response.isError).toBe(true); + expect(parseMcpToolError(response).error).toMatchObject({ + code: "invalid_request", + details: { + operation: "reconcile_javascript_runtime", + issues: [{ path: ["runtime_observations", 0, ...path], reason }], + }, + }); + expect(session.exportEvidenceBundle()).toMatchObject({ + records: [], + unknowns: [], + }); + }); + }, + ); +}); + describe("derived operation failures through the public SDK", () => { afterEach(() => vi.restoreAllMocks());