fix(wasm): distinguish source and internal failures

This commit is contained in:
morluto
2026-10-27 13:49:43 +08:00
parent 6941d5ab92
commit abfc42cfb1
2 changed files with 55 additions and 25 deletions
@@ -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();
}
});
@@ -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<never, AnalysisInputError> =>
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;
}
};