From d81393d38a9577efda317329d68cf7539016c2e7 Mon Sep 17 00:00:00 2001 From: esengine <359807859@qq.com> Date: Sat, 2 May 2026 01:39:36 -0700 Subject: [PATCH] test(mcp): pin RFC #110's cache-prefix claims with 6 unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reconnect RFC's central claim — "re-bridging tools mid-session breaks the prompt prefix when the new tool surface differs from the old" — was so far an architecture-derived assertion, not an empirical result. Cheap to lock down at the prefix-fingerprint layer. `tests/mcp-reconnect-prefix-invariant.test.ts` covers six cases the RFC directly references: - identical tool list → fingerprint unchanged (the safe-reconnect case) - one tool added → fingerprint changes - one tool removed → fingerprint changes - description-only edit on an existing tool → fingerprint changes - parameter-schema edit → fingerprint changes - same tools reordered → fingerprint changes (array order is part of the prefix bytes via JSON.stringify) This gives RFC #110 a concrete reference to point at when weighing the strict / permissive / `--force` approaches: any drift past the "identical bytes" line is a guaranteed cache miss, full stop. Refs #110. --- tests/mcp-reconnect-prefix-invariant.test.ts | 91 ++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 tests/mcp-reconnect-prefix-invariant.test.ts diff --git a/tests/mcp-reconnect-prefix-invariant.test.ts b/tests/mcp-reconnect-prefix-invariant.test.ts new file mode 100644 index 0000000..6782d3a --- /dev/null +++ b/tests/mcp-reconnect-prefix-invariant.test.ts @@ -0,0 +1,91 @@ +/** Pins down the cache-prefix claims in RFC #110 (`/mcp reconnect `). */ + +import { describe, expect, it } from "vitest"; +import { ImmutablePrefix } from "../src/memory/runtime.js"; +import type { ToolSpec } from "../src/types.js"; + +function tool(name: string, description = "", params: object = { type: "object" }): ToolSpec { + return { + type: "function", + function: { name, description, parameters: params }, + }; +} + +describe("RFC #110 — cache-prefix invariant under MCP reconnect", () => { + it("re-bridging an IDENTICAL tool list yields byte-identical fingerprint (the safe case)", () => { + const before = new ImmutablePrefix({ + system: "s", + toolSpecs: [tool("read"), tool("write"), tool("search")], + }); + const after = new ImmutablePrefix({ + system: "s", + toolSpecs: [tool("read"), tool("write"), tool("search")], + }); + expect(after.fingerprint).toBe(before.fingerprint); + }); + + it("re-bridging with one ADDED tool changes the fingerprint (cache miss next turn)", () => { + const before = new ImmutablePrefix({ + system: "s", + toolSpecs: [tool("read"), tool("write")], + }); + const after = new ImmutablePrefix({ + system: "s", + toolSpecs: [tool("read"), tool("write"), tool("delete")], + }); + expect(after.fingerprint).not.toBe(before.fingerprint); + }); + + it("re-bridging with one REMOVED tool changes the fingerprint", () => { + const before = new ImmutablePrefix({ + system: "s", + toolSpecs: [tool("read"), tool("write"), tool("search")], + }); + const after = new ImmutablePrefix({ + system: "s", + toolSpecs: [tool("read"), tool("write")], + }); + expect(after.fingerprint).not.toBe(before.fingerprint); + }); + + it("a description-only change on an existing tool changes the fingerprint", () => { + const before = new ImmutablePrefix({ + system: "s", + toolSpecs: [tool("read", "reads a file")], + }); + const after = new ImmutablePrefix({ + system: "s", + toolSpecs: [tool("read", "reads a file (utf-8)")], + }); + expect(after.fingerprint).not.toBe(before.fingerprint); + }); + + it("a parameter-schema change on an existing tool changes the fingerprint", () => { + const before = new ImmutablePrefix({ + system: "s", + toolSpecs: [tool("read", "", { type: "object", properties: { path: { type: "string" } } })], + }); + const after = new ImmutablePrefix({ + system: "s", + toolSpecs: [ + tool("read", "", { + type: "object", + properties: { path: { type: "string" }, encoding: { type: "string" } }, + }), + ], + }); + expect(after.fingerprint).not.toBe(before.fingerprint); + }); + + it("REORDERING the same tools changes the fingerprint (array order is part of the prefix)", () => { + const a = new ImmutablePrefix({ + system: "s", + toolSpecs: [tool("read"), tool("write"), tool("search")], + }); + const b = new ImmutablePrefix({ + system: "s", + toolSpecs: [tool("write"), tool("read"), tool("search")], + }); + expect(b.fingerprint).not.toBe(a.fingerprint); + }); +});