mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 13:26:10 +00:00
* fix(native): retain failed pwntools process and runtime cleanup Keep failed worker/debugger cleanup owners and their private files until verified process shutdown and successful directory removal. Retry cleanup before another inspection and expose shutdown through the shared services, CLI and MCP server. Serialize acquisition without delaying cancellation of queued callers, and preserve primary diagnostics when CLI cleanup fails. The original decoder reproduction left a live owned Node child with its runtime deleted and no retained cleanup owner; the regression now verifies retention, admission blocking, retries and eventual process/file cleanup. Validation: 10 lifecycle/public CLI/MCP tests, 20 service tests, test discovery 814/814, build:cached and full check including architecture guards. Actual POSIX process/filesystem boundaries used injected Node workers on macOS; real Linux pwntools/GDB provider parsing was not exercised. * test(native): verify retained recorded-crash cleanup lifecycle * fix(native): finalize diagnostic cleanup before publishing results * fix(native): retire decoder stages before reading runtime evidence --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
174 lines
5.9 KiB
TypeScript
174 lines
5.9 KiB
TypeScript
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it, onTestFinished } from "vitest";
|
|
|
|
import { BinaryLayoutService } from "../../../src/application/binaryDiagnostics/BinaryLayoutService.js";
|
|
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
|
import { ok } from "../../../src/domain/result.js";
|
|
import { createServer } from "../../../src/server/createServer.js";
|
|
import { analysisViewLayoutFixture } from "../../fixtures/analysisView.js";
|
|
import { BINARY_LAYOUT_TEST_PROVIDER } from "../../fixtures/binaryDiagnostics/layout.js";
|
|
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
const contract = toolContract("analyze_javascript_application");
|
|
|
|
const connect = async (
|
|
retainsEvidence: boolean,
|
|
binaryLayout?: BinaryLayoutService,
|
|
) => {
|
|
const session = createTestBinarySession(() => {
|
|
throw new Error("JavaScript analysis must not start a deep provider");
|
|
});
|
|
const server = createServer(
|
|
retainsEvidence
|
|
? { kind: "session", session }
|
|
: {
|
|
kind: "fixed",
|
|
analysis: {
|
|
execute: () => {
|
|
throw new Error("JavaScript analysis must not use a target");
|
|
},
|
|
},
|
|
},
|
|
binaryLayout === undefined ? {} : { binaryLayout },
|
|
);
|
|
const client = new Client({ name: "analysis-summary-detail", version: "1" });
|
|
onTestFinished(async () => {
|
|
await client.close();
|
|
await server.close();
|
|
await session.close();
|
|
});
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
return client;
|
|
};
|
|
|
|
const application = async () => {
|
|
const root = await createTestTempDirectory("rea-summary-detail-");
|
|
await writeFile(
|
|
join(root, "main.js"),
|
|
'import { helper } from "./util.js";\nexport const greet = (name) => helper(name);\n',
|
|
);
|
|
await writeFile(
|
|
join(root, "util.js"),
|
|
"export const helper = (value) => value.toUpperCase();\n",
|
|
);
|
|
return { input_path: root, format: "directory" };
|
|
};
|
|
|
|
const textBytes = (content: unknown): number => {
|
|
const [first] = Array.isArray(content) ? content : [];
|
|
if (first?.type !== "text") throw new Error("Missing MCP text result");
|
|
return Buffer.byteLength(String(first.text));
|
|
};
|
|
|
|
it("returns the summary of complete Evidence retained for later views", async () => {
|
|
const client = await connect(true);
|
|
const input = await application();
|
|
const complete = await client.callTool({
|
|
name: contract.name,
|
|
arguments: input,
|
|
});
|
|
expect(complete.isError).not.toBe(true);
|
|
const completeEvidence = contract.outputSchema.parse(
|
|
complete.structuredContent,
|
|
);
|
|
expect(completeEvidence.operation).toBe("analyze_javascript_application");
|
|
|
|
const summary = await client.callTool({
|
|
name: contract.name,
|
|
arguments: { ...input, detail: "summary" },
|
|
});
|
|
expect(summary.isError).not.toBe(true);
|
|
const summaryEvidence = contract.outputSchema.parse(
|
|
summary.structuredContent,
|
|
);
|
|
expect(summaryEvidence).toMatchObject({
|
|
operation: "inspect_analysis_view",
|
|
evidence_links: [completeEvidence.evidence_id],
|
|
normalized_result: {
|
|
kind: "summary",
|
|
parent_evidence_id: completeEvidence.evidence_id,
|
|
parent_operation: "analyze_javascript_application",
|
|
},
|
|
});
|
|
expect(textBytes(summary.content)).toBeLessThan(textBytes(complete.content));
|
|
|
|
const modules = await client.callTool({
|
|
name: "inspect_analysis_view",
|
|
arguments: {
|
|
source: {
|
|
kind: "retained-evidence",
|
|
evidence_id: completeEvidence.evidence_id,
|
|
},
|
|
view: { kind: "page", collection: "modules", offset: 0, limit: 8 },
|
|
},
|
|
});
|
|
expect(modules.isError).not.toBe(true);
|
|
expect(modules.structuredContent).toMatchObject({
|
|
normalized_result: {
|
|
kind: "page",
|
|
parent_evidence_id: completeEvidence.evidence_id,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("refuses summary detail when the server cannot retain the complete Evidence", async () => {
|
|
const client = await connect(false);
|
|
const failure = parseMcpToolError(
|
|
await client.callTool({
|
|
name: contract.name,
|
|
arguments: { ...(await application()), detail: "summary" },
|
|
}),
|
|
);
|
|
expect(failure.error).toMatchObject({
|
|
code: "capability_unavailable",
|
|
message: expect.stringContaining("detail complete"),
|
|
});
|
|
});
|
|
|
|
it("returns the summary of retained binary layout Evidence", async () => {
|
|
const layout = analysisViewLayoutFixture();
|
|
const client = await connect(
|
|
true,
|
|
new BinaryLayoutService({
|
|
inspectAndClose(input, options) {
|
|
return this.inspect(input, options);
|
|
},
|
|
identity: BINARY_LAYOUT_TEST_PROVIDER,
|
|
inspect: () => Promise.resolve(ok(layout)),
|
|
}),
|
|
);
|
|
const layoutContract = toolContract("inspect_binary_layout");
|
|
const summary = await client.callTool({
|
|
name: layoutContract.name,
|
|
arguments: { path: layout.artifact.path, detail: "summary" },
|
|
});
|
|
expect(summary.isError).not.toBe(true);
|
|
const evidence = layoutContract.outputSchema.parse(summary.structuredContent);
|
|
expect(evidence).toMatchObject({
|
|
operation: "inspect_analysis_view",
|
|
normalized_result: {
|
|
kind: "summary",
|
|
parent_operation: "inspect_binary_layout",
|
|
},
|
|
});
|
|
const [parentId] = evidence.evidence_links;
|
|
const sections = await client.callTool({
|
|
name: "inspect_analysis_view",
|
|
arguments: {
|
|
source: { kind: "retained-evidence", evidence_id: parentId },
|
|
view: { kind: "page", collection: "sections", offset: 0, limit: 8 },
|
|
},
|
|
});
|
|
expect(sections.isError).not.toBe(true);
|
|
expect(sections.structuredContent).toMatchObject({
|
|
normalized_result: { kind: "page", parent_evidence_id: parentId },
|
|
});
|
|
});
|