test(mcp): serialize real process capture and report unavailable authority

This commit is contained in:
morluto committed 2026-10-09 02:58:03 +08:00
1 parent fe48e779c2
commit 8f9da25164
3 files changed
+96 -73

No files matched your search

+5 -2
View File
@@ -30,8 +30,11 @@ one golden malformed-input case and a real filesystem lifecycle that proves
configure, update, readback, and removal; an add-only journey does not cover the
whole lifecycle.
Keep in-memory MCP sessions in the MCP boundary lane and direct filesystem
workflows in the filesystem boundary lane. Reserve acceptance paths for journeys
Keep transport-only in-memory MCP sessions in the MCP boundary lane and direct
filesystem workflows in the filesystem boundary lane. Real process-tree capture
belongs in the serial process boundary lane, including captures invoked through
MCP. Report unavailable capture authority as a named skip rather than returning
from the test without assertions. Reserve acceptance paths for journeys
through the compiled public CLI or MCP runtime. Calling a compiled private
workflow or injecting a provider still establishes its narrower integration
claim, even if a directory name calls it acceptance.
+1 -71
View File
@@ -1,9 +1,7 @@
import { readFile, realpath, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { Ajv2020 } from "ajv/dist/2020.js";
import { afterEach, describe, expect, it } from "vitest";
import { z } from "zod";
@@ -15,10 +13,8 @@ import type {
AnalysisProvider,
CapabilityDescriptor,
} from "../../../src/application/AnalysisProvider.js";
import { probeProcessCaptureCapability } from "../../../src/process/capture/ProcessHarness.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
import { createServer } from "../../../src/server/createServer.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { silentLogger } from "../../../src/logger.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import { MAX_JSON_DEPTH } from "../../../src/domain/jsonValue.js";
@@ -35,9 +31,6 @@ const SNAPSHOT_PROFILE = createAnalysisProfile(
);
const resources: Array<{ close(): Promise<unknown> }> = [];
const processFixture = fileURLToPath(
new URL("../../fixtures/processFidelity.mjs", import.meta.url),
);
let directory: string | undefined;
afterEach(async () => {
await Promise.all(resources.splice(0).map((resource) => resource.close()));
@@ -296,69 +289,6 @@ describe("json depth bounds over MCP", () => {
}, 10_000);
});
describe("process residuals over MCP", () => {
it("records process residuals linked to capture Evidence", async () => {
if (!(await probeProcessCaptureCapability()).available) return;
const session = createTestBinarySession(() => client("fixture", []));
const server = createServer(session, session, {
logger: silentLogger,
});
const mcp = new Client({ name: "process-unknown", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(mcp, server);
await server.connect(serverTransport);
await mcp.connect(clientTransport);
const captured = await mcp.callTool({
name: "capture_process_scenario",
arguments: {
executable: process.execPath,
arguments: [processFixture, "partial"],
},
});
expect(captured.isError, text(captured)).not.toBe(true);
const contract = toolContract("capture_process_scenario");
const wire = (await mcp.listTools()).tools.find(
({ name }) => name === contract.name,
);
if (wire?.outputSchema === undefined)
throw new Error("Missing process capture output schema");
expect(
new Ajv2020({ strict: false, validateFormats: false }).validate(
z.record(z.string(), z.unknown()).parse(wire.outputSchema),
captured.structuredContent,
),
).toBe(true);
expect(
contract.outputSchema.safeParse(captured.structuredContent).success,
).toBe(true);
const listedUnknowns = await mcp.callTool({
name: "list_unknowns",
arguments: {},
});
expect(listedUnknowns.isError, text(listedUnknowns)).not.toBe(true);
const listed = z
.object({
result: z.object({
items: z.array(
z.object({
question: z.string(),
domain: z.string(),
}),
),
}),
})
.parse(structured(listedUnknowns)).result.items;
expect(listed).toContainEqual(
expect.objectContaining({
question: "Was network behavior fully observed during capture?",
domain: "process-network",
}),
);
});
});
const client = (path: string, closed: string[]): AnalysisClient => ({
execute: (name) =>
Promise.resolve(
@@ -0,0 +1,90 @@
import { fileURLToPath } from "node:url";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { afterEach, expect } from "vitest";
import { z } from "zod";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import {
createCacheProvider,
createTestBinarySession,
} from "../../fixtures/binarySession.js";
import { createServer } from "../../../src/server/createServer.js";
import { silentLogger } from "../../../src/logger.js";
import { itWithCaptureCapability } from "./processCaptureCapability.js";
const processFixture = fileURLToPath(
new URL("../../fixtures/processFidelity.mjs", import.meta.url),
);
const resources: Array<{ close(): Promise<unknown> }> = [];
afterEach(async () => {
await Promise.all(resources.splice(0).map((resource) => resource.close()));
});
itWithCaptureCapability(
"records process residuals linked to capture Evidence",
async () => {
const session = createTestBinarySession(createCacheProvider([]));
const server = createServer(session, session, {
logger: silentLogger,
});
const mcp = new Client({ name: "process-unknown", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(mcp, server);
await server.connect(serverTransport);
await mcp.connect(clientTransport);
const captured = await mcp.callTool({
name: "capture_process_scenario",
arguments: {
executable: process.execPath,
arguments: [processFixture, "partial"],
},
});
expect(captured.isError, JSON.stringify(captured.content)).not.toBe(true);
const contract = toolContract("capture_process_scenario");
const wire = (await mcp.listTools()).tools.find(
({ name }) => name === contract.name,
);
if (wire?.outputSchema === undefined)
throw new Error("Missing process capture output schema");
expect(
new Ajv2020({ strict: false, validateFormats: false }).validate(
z.record(z.string(), z.unknown()).parse(wire.outputSchema),
captured.structuredContent,
),
).toBe(true);
expect(
contract.outputSchema.safeParse(captured.structuredContent).success,
).toBe(true);
const listedUnknowns = await mcp.callTool({
name: "list_unknowns",
arguments: {},
});
expect(
listedUnknowns.isError,
JSON.stringify(listedUnknowns.content),
).not.toBe(true);
const listed = z
.object({
result: z.object({
items: z.array(
z.object({
question: z.string(),
domain: z.string(),
}),
),
}),
})
.parse(listedUnknowns.structuredContent).result.items;
expect(listed).toContainEqual(
expect.objectContaining({
question: "Was network behavior fully observed during capture?",
domain: "process-network",
}),
);
},
);