Add opt-in Go build summaries and selected retained metadata pages while preserving complete Evidence and source identity. Validated with 258 focused tests, full static/docs checks, real CLI/SDK workflows and native metadata oracles. CI baseline remediation assertions reproduce on unmodified main; fail-fast cancels two other shards. Owner authorized bypass with those coverage limits recorded in PR #5524. Fixes #5499.
383 lines
12 KiB
TypeScript
383 lines
12 KiB
TypeScript
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect } from "vitest";
|
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
import type { AnySchema } from "ajv";
|
|
import type { Client } from "@modelcontextprotocol/client";
|
|
import {
|
|
createEvidence,
|
|
parseEvidence,
|
|
type Evidence,
|
|
} from "../../../src/domain/evidence.js";
|
|
import { goBinarySchema } from "../../../src/domain/go/goBinary.js";
|
|
import {
|
|
analysisViewResultSchema,
|
|
goBinarySummaryViewResultSchema,
|
|
} from "../../../src/domain/analysisView/analysisView.js";
|
|
import { createServer } from "../../../src/server/createServer.js";
|
|
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
|
import {
|
|
createGoBinaryFixture,
|
|
GO_MODULE_TEXT,
|
|
} from "../../fixtures/go/image.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { mcpTest } from "../../support/mcp/mcpFixture.js";
|
|
|
|
type Operation = "inspect_go_binary" | "inspect_analysis_view";
|
|
const goViewTest = mcpTest.extend<{
|
|
goViews: {
|
|
client: Client;
|
|
root: string;
|
|
path: string;
|
|
bytes: Buffer;
|
|
call: (
|
|
name: Operation,
|
|
input: Record<string, unknown>,
|
|
) => Promise<Evidence>;
|
|
};
|
|
}>({
|
|
goViews: async ({ mcp }, use) => {
|
|
const root = await createTestTempDirectory("rea-go-views-");
|
|
const path = join(root, "application");
|
|
const fixture = createGoBinaryFixture({
|
|
goVersion: "go1.26.0",
|
|
moduleText:
|
|
GO_MODULE_TEXT +
|
|
"dep\texample.com/dependency\tv2.0.0\th1:second\nfuture\tunknown\n",
|
|
});
|
|
await writeFile(path, fixture.bytes);
|
|
const session = createTestBinarySession(() => {
|
|
throw new Error("No disassembler for Go views");
|
|
});
|
|
try {
|
|
const client = await mcp.connect(
|
|
createServer({ kind: "session", session }),
|
|
);
|
|
const catalog = await client.listTools();
|
|
const ajv = new Ajv2020({ strict: false });
|
|
const schemas = new Map(
|
|
catalog.tools
|
|
.filter(
|
|
(tool) =>
|
|
tool.name === "inspect_go_binary" ||
|
|
tool.name === "inspect_analysis_view",
|
|
)
|
|
.map((tool) => {
|
|
const output = tool.outputSchema as AnySchema;
|
|
expect(tool.outputSchema?.$schema).toBe(
|
|
"https://json-schema.org/draft/2020-12/schema",
|
|
);
|
|
expect(ajv.validateSchema(output)).toBe(true);
|
|
return [
|
|
tool.name,
|
|
{
|
|
input: ajv.compile(tool.inputSchema),
|
|
output: ajv.compile(output),
|
|
},
|
|
] as const;
|
|
}),
|
|
);
|
|
const call = async (name: Operation, input: Record<string, unknown>) => {
|
|
const schema = schemas.get(name)!;
|
|
expect(schema.input(input), JSON.stringify(schema.input.errors)).toBe(
|
|
true,
|
|
);
|
|
const reply = await client.callTool({ name, arguments: input });
|
|
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
|
expect(
|
|
schema.output(reply.structuredContent),
|
|
JSON.stringify(schema.output.errors),
|
|
).toBe(true);
|
|
return parseEvidence(reply.structuredContent);
|
|
};
|
|
await use({ client, root, path, bytes: fixture.bytes, call });
|
|
} finally {
|
|
await session.close();
|
|
}
|
|
},
|
|
});
|
|
|
|
const goPage = (evidence: Evidence) => {
|
|
const view = analysisViewResultSchema.parse(evidence.normalized_result);
|
|
if (view.kind !== "go") throw new Error("Expected a Go row page");
|
|
return view;
|
|
};
|
|
|
|
goViewTest(
|
|
"rejects authenticated inline and file parents with conflicting architecture",
|
|
async ({ goViews: { client, root, path, call } }) => {
|
|
const complete = await call("inspect_go_binary", { path });
|
|
const report = goBinarySchema.parse(complete.normalized_result);
|
|
const conflicting = createEvidence(
|
|
{
|
|
path,
|
|
sha256: report.artifact.sha256,
|
|
format: "elf",
|
|
architecture: "arm64",
|
|
},
|
|
{ id: "go-build-info", name: "Go metadata test fixture", version: "1" },
|
|
{
|
|
operation: "inspect_go_binary",
|
|
parameters: { path },
|
|
result: report,
|
|
confidence: "observed",
|
|
},
|
|
);
|
|
const evidencePath = join(root, "conflicting.json");
|
|
await writeFile(evidencePath, JSON.stringify(conflicting));
|
|
for (const source of [
|
|
{ kind: "inline", evidence: conflicting },
|
|
{ kind: "evidence-file", path: evidencePath },
|
|
]) {
|
|
for (const view of [
|
|
{ kind: "summary" },
|
|
{ kind: "go", collection: "dependencies" },
|
|
]) {
|
|
const reply = await client.callTool({
|
|
name: "inspect_analysis_view",
|
|
arguments: { source, view },
|
|
});
|
|
expect(parseMcpToolError(reply).error).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
operation: "inspect_analysis_view",
|
|
issues: [{ path: ["source", "subject"], reason: "invalid_value" }],
|
|
},
|
|
});
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
goViewTest(
|
|
"retains exact Go rows and complete Evidence after source removal",
|
|
async ({ goViews: { client, root, path, bytes, call } }) => {
|
|
const complete = await call("inspect_go_binary", { path });
|
|
const report = goBinarySchema.parse(complete.normalized_result);
|
|
const summary = await call("inspect_go_binary", {
|
|
path,
|
|
detail: "summary",
|
|
});
|
|
expect(summary).toMatchObject({
|
|
raw_result: null,
|
|
normalized_result: {
|
|
kind: "summary",
|
|
parent_evidence_id: complete.evidence_id,
|
|
source_coverage: {
|
|
build_info_available: true,
|
|
module_parse_complete: false,
|
|
runtime_dependencies: "unknown",
|
|
},
|
|
summary: {
|
|
build_info: {
|
|
go_version: "go1.26.0",
|
|
module: {
|
|
available_counts: {
|
|
dependencies: 2,
|
|
settings: 3,
|
|
unparsed_lines: 1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(await readFile(path)).toEqual(bytes);
|
|
await unlink(path);
|
|
const source = {
|
|
kind: "retained-evidence",
|
|
evidence_id: complete.evidence_id,
|
|
};
|
|
const view = {
|
|
kind: "go",
|
|
collection: "dependencies",
|
|
paths: ["example.com/dependency"],
|
|
fields: ["version", "replacement"],
|
|
offset: 0,
|
|
limit: 1,
|
|
};
|
|
const first = goPage(await call("inspect_analysis_view", { source, view }));
|
|
expect(first.page.rows).toEqual([
|
|
{
|
|
index: 0,
|
|
version: "v1.2.3",
|
|
replacement: report.build_info!.module.dependencies[0]!.replacement,
|
|
},
|
|
]);
|
|
expect(first.coverage).toMatchObject({
|
|
total: 2,
|
|
next_offset: 1,
|
|
exhausted: false,
|
|
});
|
|
const second = goPage(
|
|
await call("inspect_analysis_view", {
|
|
source,
|
|
view: { ...view, offset: 1 },
|
|
}),
|
|
);
|
|
expect(second.page.rows).toEqual([
|
|
{ index: 1, version: "v2.0.0", replacement: null },
|
|
]);
|
|
expect(second.coverage).toMatchObject({
|
|
total: 2,
|
|
next_offset: null,
|
|
exhausted: true,
|
|
});
|
|
const bundlePath = join(root, "evidence.json");
|
|
const exported = await client.callTool({
|
|
name: "export_evidence_bundle",
|
|
arguments: { path: bundlePath },
|
|
});
|
|
expect(exported.isError, JSON.stringify(exported)).not.toBe(true);
|
|
const bundle = JSON.parse(await readFile(bundlePath, "utf8")) as {
|
|
records: unknown[];
|
|
};
|
|
expect(
|
|
bundle.records
|
|
.map((record) => parseEvidence(record))
|
|
.find((record) => record.evidence_id === complete.evidence_id),
|
|
).toEqual(complete);
|
|
await client.ping();
|
|
},
|
|
);
|
|
|
|
goViewTest(
|
|
"intersects exact Go selections and keeps row identity with no selected fields",
|
|
async ({ goViews: { client, path, call } }) => {
|
|
const summary = goBinarySummaryViewResultSchema.parse(
|
|
(await call("inspect_go_binary", { path, detail: "summary" }))
|
|
.normalized_result,
|
|
);
|
|
const source = {
|
|
kind: "retained-evidence",
|
|
evidence_id: summary.parent_evidence_id,
|
|
};
|
|
const identity = goPage(
|
|
await call("inspect_analysis_view", {
|
|
source,
|
|
view: { kind: "go", collection: "dependencies", fields: [] },
|
|
}),
|
|
);
|
|
expect(identity.page.rows).toEqual([{ index: 0 }, { index: 1 }]);
|
|
for (const selection of [
|
|
{ paths: [] },
|
|
{ indices: [] },
|
|
{ paths: ["other"], indices: [1] },
|
|
{ offset: 99 },
|
|
]) {
|
|
const empty = goPage(
|
|
await call("inspect_analysis_view", {
|
|
source,
|
|
view: { kind: "go", collection: "dependencies", ...selection },
|
|
}),
|
|
);
|
|
expect(empty.page.rows).toEqual([]);
|
|
expect(empty.coverage).toMatchObject({
|
|
next_offset: null,
|
|
exhausted: true,
|
|
});
|
|
}
|
|
const selected = goPage(
|
|
await call("inspect_analysis_view", {
|
|
source,
|
|
view: {
|
|
kind: "go",
|
|
collection: "dependencies",
|
|
paths: ["example.com/dependency"],
|
|
indices: [1],
|
|
fields: ["version"],
|
|
},
|
|
}),
|
|
);
|
|
expect(selected.page.rows).toEqual([{ index: 1, version: "v2.0.0" }]);
|
|
const setting = goPage(
|
|
await call("inspect_analysis_view", {
|
|
source,
|
|
view: {
|
|
kind: "go",
|
|
collection: "settings",
|
|
keys: ["GOOS"],
|
|
fields: ["value"],
|
|
},
|
|
}),
|
|
);
|
|
expect(setting.page.rows).toEqual([{ index: 1, value: "linux" }]);
|
|
expect(setting.coverage.total).toBe(1);
|
|
const invalid = await client.callTool({
|
|
name: "inspect_analysis_view",
|
|
arguments: {
|
|
source,
|
|
view: {
|
|
kind: "go",
|
|
collection: "dependencies",
|
|
fields: ["module_bytes_base64"],
|
|
},
|
|
},
|
|
});
|
|
expect(invalid.isError).toBe(true);
|
|
},
|
|
);
|
|
|
|
mcpTest(
|
|
"keeps unavailable Go metadata unknown in summaries and empty pages",
|
|
async ({ mcp }) => {
|
|
const root = await createTestTempDirectory("rea-go-absent-view-");
|
|
const path = join(root, "application");
|
|
await writeFile(path, createGoBinaryFixture({ omitBuildInfo: true }).bytes);
|
|
const session = createTestBinarySession(() => {
|
|
throw new Error("No deep provider");
|
|
});
|
|
try {
|
|
const client = await mcp.connect(
|
|
createServer({ kind: "session", session }),
|
|
);
|
|
const reply = await client.callTool({
|
|
name: "inspect_go_binary",
|
|
arguments: { path, detail: "summary" },
|
|
});
|
|
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
|
const summary = goBinarySummaryViewResultSchema.parse(
|
|
parseEvidence(reply.structuredContent).normalized_result,
|
|
);
|
|
expect(summary).toMatchObject({
|
|
summary: { build_info: null },
|
|
source_coverage: {
|
|
build_info_available: false,
|
|
module_parse_complete: null,
|
|
runtime_dependencies: "unknown",
|
|
},
|
|
});
|
|
const source = {
|
|
kind: "retained-evidence",
|
|
evidence_id: summary.parent_evidence_id,
|
|
};
|
|
const empty = await client.callTool({
|
|
name: "inspect_analysis_view",
|
|
arguments: { source, view: { kind: "go", collection: "dependencies" } },
|
|
});
|
|
expect(empty.isError, JSON.stringify(empty)).not.toBe(true);
|
|
const page = goPage(parseEvidence(empty.structuredContent));
|
|
expect(page).toMatchObject({
|
|
page: { rows: [] },
|
|
coverage: { total: 0 },
|
|
source_coverage: {
|
|
build_info_available: false,
|
|
module_parse_complete: null,
|
|
runtime_dependencies: "unknown",
|
|
},
|
|
});
|
|
expect(page.unknowns.join(" ")).toContain("remain unknown");
|
|
const incompatible = await client.callTool({
|
|
name: "inspect_analysis_view",
|
|
arguments: { source, view: { kind: "native", facet: "procedure" } },
|
|
});
|
|
expect(parseMcpToolError(incompatible).error.code).toBe(
|
|
"invalid_request",
|
|
);
|
|
} finally {
|
|
await session.close();
|
|
}
|
|
},
|
|
);
|