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>
180 lines
6.5 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
},
|
|
);
|