From abfc42cfb1f014068dc853112e24b9982abc38ec Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:49:43 +0800 Subject: [PATCH] fix(wasm): distinguish source and internal failures --- .../WasmInterfaceComparisonService.test.ts | 22 ++++++- .../wasm/WasmInterfaceComparisonService.ts | 58 +++++++++++-------- 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/application/wasm/WasmInterfaceComparisonService.test.ts b/src/application/wasm/WasmInterfaceComparisonService.test.ts index 9948c6643a..41917bcfc0 100644 --- a/src/application/wasm/WasmInterfaceComparisonService.test.ts +++ b/src/application/wasm/WasmInterfaceComparisonService.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "vitest"; +import { expect, it, vi } from "vitest"; import { setImmediate as nextTurn } from "node:timers/promises"; import { createEvidence, parseEvidence } from "../../domain/evidence.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; @@ -329,3 +329,23 @@ it("retains the complete comparison when cancelled at the final handoff", async summary: { changed: 1, added: 0, removed: 0, unchanged: 0, unknown: 0 }, }); }); + +it("does not report unexpected output-construction failures as invalid caller Evidence", async () => { + const failure = new Error("comparison output construction failed"); + const parse = vi + .spyOn(wasmInterfaceComparisonSchema, "parse") + .mockImplementation(() => { + throw failure; + }); + try { + await expect( + compareWasmInterfaces({ + left: evidence(facts("f")), + right: evidence(facts("f")), + scope: "both", + }), + ).rejects.toBe(failure); + } finally { + parse.mockRestore(); + } +}); diff --git a/src/application/wasm/WasmInterfaceComparisonService.ts b/src/application/wasm/WasmInterfaceComparisonService.ts index ec6478e551..f7b51d4fd0 100644 --- a/src/application/wasm/WasmInterfaceComparisonService.ts +++ b/src/application/wasm/WasmInterfaceComparisonService.ts @@ -8,11 +8,7 @@ import { compareWasmInterfacesInputSchema, wasmInterfaceComparisonSchema, } from "../../contracts/wasm/wasmComparisonContracts.js"; -import { - createEvidence, - parseEvidence, - type Evidence, -} from "../../domain/evidence.js"; +import { createEvidence, type Evidence } from "../../domain/evidence.js"; import { wasmArtifactSchema, wasmArtifactMatchesSubject, @@ -27,23 +23,30 @@ import { import { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; -import { err, type Result } from "../../domain/result.js"; +import { err, ok, type Result } from "../../domain/result.js"; import { resolvePairedEvidenceRequest, type EvidenceLookup, } from "../EvidenceInputResolver.js"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { z } from "zod"; +import { safeParseEvidence } from "../../domain/evidenceValidation.js"; import { completeProviderEvidence } from "../ProviderOperationExecution.js"; const OPERATION = "compare_wasm_interfaces"; const parseInspection = ( raw: unknown, -): { evidence: Evidence; result: WasmArtifact } => { - const evidence = parseEvidence(raw); +): Result<{ evidence: Evidence; result: WasmArtifact }, AnalysisInputError> => { + const authenticated = safeParseEvidence(raw); + if (!authenticated.ok) return inspectionInputFailure(authenticated.error); + const evidence = authenticated.value; if (evidence.operation !== "inspect_wasm_artifact") - throw new TypeError("Expected complete inspect_wasm_artifact Evidence"); - const result = wasmArtifactSchema.parse(evidence.normalized_result); + return inspectionInputFailure( + new TypeError("Expected complete inspect_wasm_artifact Evidence"), + ); + const parsed = wasmArtifactSchema.safeParse(evidence.normalized_result); + if (!parsed.success) return inspectionInputFailure(parsed.error); + const result = parsed.data; if ( !wasmArtifactMatchesSubject( result.artifact, @@ -56,12 +59,23 @@ const parseInspection = ( ) || evidence.parameters.path !== result.artifact.path ) - throw new TypeError( - "WASM Evidence subject/selection does not match normalized artifact identity", + return inspectionInputFailure( + new TypeError( + "WASM Evidence subject/selection does not match normalized artifact identity", + ), ); - return { evidence, result }; + return ok({ evidence, result }); }; +const inspectionInputFailure = ( + cause: Error, +): Result => + err( + new AnalysisInputError(OPERATION, { cause }, [ + { path: [], reason: "invalid_value", message: cause.message }, + ]), + ); + /** Compare only producer-owned typed facts; no raw WAT or original file is reread. */ export const compareWasmInterfaces = async ( rawInput: unknown, @@ -88,8 +102,12 @@ export const compareWasmInterfaces = async ( try { await setImmediate(); assertActive(); - const left = parseInspection(resolved.value.left); - const right = parseInspection(resolved.value.right); + const parsedLeft = parseInspection(resolved.value.left); + if (!parsedLeft.ok) return parsedLeft; + const parsedRight = parseInspection(resolved.value.right); + if (!parsedRight.ok) return parsedRight; + const left = parsedLeft.value; + const right = parsedRight.value; const scope = parsed.data.scope; const selected = (entry: WasmInterfaceEntry): boolean => scope === "both" || @@ -255,14 +273,6 @@ export const compareWasmInterfaces = async ( return completeProviderEvidence(evidence, options?.signal); } catch (cause: unknown) { if (cause instanceof AnalysisError) return err(cause); - return err( - new AnalysisInputError(OPERATION, { cause }, [ - { - path: [], - reason: "invalid_value", - message: cause instanceof Error ? cause.message : String(cause), - }, - ]), - ); + throw cause; } };