Files
rea/tests/acceptance/managedMemberRecovery.test.ts
T
Can 371f3d91ae fix(managed): retain type discovery after member limits (#5312)
fix(managed): retain types after member limits

Admit type identity and a bounded recovery envelope before member decoding so callers can discover smaller selections after capacity exhaustion.

Fixes #5146

Co-authored-by: eco-null <eco-null@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-23 06:07:03 +08:00

180 lines
6.5 KiB
TypeScript

import { createHash } from "node:crypto";
import { unlink, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect } from "vitest";
import { parseEvidence } from "../../src/domain/evidence.js";
import { managedMemberInspectionSchema } from "../../src/domain/managed/managedArtifact.js";
import {
analysisViewResultSchema,
managedMemberSummaryViewResultSchema,
} from "../../src/domain/analysisView/analysisView.js";
import { buildManagedMemberCapacityFixture } from "../fixtures/managedMemberCapacity.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
const createTransport = () =>
new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: {
PATH: process.env.PATH ?? "",
REA_LOG_LEVEL: "silent",
...(process.platform === "win32"
? {
SystemRoot: process.env.SystemRoot ?? "",
TEMP: tmpdir(),
TMP: tmpdir(),
}
: {}),
},
stderr: "pipe",
});
cliTest(
"CLI and stdio MCP recover type discovery and preserve partial retained pages",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-managed-member-recovery-");
const path = join(root, "large-method.dll");
const bytes = buildManagedMemberCapacityFixture();
await writeFile(path, bytes);
const sha256 = createHash("sha256").update(bytes).digest("hex");
const transport = createTransport();
transport.stderr?.on("data", () => undefined);
const client = new Client({ name: "managed-type-recovery", version: "1" });
try {
await client.connect(transport);
const definition = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_managed_members",
);
const ajv = new Ajv2020({ strict: false, validateFormats: false });
if (definition?.outputSchema === undefined)
throw new Error("Managed member schema is absent");
const outputSchema: Record<string, unknown> = definition.outputSchema;
expect(ajv.validateSchema(definition.inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
const output = await cli.run({
arguments: ["inspect-managed-members", path, "--json"],
});
expect(output.exitCode, output.stderr).toBe(0);
const cliEvidence = parseEvidence(output.json);
const response = await client.callTool({
name: "inspect_managed_members",
arguments: { path },
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(
ajv.validate(outputSchema, response.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const mcpEvidence = parseEvidence(response.structuredContent);
expect(mcpEvidence.normalized_result).toEqual(
cliEvidence.normalized_result,
);
expect(mcpEvidence.subject?.digest.sha256).toBe(sha256);
expect(cliEvidence.subject?.digest.sha256).toBe(sha256);
const report = managedMemberInspectionSchema.parse(
mcpEvidence.normalized_result,
);
expect(report.coverage.state).toBe("partial");
expect(report.types).toHaveLength(3);
expect(report.methods).toEqual([]);
const empty = report.types.find((type) => type.method_list.count === 0);
expect(empty).toBeDefined();
const selectedCli = await cli.run({
arguments: [
"inspect-managed-members",
path,
"--type",
empty!.full_name,
"--json",
],
});
expect(selectedCli.exitCode, selectedCli.stderr).toBe(0);
const selected = await client.callTool({
name: "inspect_managed_members",
arguments: { path, type_names: [empty!.full_name] },
});
expect(selected.isError).not.toBe(true);
const selectedEvidence = parseEvidence(selected.structuredContent);
expect(selectedEvidence.normalized_result).toEqual(
parseEvidence(selectedCli.json).normalized_result,
);
expect(selectedEvidence.normalized_result).toMatchObject({
coverage: { state: "complete", issues: [] },
});
const summaryReply = await client.callTool({
name: "inspect_managed_members",
arguments: { path, detail: "summary" },
});
expect(summaryReply.isError).not.toBe(true);
const summary = managedMemberSummaryViewResultSchema.parse(
parseEvidence(summaryReply.structuredContent).normalized_result,
);
expect(summary.summary).toMatchObject({
coverage: { state: "partial" },
reported_counts: {
types: 3,
methods: 0,
exported_types: null,
user_strings: null,
},
});
await unlink(path);
for (const collection of ["types", "methods"] as const) {
const reply = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source: {
kind: "retained-evidence",
evidence_id: summary.parent_evidence_id,
},
view: { kind: "managed-members", collection, offset: 0, limit: 10 },
},
});
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
const page = analysisViewResultSchema.parse(
parseEvidence(reply.structuredContent).normalized_result,
);
if (page.kind !== "managed-members")
throw new Error("Expected managed member page");
expect(page.source_coverage.state).toBe("partial");
expect(page.page.rows).toEqual(
collection === "types" ? report.types : [],
);
}
const unknown = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source: {
kind: "retained-evidence",
evidence_id: summary.parent_evidence_id,
},
view: {
kind: "managed-members",
collection: "exported_types",
offset: 0,
limit: 10,
},
},
});
expect(unknown.isError).toBe(true);
expect(JSON.stringify(unknown)).toContain(
"did not examine exported_types",
);
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
},
);