209 lines
6.7 KiB
JavaScript
209 lines
6.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
|
|
import { join, resolve, isAbsolute } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { promisify } from "node:util";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { parseEvidence } from "../dist/domain/evidence.js";
|
|
|
|
const execute = promisify(execFile);
|
|
const bin = process.env.REA_WABT_BIN_DIRECTORY;
|
|
if (!isAbsolute(bin ?? ""))
|
|
throw new Error("Supply absolute REA_WABT_BIN_DIRECTORY with WABT 1.0.42");
|
|
const root = await mkdtemp(join(tmpdir(), "rea-wasm-interface-real-"));
|
|
const cli = resolve("scripts/rea.mjs");
|
|
const environment = { ...process.env };
|
|
const command = async (file, args) =>
|
|
(
|
|
await execute(file, args, {
|
|
env: environment,
|
|
cwd: root,
|
|
timeout: 120000,
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
})
|
|
).stdout;
|
|
const client = new Client({
|
|
name: "wasm-interface-real-verifier",
|
|
version: "1",
|
|
});
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [cli, "mcp"],
|
|
env: environment,
|
|
stderr: "pipe",
|
|
});
|
|
const source = (parameter) =>
|
|
`(module (type (func (param ${parameter}) (result i64))) (import "env" "f" (func (type 0))) (table 1 3 funcref) (memory 1 2) (global (mut i64) (i64.const 0)) (tag (param i32)) (export "run\\22\\ef\\bb\\bf" (func 0)) (export "table" (table 0)) (export "memory" (memory 0)) (export "global" (global 0)) (export "tag" (tag 0)))`;
|
|
const assemble = async (name, wat) => {
|
|
await writeFile(join(root, `${name}.wat`), wat);
|
|
const path = join(root, `${name}.wasm`);
|
|
await command(join(bin, "wat2wasm"), [
|
|
"--enable-all",
|
|
`${name}.wat`,
|
|
"-o",
|
|
path,
|
|
]);
|
|
return path;
|
|
};
|
|
const inspect = async (path) => {
|
|
const result = await client.callTool({
|
|
name: "inspect_wasm_artifact",
|
|
arguments: { path },
|
|
});
|
|
assert.notEqual(result.isError, true, JSON.stringify(result));
|
|
return parseEvidence(result.structuredContent);
|
|
};
|
|
const compare = async (left, right, scope = "both") => {
|
|
const input = {
|
|
left: { kind: "retained-evidence", evidence_id: left.evidence_id },
|
|
right: { kind: "retained-evidence", evidence_id: right.evidence_id },
|
|
scope,
|
|
};
|
|
const reply = await client.callTool({
|
|
name: "compare_wasm_interfaces",
|
|
arguments: input,
|
|
});
|
|
assert.notEqual(reply.isError, true, JSON.stringify(reply));
|
|
const mcp = parseEvidence(reply.structuredContent);
|
|
const file = join(root, "comparison.json");
|
|
await writeFile(file, JSON.stringify({ left, right, scope }));
|
|
const local = parseEvidence(
|
|
JSON.parse(
|
|
await command(process.execPath, [
|
|
cli,
|
|
"compare-wasm-interfaces",
|
|
file,
|
|
"--json",
|
|
]),
|
|
),
|
|
);
|
|
assert.deepEqual(local, mcp);
|
|
return mcp.normalized_result;
|
|
};
|
|
try {
|
|
assert.equal(
|
|
(await command(join(bin, "wat2wasm"), ["--version"])).trim(),
|
|
"1.0.42",
|
|
);
|
|
await client.connect(transport);
|
|
const path = await assemble("baseline", source("i32"));
|
|
const original = await readFile(path);
|
|
assert.equal(original[8], 1);
|
|
assert.ok(original[9] < 128);
|
|
// Section lengths allow padded u32 LEB128 encodings. Different original
|
|
// byte identities must compare equal after validated type resolution.
|
|
const equivalentPath = join(root, "equivalent.wasm");
|
|
await writeFile(
|
|
equivalentPath,
|
|
Buffer.concat([
|
|
original.subarray(0, 9),
|
|
Buffer.from([original[9] | 128, 0]),
|
|
original.subarray(10),
|
|
]),
|
|
);
|
|
const left = await inspect(path);
|
|
const equivalent = await inspect(equivalentPath);
|
|
assert.notEqual(left.subject.digest.sha256, equivalent.subject.digest.sha256);
|
|
const same = await compare(left, equivalent);
|
|
assert.deepEqual(same.summary, {
|
|
added: 0,
|
|
removed: 0,
|
|
changed: 0,
|
|
unchanged: 6,
|
|
unknown: 0,
|
|
});
|
|
const facts = left.normalized_result.interface.entries;
|
|
assert.deepEqual(facts.find((e) => e.name === "memory").signature, {
|
|
kind: "memory",
|
|
page_size: "65536",
|
|
address_type: "i32",
|
|
minimum: "1",
|
|
maximum: "2",
|
|
shared: false,
|
|
});
|
|
assert.deepEqual(facts.find((e) => e.name === "tag").signature, {
|
|
kind: "tag",
|
|
params: ["i32"],
|
|
results: [],
|
|
});
|
|
assert.deepEqual(facts.find((e) => e.name === 'run"\ufeff').signature, {
|
|
kind: "func",
|
|
params: ["i32"],
|
|
results: ["i64"],
|
|
});
|
|
const changed = await inspect(await assemble("changed", source("i64")));
|
|
assert.deepEqual((await compare(left, changed)).summary, {
|
|
added: 0,
|
|
removed: 0,
|
|
changed: 2,
|
|
unchanged: 4,
|
|
unknown: 0,
|
|
});
|
|
const smallPage = await inspect(
|
|
await assemble(
|
|
"small-page",
|
|
'(module (memory 1 2 (pagesize 1)) (export "memory" (memory 0)))',
|
|
),
|
|
);
|
|
const defaultPage = await inspect(
|
|
await assemble(
|
|
"default-page",
|
|
'(module (memory 1 2) (export "memory" (memory 0)))',
|
|
),
|
|
);
|
|
assert.equal((await compare(defaultPage, smallPage)).summary.changed, 1);
|
|
const empty = await inspect(await assemble("empty", "(module)"));
|
|
assert.equal((await compare(left, empty)).summary.removed, 6);
|
|
assert.equal((await compare(empty, left, "exports")).summary.added, 5);
|
|
const duplicate = await inspect(
|
|
await assemble(
|
|
"duplicate",
|
|
'(module (import "e" "same" (func (param i32))) (import "e" "same" (func (param i64))))',
|
|
),
|
|
);
|
|
const duplicates = await compare(duplicate, duplicate, "imports");
|
|
assert.equal(duplicates.summary.unknown, 1);
|
|
assert.equal(duplicates.changes[0].left.length, 2);
|
|
const unsupported = await inspect(
|
|
await assemble(
|
|
"exnref",
|
|
'(module (import "e" "abstract-ref" (global exnref)))',
|
|
),
|
|
);
|
|
assert.equal((await compare(unsupported, unsupported)).summary.unknown, 1);
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
verified: true,
|
|
toolchain: "WABT 1.0.42",
|
|
transport: "compiled CLI + stdio MCP",
|
|
interface_fact_bytes: Buffer.byteLength(
|
|
JSON.stringify(left.normalized_result.interface),
|
|
),
|
|
comparison_result_bytes: Buffer.byteLength(JSON.stringify(same)),
|
|
original_sha256: left.subject.digest.sha256,
|
|
equivalent_sha256: equivalent.subject.digest.sha256,
|
|
cases: [
|
|
"equivalent padded LEB128",
|
|
"function signature change",
|
|
"all admitted kinds",
|
|
"custom memory page-size change",
|
|
"escaped/BOM name",
|
|
"empty interface additions/removals",
|
|
"scope",
|
|
"duplicate imports",
|
|
"unsupported exception reference",
|
|
],
|
|
runtime_execution: "not-performed",
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} finally {
|
|
await client.close();
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|