Files
rea/tests/boundary/mcp/desktopApplicationMcp.test.ts
T
gensokyo-dev b4938aed58 feat(artifacts): triage desktop installation directories (#5207)
* feat(artifacts): triage desktop installation directories

* fix(artifacts): retain completed probes through failed file cleanup

* perf(artifacts): decode only assembly reference names for desktop triage

* fix(artifacts): enforce exact signatures and responsive directory probes

* fix(artifacts): recognize Unity bundles and bound FORM chunks

* fix(artifacts): retain bounded desktop omission evidence

* fix(artifacts): select ASAR targets before inventory probes

* chore: preserve tested upstream HTTP owner during integration

* fix(artifacts): reuse Mach-O signatures for FAT64 desktop probes

* fix(artifacts): expose bounded desktop probe diagnostics

* fix(artifacts): derive desktop snapshots from observed source identities

* fix(artifacts): admit desktop facts before JSON expansion
2026-10-20 04:01:36 +08:00

395 lines
14 KiB
TypeScript

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { execFileSync } from "node:child_process";
import { createPackageWithOptions } from "@electron/asar";
import { expect, it, onTestFinished } from "vitest";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { desktopApplicationSchema } from "../../../src/domain/desktopApplication.js";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { buildNativePeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js";
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
it.each([false, true])(
"follows the ASAR selection probe with a previously active target: %s",
async (previous) => {
const root = await createTestTempDirectory("rea-desktop-asar-probe-");
const source = join(root, "source");
const installation = join(root, "installation");
const resources = join(installation, "resources");
await mkdir(source);
await mkdir(resources, { recursive: true });
await writeFile(
join(source, "selected.js"),
"export const selected = true;\n",
);
const archive = join(resources, "app.asar");
await createPackageWithOptions(source, archive, {});
const session = createTestBinarySession(new ArtifactProvider(process.env));
const harness = await createApplicationMcpHarness({ session });
onTestFinished(harness.close);
if (previous) {
const other = join(root, "previous");
await mkdir(other);
await writeFile(
join(other, "previous.js"),
"export const previous = true;\n",
);
const path = join(root, "previous.asar");
await createPackageWithOptions(other, path, {});
const opened = await harness.client.callTool({
name: "open_binary",
arguments: { path },
});
expect(opened.isError).not.toBe(true);
}
const result = await harness.client.callTool({
name: "inspect_desktop_application",
arguments: { path: installation, detail: "complete" },
});
expect(result.isError).not.toBe(true);
const report = desktopApplicationSchema.parse(
parseEvidence(result.structuredContent).normalized_result,
);
const archiveFile = report.files.find(
(row) => row.path === "resources/app.asar",
);
if (archiveFile === undefined) throw new Error("Missing selected archive");
const probe = report.next_probes.find(
(row) =>
row.file_id === archiveFile.file_id &&
row.operation !== "analyze_javascript_application",
);
if (probe === undefined) throw new Error("Missing archive selection probe");
expect(probe.operation).toBe("open_binary");
const selected = await harness.client.callTool({
name: probe.operation,
arguments: { path: probe.path },
});
expect(selected.isError).not.toBe(true);
const inspected = await harness.client.callTool({
name: "inspect_artifact",
arguments: {},
});
expect(inspected.isError).not.toBe(true);
const inspection = artifactInspectionResultSchema.parse(
parseEvidence(inspected.structuredContent).normalized_result,
);
const inventory = artifactInventoryResultSchema.parse(
inspection.substeps[0]?.evidence.normalized_result,
);
expect(inventory.occurrences.map((row) => row.logical_path)).toContain(
"selected.js",
);
expect(inventory.occurrences.map((row) => row.logical_path)).not.toContain(
"previous.js",
);
},
);
it("advertises exact schemas and retains a compact directory answer with typed selected pages", async () => {
const root = await createTestTempDirectory("rea-desktop-mcp-");
await mkdir(join(root, "resources"));
await writeFile(join(root, "resources/app.asar"), "path marker only");
const session = createTestBinarySession(() => {
throw new Error("Directory inspection must not acquire a deep provider");
});
const server = createServer({ kind: "session", session });
const client = new Client({ name: "desktop-directory-test", 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);
const schemas = await advertisedMcpToolSchemas(
client,
"inspect_desktop_application",
);
const views = await advertisedMcpToolSchemas(client, "inspect_analysis_view");
expect(schemas.ajv.validate(schemas.inputSchema, { path: root })).toBe(true);
expect(
schemas.ajv.validate(schemas.inputSchema, { path: root, execute: true }),
).toBe(false);
const result = await client.callTool({
name: "inspect_desktop_application",
arguments: { path: root },
});
expect(result.isError).not.toBe(true);
expect(
schemas.ajv.validate(schemas.outputSchema, result.structuredContent),
JSON.stringify(schemas.ajv.errors),
).toBe(true);
const summary = parseEvidence(result.structuredContent);
const view = analysisViewResultSchema.parse(summary.normalized_result);
expect(view.kind).toBe("summary");
const retained = session.evidenceById(view.parent_evidence_id);
expect(retained).toBeDefined();
const full = desktopApplicationSchema.parse(retained?.normalized_result);
expect(full.candidates.map((candidate) => candidate.family)).toEqual([
"electron",
]);
const archive = full.files.find((file) => file.path === "resources/app.asar");
if (archive === undefined)
throw new Error("Missing retained ASAR convention");
const selected = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source: {
kind: "retained-evidence",
evidence_id: view.parent_evidence_id,
},
view: {
kind: "desktop-application",
collection: "files",
file_ids: [archive.file_id],
fields: ["path", "read_status"],
limit: 1,
},
},
});
expect(selected.isError).not.toBe(true);
expect(
views.ajv.validate(views.outputSchema, selected.structuredContent),
JSON.stringify(views.ajv.errors),
).toBe(true);
const page = parseEvidence(selected.structuredContent);
expect(page.evidence_links).toEqual([view.parent_evidence_id]);
expect(page.normalized_result).toMatchObject({
items: [
{
file_id: expect.any(String),
path: "resources/app.asar",
read_status: "not-selected",
},
],
source_coverage: { status: "complete-within-scan" },
coverage: { total: 1, exhausted: true },
});
const complete = await client.callTool({
name: "inspect_desktop_application",
arguments: { path: root, detail: "complete" },
});
expect(complete.isError).not.toBe(true);
expect(parseEvidence(complete.structuredContent).evidence_id).toBe(
view.parent_evidence_id,
);
});
it("surfaces unavailable probes and filters their complete retained diagnostics through MCP", async () => {
const session = createTestBinarySession(() => {
throw new Error("Directory inspection must not acquire a deep provider");
});
const harness = await createApplicationMcpHarness({ session });
onTestFinished(harness.close);
const client = harness.client;
const schemas = await advertisedMcpToolSchemas(
client,
"inspect_desktop_application",
);
const views = await advertisedMcpToolSchemas(client, "inspect_analysis_view");
const partialRoot = await createTestTempDirectory(
"rea-desktop-probe-issues-mcp-",
);
await mkdir(join(partialRoot, "clean"));
for (let index = 0; index < 20; index++)
await writeFile(
join(partialRoot, `probe${String(index).padStart(2, "0")}.exe`),
buildNativePeFixture(),
);
const partial = await client.callTool({
name: "inspect_desktop_application",
arguments: { path: partialRoot, max_probe_bytes: 4096 },
});
expect(partial.isError).not.toBe(true);
expect(
schemas.ajv.validate(schemas.outputSchema, partial.structuredContent),
).toBe(true);
const partialEvidence = parseEvidence(partial.structuredContent);
const partialView = analysisViewResultSchema.parse(
partialEvidence.normalized_result,
);
expect(partialView).toMatchObject({
kind: "summary",
summary: {
probe_issues: {
file_count: expect.any(Number),
issue_count: expect.any(Number),
preview_truncated: true,
},
},
});
const partialSource = session.evidenceById(partialView.parent_evidence_id);
const partialReport = desktopApplicationSchema.parse(
partialSource?.normalized_result,
);
const failed = partialReport.files.filter((file) => file.issues.length > 0);
expect(failed.length).toBeGreaterThan(2);
expect(
failed.every((file) =>
file.issues.some((reason) => /budget/iu.test(reason)),
),
).toBe(true);
expect(partialView).toMatchObject({
summary: {
source_coverage: { status: "partial", bytes_read: expect.any(Number) },
probe_issues: {
file_count: failed.length,
issue_count: failed.reduce((sum, file) => sum + file.issues.length, 0),
preview: failed.slice(0, 2).map((file) => ({
file_id: file.file_id,
path_preview: file.path,
read_status: file.read_status,
issue_count: file.issues.length,
first_issue_preview: file.issues[0],
text_truncated: false,
})),
},
},
});
for (const [hasIssues, offset, paths, total] of [
[true, 0, failed.slice(0, 2).map((file) => file.path), failed.length],
[true, failed.length - 1, [failed.at(-1)!.path], failed.length],
[false, 0, ["clean"], 1],
] as const) {
const args = {
source: {
kind: "retained-evidence",
evidence_id: partialView.parent_evidence_id,
},
view: {
kind: "desktop-application",
collection: "files",
has_issues: hasIssues,
...(hasIssues ? {} : { paths: ["clean"] }),
fields: ["path", "read_status", "issues"],
offset,
limit: 2,
},
};
expect(views.ajv.validate(views.inputSchema, args)).toBe(true);
const response = await client.callTool({
name: "inspect_analysis_view",
arguments: args,
});
expect(response.isError).not.toBe(true);
expect(
views.ajv.validate(views.outputSchema, response.structuredContent),
).toBe(true);
const evidence = parseEvidence(response.structuredContent);
expect(evidence.evidence_links).toEqual([partialView.parent_evidence_id]);
expect(evidence.normalized_result).toMatchObject({
items: paths.map((path) => {
const file = partialReport.files.find((row) => row.path === path)!;
return {
file_id: file.file_id,
path,
read_status: file.read_status,
issues: file.issues,
};
}),
source_coverage: { status: "partial" },
coverage: { total, exhausted: offset + paths.length === total },
});
}
});
it.skipIf(process.platform === "win32")(
"retains special-entry facts and advertises batched omission selections through MCP",
async () => {
const root = await createTestTempDirectory("rea-desktop-omissions-mcp-");
execFileSync("mkfifo", [join(root, "b"), join(root, "a")], {
timeout: 10_000,
});
const harness = await createApplicationMcpHarness();
onTestFinished(harness.close);
const schemas = await advertisedMcpToolSchemas(
harness.client,
"inspect_analysis_view",
);
const inspected = await harness.client.callTool({
name: "inspect_desktop_application",
arguments: { path: root },
});
expect(inspected.isError).not.toBe(true);
const summary = analysisViewResultSchema.parse(
parseEvidence(inspected.structuredContent).normalized_result,
);
expect(summary).toMatchObject({
summary: { available_counts: { omissions: 2 } },
});
const source = {
kind: "retained-evidence",
evidence_id: summary.parent_evidence_id,
};
const listed = await harness.client.callTool({
name: "inspect_analysis_view",
arguments: {
source,
view: {
kind: "desktop-application",
collection: "omissions",
limit: 2,
},
},
});
expect(listed.isError).not.toBe(true);
expect(
schemas.ajv.validate(schemas.outputSchema, listed.structuredContent),
).toBe(true);
const page = analysisViewResultSchema.parse(
parseEvidence(listed.structuredContent).normalized_result,
);
if (page.kind !== "desktop-application") throw new Error("Expected rows");
const ids = page.items.map((row) => {
if (!("omission_id" in row) || typeof row.omission_id !== "string")
throw new Error("Missing stable omission identity");
return row.omission_id;
});
expect(page.items).toMatchObject([
{ path: "a", kind: "fifo" },
{ path: "b", kind: "fifo" },
]);
for (const selection of [ids, []]) {
const args = {
source,
view: {
kind: "desktop-application",
collection: "omissions",
omission_ids: selection,
fields: ["kind"],
limit: 2,
},
};
expect(schemas.ajv.validate(schemas.inputSchema, args)).toBe(true);
const selected = await harness.client.callTool({
name: "inspect_analysis_view",
arguments: args,
});
expect(selected.isError).not.toBe(true);
expect(
schemas.ajv.validate(schemas.outputSchema, selected.structuredContent),
).toBe(true);
const evidence = parseEvidence(selected.structuredContent);
expect(evidence.evidence_links).toEqual([summary.parent_evidence_id]);
expect(evidence.normalized_result).toHaveProperty(
"items",
selection.map((omission_id) => ({ omission_id, kind: "fifo" })),
);
expect(evidence.normalized_result).toMatchObject({
source_coverage: { status: "partial" },
});
}
},
);