mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(wasm): retain acquired inputs across failed inspection
Keep completed module and candidate identities, glue observations, and the admitted tool profile when WABT validation or later inspection fails. Recover stable reads from their existing owner when descriptor finalization fails; mark unparsed glue explicitly and preserve validation uncertainty. Forward partial Evidence through output/cleanup errors and preserve pre-abort reasons. Exercise producer failures, partial acquisition, decoding, finalization and cleanup retry with simulated owned WABT commands. Public CLI and stdio MCP coverage follows the retained Evidence reference through get_evidence_bundle. Real WABT 1.0.42 and the Windows shell-fixture lane were not verified.
This commit is contained in:
1 parent
149b43ee61
commit
aced9fa9b4
4 files changed
+489
-59
No files matched your search
@@ -71,6 +71,19 @@ selected byte SHA-256 and size, section payload ranges, full header/detail outpu
|
||||
and its UTF-8 SHA-256. Producer format drift and invalid or truncated modules
|
||||
are failures, with validation diagnostics and partial Evidence retaining
|
||||
selected byte identity and tool provenance.
|
||||
If inspection stops after selected files were read, partial Evidence also keeps
|
||||
the completed candidate file identities and glue parse observations. A stable
|
||||
read remains observed when descriptor finalization later fails; cleanup still
|
||||
requires retry. Partial Evidence for a glue read whose finalization fails
|
||||
before decoding retains its digest with `not-performed` parse status. Invalid
|
||||
UTF-8 retains the digest with failed parse status. Neither case invents
|
||||
references, and a failed file read contributes no invented facts.
|
||||
These independent observations do not establish that the selected WASM module is
|
||||
valid: partial results state whether WABT validation is unknown, rejected the
|
||||
module, or succeeded before a later inspection failure.
|
||||
CLI errors include this partial Evidence inline. MCP errors can return its
|
||||
`retained-evidence` reference in `details.partial_observation`; use
|
||||
`get_evidence_bundle` with `detail: "complete"` to retrieve the matching record.
|
||||
|
||||
Core validation remains authoritative when an optional custom section cannot
|
||||
be interpreted. WABT warning output is preserved in `custom_section_warnings`;
|
||||
|
||||
@@ -436,6 +436,29 @@ it("returns actionable unavailable without creating a workspace when tools are a
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves a pre-aborted caller reason without acquiring WASM facts", async () => {
|
||||
const reason = { source: "caller cancelled before provider admission" };
|
||||
const controller = new AbortController();
|
||||
controller.abort(reason);
|
||||
const createRuntime = vi.fn(() => {
|
||||
throw new Error("Pre-aborted inspection must not create a workspace");
|
||||
});
|
||||
const provider = new WabtArtifactProvider({}, undefined, createRuntime);
|
||||
try {
|
||||
const result = await provider.inspect(
|
||||
{ path: "/not-read.wasm", glue_paths: [], candidate_paths: [] },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toBeUndefined();
|
||||
expect(createRuntime).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ tool: "wasm-validate", stdout: "1.0.41\n", stderr: "" },
|
||||
{ tool: "wasm-objdump", stdout: "", stderr: "" },
|
||||
@@ -559,6 +582,21 @@ it("retries a retained input descriptor before admitting the next WABT operation
|
||||
cleanupIncomplete: true,
|
||||
cleanupResources: [path],
|
||||
cause: { message: "injected WABT input close failure" },
|
||||
partialObservation: {
|
||||
normalized_result: {
|
||||
artifact: {
|
||||
path,
|
||||
sha256: createHash("sha256")
|
||||
.update(Buffer.alloc(76, 1))
|
||||
.digest("hex"),
|
||||
bytes: 76,
|
||||
},
|
||||
candidates: [{ path }],
|
||||
glue: [],
|
||||
validation: "unknown",
|
||||
tool_profile: { release: "1.0.42" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(fileCloseFailure.attempts).toBe(1);
|
||||
@@ -979,6 +1017,140 @@ it.each(outcomeModes)(
|
||||
"owns private artifact bytes and retains %s outcome",
|
||||
(mode) => runOwnedArtifactOutcome(mode),
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
stage: "validation rejection",
|
||||
mode: "invalid",
|
||||
validation: "rejected-by-selected-profile",
|
||||
},
|
||||
{ stage: "candidate admission", mode: "success", validation: "unknown" },
|
||||
{ stage: "glue decoding", mode: "success", validation: "unknown" },
|
||||
{ stage: "glue finalization", mode: "success", validation: "unknown" },
|
||||
{ stage: "producer output", mode: "malformed-wat", validation: "valid" },
|
||||
] as const)(
|
||||
"retains acquired file facts after $stage without claiming more module coverage",
|
||||
async ({ stage, mode, validation }) => {
|
||||
// Owned Node commands supply WABT-shaped responses; this checks provider
|
||||
// Evidence retention, not real WABT acceptance of the fixture bytes.
|
||||
const fixture = await createWabtArtifactFixture();
|
||||
const candidate = join(fixture.selectedRoot, "candidate.wasm");
|
||||
const candidateBytes = Buffer.from("independent candidate bytes");
|
||||
await writeFile(candidate, candidateBytes);
|
||||
const missing = join(fixture.selectedRoot, "missing.wasm");
|
||||
const gluePath = join(fixture.selectedRoot, "glue.js");
|
||||
const glueSource = 'fetch("./selected.wasm");';
|
||||
const glueBytes =
|
||||
stage === "glue decoding" ? Buffer.from([0xff]) : Buffer.from(glueSource);
|
||||
await writeFile(gluePath, glueBytes);
|
||||
const provider = new WabtArtifactProvider(
|
||||
{ REA_WABT_BIN_DIRECTORY: fixture.selectedRoot },
|
||||
(input) =>
|
||||
launchWabtOutcome(input, mode, {
|
||||
...fixture,
|
||||
controller: new AbortController(),
|
||||
}),
|
||||
);
|
||||
if (stage === "glue finalization") {
|
||||
fileCloseFailure.selectedPath = gluePath;
|
||||
fileCloseFailure.attempts = 0;
|
||||
fileCloseFailure.failFirst = true;
|
||||
}
|
||||
try {
|
||||
const result = await provider.inspect({
|
||||
path: fixture.path,
|
||||
candidate_paths:
|
||||
stage === "candidate admission" ? [candidate, missing] : [candidate],
|
||||
glue_paths: stage === "candidate admission" ? [] : [gluePath],
|
||||
});
|
||||
if (result.ok) throw new Error("Expected the selected failure stage");
|
||||
expect(result.error._tag).toBe(
|
||||
validation === "rejected-by-selected-profile"
|
||||
? "AnalysisInputError"
|
||||
: validation === "unknown"
|
||||
? "AnalysisInputError"
|
||||
: "AnalysisOutputError",
|
||||
);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
normalized_result: {
|
||||
artifact: {
|
||||
path: fixture.path,
|
||||
sha256: createHash("sha256").update(fixture.bytes).digest("hex"),
|
||||
},
|
||||
validation,
|
||||
tool_profile: { release: "1.0.42" },
|
||||
candidates: [
|
||||
{ path: fixture.path },
|
||||
{
|
||||
path: candidate,
|
||||
sha256: createHash("sha256").update(candidateBytes).digest("hex"),
|
||||
},
|
||||
],
|
||||
glue:
|
||||
stage === "candidate admission"
|
||||
? []
|
||||
: [
|
||||
{
|
||||
artifact: {
|
||||
path: gluePath,
|
||||
sha256: createHash("sha256")
|
||||
.update(glueBytes)
|
||||
.digest("hex"),
|
||||
},
|
||||
parse_status:
|
||||
stage === "glue finalization"
|
||||
? "not-performed"
|
||||
: stage === "glue decoding"
|
||||
? "failed"
|
||||
: "complete",
|
||||
references:
|
||||
stage === "glue decoding" || stage === "glue finalization"
|
||||
? []
|
||||
: [
|
||||
{
|
||||
value: "./selected.wasm",
|
||||
candidate_paths: [fixture.path],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (validation === "rejected-by-selected-profile")
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
normalized_result: {
|
||||
exit_code: 1,
|
||||
stderr: "module.wasm: invalid bytes\n",
|
||||
},
|
||||
});
|
||||
if (validation === "unknown")
|
||||
expect(result.error).toMatchObject({
|
||||
issues: [
|
||||
{
|
||||
path:
|
||||
stage === "candidate admission"
|
||||
? ["candidate_paths", 1]
|
||||
: ["glue_paths", 0],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (stage === "glue finalization") {
|
||||
expect(result.error).toMatchObject({
|
||||
cleanupIncomplete: true,
|
||||
cleanupResources: [gluePath],
|
||||
});
|
||||
expect(fileCloseFailure.attempts).toBe(1);
|
||||
await provider.close();
|
||||
expect(fileCloseFailure.attempts).toBe(2);
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
}
|
||||
} finally {
|
||||
await provider.close();
|
||||
fileCloseFailure.selectedPath = "";
|
||||
fileCloseFailure.failFirst = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
it.each([null, { reason: "caller-owned object" }, undefined])(
|
||||
"preserves finalization abort reason %j through cleanup recovery",
|
||||
(reason) => runOwnedArtifactOutcome("cancelled-close-failure", reason),
|
||||
|
||||
@@ -8,7 +8,11 @@ import type {
|
||||
WasmArtifactExecution,
|
||||
WasmArtifactPort,
|
||||
} from "../application/wasm/WasmArtifactPort.js";
|
||||
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
completedStableArtifact,
|
||||
readStableArtifact,
|
||||
type StableArtifactSnapshot,
|
||||
} from "../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
ArtifactResourceScope,
|
||||
ArtifactResourceRetryFailure,
|
||||
@@ -36,6 +40,7 @@ import {
|
||||
wasmArtifactSchema,
|
||||
DEFAULT_WASM_COMMAND_TIMEOUT_MS,
|
||||
type InspectWasmArtifactInput,
|
||||
type WasmArtifact,
|
||||
} from "../domain/wasm/wasmArtifact.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { runOwnedCommand } from "../process/OwnedCommand.js";
|
||||
@@ -56,6 +61,13 @@ import { parseWabtWat } from "./WabtWat.js";
|
||||
import { associateWasmGlue } from "./WasmGlueAdapter.js";
|
||||
|
||||
type Launcher = NonNullable<Parameters<typeof runOwnedCommand>[2]>["launcher"];
|
||||
type AcquiredGlue =
|
||||
| WasmArtifact["glue"][number]
|
||||
| {
|
||||
readonly artifact: WasmArtifact["glue"][number]["artifact"];
|
||||
readonly parse_status: "not-performed";
|
||||
readonly references: [];
|
||||
};
|
||||
const OPERATION = "inspect_wasm_artifact";
|
||||
const digest = (text: string) =>
|
||||
createHash("sha256").update(text).digest("hex");
|
||||
@@ -122,10 +134,71 @@ export class WabtArtifactProvider implements WasmArtifactPort {
|
||||
path: input.path,
|
||||
field: ["path"],
|
||||
};
|
||||
let toolProfile: WasmArtifact["tool_profile"] | undefined;
|
||||
let artifact: WasmArtifact["artifact"] | undefined;
|
||||
const candidates: WasmArtifact["candidates"] = [];
|
||||
const glue: AcquiredGlue[] = [];
|
||||
let validationState: "unknown" | "valid" = "unknown";
|
||||
const acquiredFacts = () => ({
|
||||
...(artifact === undefined ? {} : { artifact }),
|
||||
...(toolProfile === undefined ? {} : { tool_profile: toolProfile }),
|
||||
candidates: [...candidates],
|
||||
glue: [...glue],
|
||||
});
|
||||
const acquiredEvidence = () =>
|
||||
toolProfile === undefined
|
||||
? undefined
|
||||
: createEvidence(
|
||||
artifact === undefined
|
||||
? undefined
|
||||
: {
|
||||
path: artifact.path,
|
||||
format: "file",
|
||||
sha256: artifact.sha256,
|
||||
},
|
||||
WABT_PROVIDER_IDENTITY,
|
||||
{
|
||||
operation: OPERATION,
|
||||
parameters: input,
|
||||
result: { ...acquiredFacts(), validation: validationState },
|
||||
confidence: "observed",
|
||||
locations: [{ kind: "artifact-path", path: input.path }],
|
||||
limitations: [
|
||||
artifact === undefined
|
||||
? "The selected WASM byte identity was not acquired. The admitted WABT tool profile does not establish module validity or runtime behavior."
|
||||
: validationState === "valid"
|
||||
? "WABT validated the selected module, but inspection did not complete. Acquired file identities and glue references do not establish runtime loading or execution."
|
||||
: "WABT validation has no completed result. Acquired file identities and glue references do not establish module validity, runtime loading or execution.",
|
||||
],
|
||||
},
|
||||
);
|
||||
const readSelectedInput = async (
|
||||
path: string,
|
||||
onCompleted: (snapshot: StableArtifactSnapshot) => void,
|
||||
): Promise<StableArtifactSnapshot> => {
|
||||
try {
|
||||
const snapshot = await readStableArtifact(
|
||||
path,
|
||||
WABT_LIMITS.inputBytes,
|
||||
options?.signal,
|
||||
undefined,
|
||||
this.#resources,
|
||||
);
|
||||
onCompleted(snapshot);
|
||||
return snapshot;
|
||||
} catch (cause: unknown) {
|
||||
const completed = completedStableArtifact(cause);
|
||||
if (completed !== undefined) onCompleted(completed);
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
const commandTimeoutMs =
|
||||
input.command_timeout_ms ?? DEFAULT_WASM_COMMAND_TIMEOUT_MS;
|
||||
try {
|
||||
if (options?.signal?.aborted) throw new AnalysisCancelledError(OPERATION);
|
||||
if (options?.signal?.aborted)
|
||||
throw new AnalysisCancelledError(OPERATION, {
|
||||
cause: options.signal.reason,
|
||||
});
|
||||
const directory = this.environment.REA_WABT_BIN_DIRECTORY ?? "";
|
||||
if (!isAbsolute(directory))
|
||||
throw new Error(
|
||||
@@ -268,7 +341,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
|
||||
versionProbe = undefined;
|
||||
}
|
||||
// Success and validation failure report the same admitted tool/execution facts.
|
||||
const toolProfile = {
|
||||
toolProfile = {
|
||||
source: "https://github.com/WebAssembly/wabt",
|
||||
release: WABT_VERSION,
|
||||
source_commit: "ff0ef7e0009402740c805a9744c09b05be063e48",
|
||||
@@ -277,66 +350,70 @@ export class WabtArtifactProvider implements WasmArtifactPort {
|
||||
output_budget_bytes: WABT_LIMITS.outputBytes,
|
||||
};
|
||||
phase = "artifact";
|
||||
const selected = await readStableArtifact(
|
||||
input.path,
|
||||
WABT_LIMITS.inputBytes,
|
||||
options?.signal,
|
||||
undefined,
|
||||
this.#resources,
|
||||
);
|
||||
const artifact = {
|
||||
path: input.path,
|
||||
sha256: selected.sha256,
|
||||
bytes: selected.bytes.length,
|
||||
};
|
||||
const candidates = [];
|
||||
const selected = await readSelectedInput(input.path, (snapshot) => {
|
||||
const observed = {
|
||||
path: input.path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
};
|
||||
artifact = observed;
|
||||
candidates.push(observed);
|
||||
});
|
||||
for (const path of new Set([input.path, ...input.candidate_paths])) {
|
||||
if (path === input.path) continue;
|
||||
artifactInput = {
|
||||
path,
|
||||
field:
|
||||
path === input.path
|
||||
? ["path"]
|
||||
: ["candidate_paths", input.candidate_paths.indexOf(path)],
|
||||
field: ["candidate_paths", input.candidate_paths.indexOf(path)],
|
||||
};
|
||||
const bytes =
|
||||
path === input.path
|
||||
? selected
|
||||
: await readStableArtifact(
|
||||
path,
|
||||
WABT_LIMITS.inputBytes,
|
||||
options?.signal,
|
||||
undefined,
|
||||
this.#resources,
|
||||
);
|
||||
candidates.push({
|
||||
await readSelectedInput(path, (snapshot) => {
|
||||
candidates.push({
|
||||
path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
});
|
||||
});
|
||||
}
|
||||
for (const [index, path] of input.glue_paths.entries()) {
|
||||
artifactInput = { path, field: ["glue_paths", index] };
|
||||
const glueIndex = glue.length;
|
||||
const bytes = await readSelectedInput(path, (snapshot) => {
|
||||
glue.push({
|
||||
artifact: {
|
||||
path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
},
|
||||
parse_status: "not-performed",
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
const glueArtifact = {
|
||||
path,
|
||||
sha256: bytes.sha256,
|
||||
bytes: bytes.bytes.length,
|
||||
});
|
||||
}
|
||||
const glue = [];
|
||||
for (const [index, path] of input.glue_paths.entries()) {
|
||||
artifactInput = { path, field: ["glue_paths", index] };
|
||||
const bytes = await readStableArtifact(
|
||||
path,
|
||||
WABT_LIMITS.inputBytes,
|
||||
options?.signal,
|
||||
undefined,
|
||||
this.#resources,
|
||||
);
|
||||
// Keep source characters intact so locations match the hashed artifact.
|
||||
const source = new TextDecoder("utf-8", {
|
||||
fatal: true,
|
||||
ignoreBOM: true,
|
||||
}).decode(bytes.bytes);
|
||||
glue.push({
|
||||
artifact: { path, sha256: bytes.sha256, bytes: bytes.bytes.length },
|
||||
...associateWasmGlue(
|
||||
source,
|
||||
path,
|
||||
candidates.map((candidate) => candidate.path),
|
||||
),
|
||||
});
|
||||
};
|
||||
try {
|
||||
// Keep source characters intact so locations match the hashed artifact.
|
||||
const source = new TextDecoder("utf-8", {
|
||||
fatal: true,
|
||||
ignoreBOM: true,
|
||||
}).decode(bytes.bytes);
|
||||
glue[glueIndex] = {
|
||||
artifact: glueArtifact,
|
||||
...associateWasmGlue(
|
||||
source,
|
||||
path,
|
||||
candidates.map((candidate) => candidate.path),
|
||||
),
|
||||
};
|
||||
} catch (cause: unknown) {
|
||||
glue[glueIndex] = {
|
||||
artifact: glueArtifact,
|
||||
parse_status: "failed",
|
||||
references: [],
|
||||
};
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
phase = "producer";
|
||||
await writeFile(snapshotPath, selected.bytes, {
|
||||
@@ -365,9 +442,8 @@ export class WabtArtifactProvider implements WasmArtifactPort {
|
||||
operation: OPERATION,
|
||||
parameters: input,
|
||||
result: {
|
||||
artifact,
|
||||
...acquiredFacts(),
|
||||
validation: "rejected-by-selected-profile",
|
||||
tool_profile: toolProfile,
|
||||
exit_code: validation.exitCode ?? null,
|
||||
stdout: validation.stdout.text,
|
||||
stderr: validation.stderr.text,
|
||||
@@ -394,6 +470,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
|
||||
},
|
||||
],
|
||||
);
|
||||
validationState = "valid";
|
||||
const customSectionWarnings: {
|
||||
tool: (typeof commands)[number]["tool"];
|
||||
text: string;
|
||||
@@ -473,7 +550,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
|
||||
};
|
||||
const { sections, headers, details } = parseWabtOutput(
|
||||
dump.stdout.text,
|
||||
artifact.bytes,
|
||||
selected.bytes.length,
|
||||
customSectionNames,
|
||||
debugModuleNames,
|
||||
);
|
||||
@@ -530,6 +607,10 @@ export class WabtArtifactProvider implements WasmArtifactPort {
|
||||
...(versionProbe === undefined ? {} : { versionProbe }),
|
||||
},
|
||||
);
|
||||
if (failure.partialObservation === undefined) {
|
||||
const partial = acquiredEvidence();
|
||||
if (partial !== undefined) failure.retainPartialObservation(partial);
|
||||
}
|
||||
runtime?.retainCommandFailure(cause, failure, this.#resources);
|
||||
result = err(
|
||||
failure instanceof AnalysisOutputError &&
|
||||
@@ -540,6 +621,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
|
||||
capturedOutput,
|
||||
cleanup: failure.cleanup,
|
||||
diagnostics: failure.diagnostics,
|
||||
partialObservation: failure.partialObservation,
|
||||
}).bindCleanupDependencies([failure])
|
||||
: failure,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { analysisErrorProjectionSchema } from "../../../src/contracts/errorSchemas.js";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const sha256 = (bytes: Buffer | string): string =>
|
||||
createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
// These scripts exercise the public adapters with WABT-shaped responses. They
|
||||
// do not establish how the real WABT release classifies the selected bytes.
|
||||
cliTest.runIf(process.platform !== "win32")(
|
||||
"retains selected, candidate, and glue facts after simulated WABT rejection through CLI and stdio MCP",
|
||||
async ({ cli }) => {
|
||||
const directory = await createTestTempDirectory("rea-wasm-acquired-");
|
||||
const tools = join(directory, "tools");
|
||||
await mkdir(tools);
|
||||
for (const name of ["wasm-validate", "wasm-objdump", "wasm2wat"]) {
|
||||
const path = join(tools, name);
|
||||
const body =
|
||||
name === "wasm-validate"
|
||||
? "echo 'fixture validation rejected' >&2\nexit 1"
|
||||
: "exit 0";
|
||||
await writeFile(
|
||||
path,
|
||||
`#!/bin/sh
|
||||
if [ "$1" = "--version" ]; then echo '1.0.42'; exit 0; fi
|
||||
${body}
|
||||
`,
|
||||
{ mode: 0o700 },
|
||||
);
|
||||
await chmod(path, 0o700);
|
||||
}
|
||||
const selected = join(directory, "selected.wasm");
|
||||
const candidate = join(directory, "candidate.wasm");
|
||||
const glue = join(directory, "glue.js");
|
||||
const selectedBytes = Buffer.from("0061736d01000000", "hex");
|
||||
const candidateBytes = Buffer.from("independent candidate bytes");
|
||||
const glueSource = 'fetch("./selected.wasm");';
|
||||
const environment = {
|
||||
HOME: directory,
|
||||
XDG_CONFIG_HOME: directory,
|
||||
XDG_CACHE_HOME: directory,
|
||||
REA_WABT_BIN_DIRECTORY: tools,
|
||||
};
|
||||
await Promise.all([
|
||||
writeFile(selected, selectedBytes),
|
||||
writeFile(candidate, candidateBytes),
|
||||
writeFile(glue, glueSource),
|
||||
]);
|
||||
const expected = {
|
||||
artifact: {
|
||||
path: selected,
|
||||
sha256: sha256(selectedBytes),
|
||||
bytes: selectedBytes.length,
|
||||
},
|
||||
candidates: [
|
||||
{
|
||||
path: selected,
|
||||
sha256: sha256(selectedBytes),
|
||||
bytes: selectedBytes.length,
|
||||
},
|
||||
{
|
||||
path: candidate,
|
||||
sha256: sha256(candidateBytes),
|
||||
bytes: candidateBytes.length,
|
||||
},
|
||||
],
|
||||
glue: [
|
||||
{
|
||||
artifact: {
|
||||
path: glue,
|
||||
sha256: sha256(glueSource),
|
||||
bytes: Buffer.byteLength(glueSource),
|
||||
},
|
||||
parse_status: "complete",
|
||||
references: [
|
||||
{
|
||||
value: "./selected.wasm",
|
||||
candidate_paths: [selected],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
validation: "rejected-by-selected-profile",
|
||||
tool_profile: { release: "1.0.42" },
|
||||
exit_code: 1,
|
||||
stderr: "fixture validation rejected\n",
|
||||
};
|
||||
const invocation = await cli.run({
|
||||
arguments: [
|
||||
"inspect-wasm-artifact",
|
||||
selected,
|
||||
"--candidate_paths",
|
||||
candidate,
|
||||
"--glue_paths",
|
||||
glue,
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
environment,
|
||||
});
|
||||
expect(invocation.exitCode, invocation.stderr).toBe(1);
|
||||
const cliError = analysisErrorProjectionSchema.parse(invocation.json);
|
||||
expect(cliError).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
partial_observation: {
|
||||
normalized_result: expected,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { client } = await connectLocalToolsMcp(environment);
|
||||
const response = await client.callTool({
|
||||
name: "inspect_wasm_artifact",
|
||||
arguments: {
|
||||
path: selected,
|
||||
candidate_paths: [candidate],
|
||||
glue_paths: [glue],
|
||||
},
|
||||
});
|
||||
const mcpError = parseMcpToolError(response).error;
|
||||
expect(mcpError.code).toBe("invalid_request");
|
||||
const partialReference = z
|
||||
.object({
|
||||
kind: z.literal("retained-evidence"),
|
||||
evidence_id: z.string(),
|
||||
})
|
||||
.parse(
|
||||
z
|
||||
.object({ partial_observation: z.unknown() })
|
||||
.passthrough()
|
||||
.parse(mcpError.details).partial_observation,
|
||||
);
|
||||
const retainedReply = await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: { detail: "complete" },
|
||||
});
|
||||
expect(retainedReply.isError).not.toBe(true);
|
||||
const retainedBundle = toolContract(
|
||||
"get_evidence_bundle",
|
||||
).outputSchema.parse(retainedReply.structuredContent).result;
|
||||
if ("kind" in retainedBundle)
|
||||
throw new Error("Expected complete retained Evidence");
|
||||
const retained = parseEvidence(
|
||||
retainedBundle.records.find(
|
||||
(record) => record.evidence_id === partialReference.evidence_id,
|
||||
),
|
||||
);
|
||||
expect(retained.evidence_id).toBe(partialReference.evidence_id);
|
||||
expect(retained.normalized_result).toMatchObject(expected);
|
||||
await client.ping();
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user