Files
reverse-engineering-anything/src/server/EvidenceFileDiagnosticsMcp.test.ts
T
吴佳浩Alben 1c0a02d4a4 fix(evidence): bound malformed file field diagnostics (#8557)
* fix(evidence): bound malformed file field diagnostics

`readEvidenceFileSource` projected every validator issue, and the generic error
delivery applied the MCP response budget afterwards. A file with many invalid
fields therefore produced a transport `resource_constraint` that replaced the
typed file error, so the caller lost the selected path entirely. Issue #8453
reports exactly that: 20,000 invalid values still returned a direct
`invalid_request` with the path and every field issue, while 125,000 returned
`resource_constraint` with neither.

`projectInputIssuesWithinBudget` already existed for this but had no production
caller. The reader now projects within a 64 KiB allowance and, when the whole list
does not fit, keeps the largest leading group that does and appends one explicit
omission issue naming how many of how many were dropped. The typed file error and
its `path` therefore survive the response boundary, and the diagnostics stay
actionable and explicitly incomplete instead of hiding the real failure.

Two regressions cover the bound and the fits-in-budget case. With the unbounded
projection restored, the bound test and its supporting fixtures fail.

* chore: retrigger title and readiness checks after the title was corrected

* fix(evidence): bound file diagnostics in one pass and cover the MCP boundary

Review findings on the first version.

**The prefix was not maximal.** `boundedContentIssues` halved the list and returned
the first attempt that fit, so a file just over the allowance lost roughly half its
diagnostics. Prefix selection now lives in the shared projector
(`projectInputIssuesBounded`), which walks the issues once and stops exactly at the
allowance, keeping the largest fitting prefix.

**A compound issue could still lose its location.** One `unrecognized_keys` issue
naming hundreds of keys expands into hundreds of field diagnostics and can exceed
the allowance on its own. The projector now retains a diagnostic at that issue's
path instead of returning only the omission notice.

**The regression never crossed the MCP boundary.** The issue is about the delivered
MCP error, so `evidenceFileSourceMcp.test.ts` sends the 125,000-value fixture
through the production `inspect_analysis_view` tool and asserts the delivered error
stays `invalid_request`, keeps `details.path`, keeps bounded `content_issues`, and
fits the response budget. Both regressions fail without the bound.

* test(evidence): move the MCP delivery regression to the adapters project

Review finding. `scripts/lib/test-projects.mjs` assigns `src/application/**` to the
`services` project, so the delivery regression sat in the wrong project even though
it exercises the production MCP tool registration. Moving it to `src/server/` puts
it in `adapters`, which runs the other MCP boundary tests and plans the same way.
2026-11-06 14:53:25 +08:00

72 lines
2.7 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { analysisViewCaptureEvidence } from "../../tests/fixtures/analysisView.js";
import { createApplicationMcpHarness } from "../../tests/fixtures/applicationMcpHarness.js";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import type { Evidence } from "../domain/evidence.js";
/**
* Issue #8453 is about the delivered MCP error, not only the reader's result. On
* unmodified `main`, a malformed Evidence file with 125,000 invalid `limitations`
* values returned `resource_constraint` at the MCP response boundary with an
* original-error reference and no file path, because the reader projected every
* validator issue before the response budget applied.
*/
it("delivers a bounded file validation error through inspect_analysis_view", async () => {
const root = await createTestTempDirectory("rea-mcp-bounded-issues-");
const evidencePath = join(root, "many.json");
const source = structuredClone(analysisViewCaptureEvidence());
const document = {
...source,
limitations: Array.from({ length: 125_000 }, (_value, index) => index),
} as unknown as Evidence;
await writeFile(evidencePath, JSON.stringify(document));
const { client } = await createApplicationMcpHarness();
const result = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source: { kind: "evidence-file", path: evidencePath },
view: { kind: "summary" },
},
});
expect(result.isError).toBe(true);
const text = (result.content as readonly { type: string; text?: string }[])
.flatMap(({ type, text: value }) =>
type === "text" && value !== undefined ? [value] : [],
)
.join("");
// The typed file error and the selected path survive the response boundary.
const delivered = JSON.parse(text) as {
error?: {
code?: string;
details?: {
path?: string;
content_issues?: readonly {
path?: readonly (string | number)[];
reason?: string;
message?: string;
}[];
};
};
};
expect(delivered.error?.code).toBe("invalid_request");
expect(delivered.error?.details?.path).toBe(evidencePath);
const issues = delivered.error?.details?.content_issues ?? [];
// Bounded rather than all 125,000, and explicitly incomplete.
expect(issues.length).toBeGreaterThan(0);
expect(issues.length).toBeLessThan(1_000);
expect(issues).toContainEqual(
expect.objectContaining({
path: [],
message: expect.stringContaining("validator issues were omitted"),
}),
);
// The whole delivered response stays well inside the default MCP budget.
expect(Buffer.byteLength(text, "utf8")).toBeLessThan(1 << 20);
});