Files
e79e014f23 fix(browser): preserve passive network inventory uncertainty
Preserve uncertainty in offline passive network comparisons when request
identity, metadata, or per-kind inventory coverage is incomplete. Retain
proven HTTP/WebSocket changes without using missing information to infer
membership, duplicate counts, or equality.

Compare redirect resource types only where both observations retain them.
Keep metadata completeness with its owner, stream HTTP content digests,
and require full coverage for multiplicity and WebSocket stream inventories.
Preserve independently known metadata changes and full-inventory counts.

Keep the original JavaScript configuration-escape, JSON, cancellation,
SAZ and schema-documentation fixes compatible with current main. Split
public CLI comparisons into isolated test cases without loosening timeouts.
Validate actual Chromium/HTTP captures through CLI and real stdio SDK,
with explicit limits and unchanged source bytes.

Co-authored-by: To3akaRin <131595336+To3akaRin@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-15 02:28:46 +08:00

319 lines
11 KiB
TypeScript

import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { CATALOG_IDENTITY } from "../../../src/catalogIdentity.js";
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
import {
evidenceResultOf,
expandToolAvailability,
toolAvailabilityInventory,
} from "../../../src/contracts/toolOutputSchemaPrimitives.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
const mainPath = fileURLToPath(
new URL("../../../dist/main.js", import.meta.url),
);
const fixturePath = fileURLToPath(
new URL("../../fixtures/fakeLauncher.mjs", import.meta.url),
);
const expectAvailableToolInventory = async (
client: Client,
listed: Awaited<ReturnType<Client["listTools"]>>,
) => {
const binarySession = listed.tools.find(
({ name }) => name === "binary_session",
);
if (binarySession === undefined)
throw new Error("The compiled catalog omitted binary_session");
const status = await client.callTool(
{ name: "binary_session", arguments: {} },
{ toolDefinition: binarySession },
);
expect(status.isError).not.toBe(true);
const inventory = z
.object({
result: z.object({
tool_availability: toolAvailabilityInventory,
}),
})
.parse(status.structuredContent).result.tool_availability;
const availability = expandToolAvailability(inventory);
// This asserts the compiled entrypoint matches the source catalog. When it
// fails, check that `dist/` is current (`npm run build:cached`) before
// suspecting a registration bug -- a stale build is by far the likeliest
// cause of a pure count mismatch here.
expect(
availability.length,
"compiled tool inventory is stale; run npm run build:cached",
).toBe(CATALOG_IDENTITY.counts.mcp_tools);
expect(new Set(listed.tools.map(({ name }) => name))).toEqual(
new Set(TOOL_CONTRACTS.map(({ name }) => name)),
);
return status;
};
/**
* Parses complete newline-delimited records, dropping any partial trailing
* line so a chunk boundary cannot turn into a JSON parse error.
*/
const completeStderrRecords = (stderr: string): unknown[] =>
stderr
.split("\n")
.slice(0, -1)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map((line): unknown => JSON.parse(line));
const callCurrentDocument = async (
target?: {
readonly path: string;
readonly kind?: "database";
},
inspectSession?: (client: Client) => Promise<void>,
) => {
const transport = new StdioClientTransport({
command: process.execPath,
args: [mainPath],
cwd: process.cwd(),
env: {
PATH: process.env.PATH ?? "",
HOPPER_LAUNCHER_PATH: process.execPath,
...(target === undefined
? {}
: {
HOPPER_TARGET_PATH: target.path,
...(target.kind === undefined
? {}
: { HOPPER_TARGET_KIND: target.kind }),
}),
HOPPER_LOADER_ARGS_JSON: JSON.stringify([fixturePath]),
},
stderr: "pipe",
});
let stderr = "";
transport.stderr?.on("data", (chunk: Buffer | string) => {
stderr += chunk.toString();
});
const client = new Client({ name: "runtime-smoke", version: "1.0.0" });
let result: Awaited<ReturnType<Client["callTool"]>>;
try {
await client.connect(transport);
result = await client.callTool({
name: "current_document",
arguments: {},
});
await inspectSession?.(client);
} finally {
await client.close();
await transport.close();
}
return { result, stderr, records: completeStderrRecords(stderr) };
};
describe("production stdio runtime", () => {
// The `test:acceptance` lane runs the real entrypoint on the current host;
// these cases state each supported host's result in the test name and body.
it("reports a missing target through MCP and structured logs", async () => {
const { result, stderr, records } = await callCurrentDocument();
expect(result.isError).toBe(true);
expect(result.content).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: "text",
text: expect.stringContaining("No app is open."),
}),
]),
);
expect(records).toContainEqual(
expect.objectContaining({
application: "rea",
mode: "mcp",
layer: "server",
tool: "current_document",
status: "error",
}),
);
expect(stderr).not.toContain("HOPPER_LOADER_ARGS_JSON");
expect(stderr).not.toContain(fixturePath);
}, 15_000);
it.runIf(process.platform === "darwin")(
"returns the selected document on macOS through MCP and structured logs",
async () => {
const { result, stderr, records } = await callCurrentDocument({
path: process.execPath,
});
expect(result.isError).not.toBe(true);
const observation = evidenceResultOf(z.literal("fixture")).parse(
result.structuredContent,
);
const evidence = parseEvidence(observation);
expect(evidence.operation).toBe("current_document");
expect(evidence.provider.id).toBe("hopper");
expect(evidence.normalized_result).toBe(observation.normalized_result);
// Hopper's reply is the result; it is not repeated as raw_result.
expect(evidence.raw_result).toBeNull();
expect(records).toContainEqual(
expect.objectContaining({
application: "rea",
mode: "mcp",
layer: "server",
tool: "current_document",
status: "ok",
}),
);
expect(stderr).not.toContain("HOPPER_LOADER_ARGS_JSON");
expect(stderr).not.toContain(fixturePath);
},
15_000,
);
// Use the suite's bounded acceptance deadline, including its coverage allowance.
// This cold-start workflow computes the growing catalog's schema digests; it
// verifies catalog and lifecycle behavior, not a 15-second performance target.
it("serves the compiled catalog without an initial target or fatal record", async () => {
const transport = new StdioClientTransport({
command: process.execPath,
args: [mainPath],
cwd: process.cwd(),
env: {
PATH: process.env.PATH ?? "",
},
stderr: "pipe",
});
let stderr = "";
transport.stderr?.on("data", (chunk: Buffer | string) => {
stderr += chunk.toString();
});
const client = new Client({ name: "catalog-runtime", version: "1.0.0" });
try {
await client.connect(transport);
// Keep catalog parity independent of initial target/provider startup.
// Validate each called tool against its advertised definition without
// asking the SDK to compile every unrelated output schema in the catalog.
const listed = await client.listTools();
const status = await expectAvailableToolInventory(client, listed);
expect(
z
.object({ result: z.object({ open: z.boolean() }) })
.parse(status.structuredContent).result,
).toEqual({ open: false });
// Closing the empty session produces a structured lifecycle record,
// making the fatal-record check observable without launching a provider.
const closeBinary = listed.tools.find(
({ name }) => name === "close_binary",
);
if (closeBinary === undefined)
throw new Error("The compiled catalog omitted close_binary");
const closed = await client.callTool(
{ name: "close_binary", arguments: {} },
{ toolDefinition: closeBinary },
);
expect(closed.isError).not.toBe(true);
} finally {
await client.close();
await transport.close();
}
// The transport has closed, so the stderr pipe has ended and every record
// the child wrote has been delivered. Reading while the child is still
// running races the pipe and can observe an empty or partial buffer, which
// would silently miss the fatal record this test exists to catch.
const records = completeStderrRecords(stderr);
expect(
records.length,
"no startup records were observed, so the fatal-record check is vacuous",
).toBeGreaterThan(0);
expect(records).toContainEqual(
expect.objectContaining({ tool: "close_binary", status: "ok" }),
);
const fatal = records.filter(
(record): record is { level: number } =>
typeof record === "object" &&
record !== null &&
"level" in record &&
typeof record.level === "number" &&
record.level >= 50,
);
expect(fatal).toEqual([]);
});
});
describe("production stdio saved-database admission", () => {
it.runIf(process.platform === "darwin")(
"refuses database analysis on macOS while keeping the session usable without a fatal record",
async () => {
const path = fileURLToPath(import.meta.url);
const { result, records } = await callCurrentDocument(
{ path, kind: "database" },
async (client) => {
const status = await expectAvailableToolInventory(
client,
await client.listTools(),
);
expect(status.structuredContent).toMatchObject({
result: {
open: true,
path,
kind: "database",
format: "analysis-database",
analysis_provider_binding: null,
},
});
},
);
expect(parseMcpToolError(result)).toMatchObject({
error: {
code: "capability_unavailable",
category: "unsupported_provider",
retryable: false,
details: {
operation: "current_document",
selection_reason: "target_unsupported",
rejections: expect.arrayContaining([
expect.objectContaining({
provider_id: "hopper",
code: "target_kind_unsupported",
reason: expect.stringContaining(
"cannot start REA's analysis bridge",
),
diagnostics: {
target_kind: "database",
target_format: "analysis-database",
architecture: null,
},
}),
]),
},
},
});
expect(records).toContainEqual(
expect.objectContaining({
tool: "current_document",
status: "error",
level: 40,
}),
);
expect(
records.some(
(record) =>
typeof record === "object" &&
record !== null &&
"level" in record &&
typeof record.level === "number" &&
record.level >= 50,
),
).toBe(false);
},
15_000,
);
});