From 4f4f6cd7b5d7a400b90772a138d7b9f942c2a556 Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:07:40 +0800 Subject: [PATCH] test(evm): verify static descriptor bindings before use (#2427) Verify the static EVM descriptor operation, CLI command, factory and input/output schema bindings with compiler-backed negative assignability checks. Prove SDK duplicate registration and invalid target input fail before provider invocation, preserving named bindings and separate lazy owners. Validated complete supported single-thread typecheck, focused12 SDK/composition cases plus a targeted2-case provider-spy rerun, static checks, checked pre-push, manual review and all exact-head CI checks. Production behavior/defaults and provider identity remain unchanged. Dynamic third-party module admission and ABI negotiation remain deferred. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../analysis-efficiency/evm-descriptor.md | 37 +++++++++++ tests/boundary/mcp/evmInterfaceMcp.test.ts | 6 +- tests/composition/evmModuleContract.test.ts | 62 +++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/composition/evmModuleContract.test.ts diff --git a/experiments/analysis-efficiency/evm-descriptor.md b/experiments/analysis-efficiency/evm-descriptor.md index 8e3611f28..0fcdf4d44 100644 --- a/experiments/analysis-efficiency/evm-descriptor.md +++ b/experiments/analysis-efficiency/evm-descriptor.md @@ -179,3 +179,40 @@ interface question through an explicitly selected precise view, retain full bytecode/original Evidence under the existing lifetime, and preserve complete defaults. Discover support before sending new options to an older server. Include discovery, follow-up, failures and retries in the complete task cost. + +## Static contract admission verification + +The follow-up from main `c462a4ebd3a10e59253bcf38353a0914b176738b` tests the +pilot's actual static boundary, without adding a dynamic module registry or +changing production code. Complete-project TypeScript checking now verifies +that the inferred descriptor type rejects a WASM operation contract, a WASM +CLI command, a WASM service factory, an unrelated input schema and an unrelated +output schema. These are negative assignability assertions: their guarantee +comes from the compiler, not from executing an incompatible descriptor. +The existing named contract and CLI identity are also checked at runtime; +separate factory calls produce distinct service/provider owners. Invalid target +input remains an application error without launching a worker. + +The actual public in-memory MCP SDK test additionally spies on the provider: +duplicate EVM registration throws the named `already registered` error before +any provider call. Converted input/output JSON Schemas validate before the +first valid operation. Invalid client arguments are rejected before execution; +incompatible provider reply data is validated **after** execution and is not +presented as a pre-use contract guarantee. Existing healthy-peer, cancellation, +retained summary/default and cleanup-retry scenarios remain covered. + +Full project typecheck passes with the installed compiler's supported single +thread scheduling; it includes all source/test files. The two exact focused +files pass 12 runtime cases. The resource scopes sample 2,057,961,472 bytes for +typecheck and 2,426,290,176 bytes for required build plus focused tests, below the +unchanged 3 GiB early-stop/4 GiB hard ceiling. Final transient-cgroup counters +remain unknown. No new real-provider or performance improvement is claimed by +these compile/SDK checks; the earlier pinned real EVMole/WABT/lifecycle evidence +retains its original scope and source identity. + +This proves the one statically composed descriptor's named/schema/factory +compatibility and duplicate-registration guard. It does not implement runtime +admission of arbitrary JavaScript descriptors, third-party plugin ABI/version +negotiation or a general plugin SDK. Those remain deferred by the original plan. +Existing callers, full defaults, provider inputs, owned lifetimes and output +identities are unchanged. diff --git a/tests/boundary/mcp/evmInterfaceMcp.test.ts b/tests/boundary/mcp/evmInterfaceMcp.test.ts index 704af92c5..f3594dd89 100644 --- a/tests/boundary/mcp/evmInterfaceMcp.test.ts +++ b/tests/boundary/mcp/evmInterfaceMcp.test.ts @@ -28,8 +28,11 @@ import { it("advertises exact valid schemas and records inline EVM interface evidence without a binary target", async () => { const value = evmInterfaceFixture(); + const inspect = vi.fn(() => + Promise.resolve(ok(evmInterfaceExecution(value))), + ); const service = new EvmInterfaceService({ - inspect: () => Promise.resolve(ok(evmInterfaceExecution(value))), + inspect, }); const session = createTestBinarySession(() => { throw new Error("deep provider must not start"); @@ -71,6 +74,7 @@ it("advertises exact valid schemas and records inline EVM interface evidence wit expect(() => registerEvmTools(server, service, silentLogger)).toThrow( /inspect_evm_interface.*already registered/, ); + expect(inspect).not.toHaveBeenCalled(); expect( (await client.listTools()).tools.filter( (tool) => tool.name === "inspect_evm_interface", diff --git a/tests/composition/evmModuleContract.test.ts b/tests/composition/evmModuleContract.test.ts new file mode 100644 index 000000000..b04a41d93 --- /dev/null +++ b/tests/composition/evmModuleContract.test.ts @@ -0,0 +1,62 @@ +import { expect, expectTypeOf, it, vi } from "vitest"; +import { z } from "zod"; +import { evmInterfaceModule } from "../../src/composition/evm.js"; +import { wasmArtifactModule } from "../../src/composition/wasm.js"; +import { CLI_COMMANDS } from "../../src/cliCommandNames.js"; +import { INSPECT_EVM_INTERFACE_CONTRACT } from "../../src/contracts/evm/evmToolContracts.js"; + +it("rejects incompatible static bindings through the complete project typecheck", () => { + // expect-type's negative assertions are checked by tsc, not the runtime + // test runner. No malformed descriptor is registered or executed here. + expectTypeOf({ + ...evmInterfaceModule, + contract: wasmArtifactModule.contract, + }).not.toExtend(); + expectTypeOf({ + ...evmInterfaceModule, + cliCommand: CLI_COMMANDS.inspectWasmArtifact, + }).not.toExtend(); + expectTypeOf({ + ...evmInterfaceModule, + create: wasmArtifactModule.create, + }).not.toExtend(); + expectTypeOf({ + ...evmInterfaceModule, + contract: { + ...evmInterfaceModule.contract, + inputSchema: z.strictObject({ unrelated: z.string() }), + }, + }).not.toExtend(); + expectTypeOf({ + ...evmInterfaceModule, + contract: { + ...evmInterfaceModule.contract, + outputSchema: z.strictObject({ unrelated: z.string() }), + }, + }).not.toExtend(); +}); + +it("keeps the named binding and gives each caller a separate lazy service owner", async () => { + expect(evmInterfaceModule.contract).toBe(INSPECT_EVM_INTERFACE_CONTRACT); + expect(evmInterfaceModule.cliCommand).toBe(CLI_COMMANDS.inspectEvmInterface); + const first = evmInterfaceModule.create({}); + const second = evmInterfaceModule.create({}); + try { + expect(first).not.toBe(second); + expect(first.provider).not.toBe(second.provider); + await first.close(); + const inspect = vi.spyOn(second.provider, "inspect"); + const invalid = await second.inspect({ + path: "relative.hex", + encoding: "hex", + }); + expect(invalid).toMatchObject({ + ok: false, + error: { _tag: "AnalysisInputError" }, + }); + expect(inspect).not.toHaveBeenCalled(); + } finally { + await first.close(); + await second.close(); + } +});