mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
fix(snapshot): retain schema diagnostics in file failures
Refs morluto/rea#7442 Fixes #7442 Supersedes #7875 Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
7471b07a90
commit
213fcc8ce1
4 files changed
+153
-4
No files matched your search
@@ -690,6 +690,12 @@ an exact result when the target bytes, operation, parameters, provider and
|
||||
settings match. Mutations and cursor-dependent calls are excluded from the
|
||||
cache. Snapshot files are local and use owner-only permissions.
|
||||
|
||||
Malformed snapshot headers or entries remain integrity failures. The diagnostic
|
||||
keeps the first schema issue's location and message, plus the number of further
|
||||
issues, rather than replacing it with a constant validation reason. File-selection
|
||||
and JSON-syntax failures remain distinct; internal exception objects and stacks
|
||||
are not serialized into the public error.
|
||||
|
||||
A binary CLI replay reports the current invocation's selected path while leaving
|
||||
the saved Evidence and snapshot query unchanged. See
|
||||
[caller-selected paths](../reference/mcp-contracts.md#caller-selected-paths)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { setImmediate } from "node:timers/promises";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
prepareSnapshotEntry,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
EvidenceIntegrityError,
|
||||
} from "../../domain/evidenceErrors.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { describeValidationFailure } from "../../domain/validationFailure.js";
|
||||
import type { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
|
||||
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
||||
import { readJsonObjectMembersFile, writeTextFile } from "../JsonFiles.js";
|
||||
@@ -140,7 +142,10 @@ export const writeAnalysisSnapshot = async (
|
||||
return writeTextFile(encoded, path, overwrite, resources);
|
||||
};
|
||||
|
||||
const snapshotValidationMessage = (cause: unknown): string =>
|
||||
cause instanceof TypeError
|
||||
const snapshotValidationMessage = (cause: unknown): string => {
|
||||
if (cause instanceof z.ZodError)
|
||||
return `Analysis snapshot validation failed ${describeValidationFailure(cause)}`;
|
||||
return cause instanceof TypeError
|
||||
? cause.message
|
||||
: "Analysis snapshot validation failed";
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
import { expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { analysisErrorProjectionSchema } from "../../src/contracts/errorSchemas.js";
|
||||
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"preserves selected snapshot schema reasons through compiled CLI and stdio MCP",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-snapshot-diagnostic-boundary-",
|
||||
);
|
||||
const target = join(root, "target.zip");
|
||||
const snapshot = join(root, "selected.json");
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
await writer.add("entry.txt", new TextReader("inert"), { level: 0 });
|
||||
const targetBytes = await writer.close();
|
||||
await writeFile(target, targetBytes);
|
||||
const call = await cli.sharedMcpCall!();
|
||||
for (const [document, field] of [
|
||||
[{ entries: [], workflow_entries: [] }, "target"],
|
||||
[{ entries: [{}], workflow_entries: [] }, "query_id"],
|
||||
[{ entries: [], workflow_entries: [{}] }, "query_id"],
|
||||
] as const) {
|
||||
const encoded = JSON.stringify(document);
|
||||
await writeFile(snapshot, encoded);
|
||||
const output = await cli.sharedCliRun!({
|
||||
arguments: [
|
||||
"analyze",
|
||||
target,
|
||||
"--snapshot",
|
||||
snapshot,
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
});
|
||||
expect(output.exitCode, output.stderr).toBe(1);
|
||||
const { error: description, ...projection } = z
|
||||
.record(z.string(), z.unknown())
|
||||
.parse(output.json);
|
||||
expect(description).toBe("Analysis failed");
|
||||
const cliError = analysisErrorProjectionSchema.parse(projection);
|
||||
const response = await call("open_binary", {
|
||||
path: target,
|
||||
snapshot_path: snapshot,
|
||||
});
|
||||
const mcpError = parseMcpToolError(response).error;
|
||||
for (const error of [cliError, mcpError]) {
|
||||
expect(error).toMatchObject({
|
||||
code: "evidence_integrity_mismatch",
|
||||
category: "integrity_mismatch",
|
||||
retryable: false,
|
||||
details: { reason: expect.stringContaining(`at ${field}:`) },
|
||||
});
|
||||
expect(error).not.toHaveProperty("cause");
|
||||
expect(error).not.toHaveProperty("stack");
|
||||
}
|
||||
expect(mcpError.details).toEqual(cliError.details);
|
||||
expect(await readFile(snapshot, "utf8")).toBe(encoded);
|
||||
}
|
||||
expect(
|
||||
Buffer.compare(await readFile(target), Buffer.from(targetBytes)),
|
||||
).toBe(0);
|
||||
},
|
||||
);
|
||||
@@ -1,9 +1,12 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { readAnalysisSnapshot } from "../../../src/application/binary/AnalysisSnapshotFiles.js";
|
||||
import {
|
||||
readAnalysisSnapshot,
|
||||
writeAnalysisSnapshot,
|
||||
} from "../../../src/application/binary/AnalysisSnapshotFiles.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import {
|
||||
snapshotBinding,
|
||||
@@ -15,11 +18,53 @@ import {
|
||||
} from "../../../src/domain/analysisSnapshot.fixture.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { withArtifactResourceScope } from "../../fixtures/artifactInventory.js";
|
||||
|
||||
describe.each([
|
||||
{ mode: "native", paddingBytes: 0 },
|
||||
{ mode: "streamed", paddingBytes: 9 * 1024 * 1024 },
|
||||
])("analysis snapshot input ($mode)", ({ paddingBytes }) => {
|
||||
it.each([
|
||||
{
|
||||
label: "header",
|
||||
document: { entries: [], workflow_entries: [] },
|
||||
field: "target",
|
||||
},
|
||||
{
|
||||
label: "provider entry",
|
||||
document: { entries: [{}], workflow_entries: [] },
|
||||
field: "query_id",
|
||||
},
|
||||
{
|
||||
label: "workflow entry",
|
||||
document: { entries: [], workflow_entries: [{}] },
|
||||
field: "query_id",
|
||||
},
|
||||
])(
|
||||
"retains the first $label schema diagnostic through the file wrapper",
|
||||
async ({ document, field }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-snapshot-schema-diagnostic-",
|
||||
);
|
||||
const path = join(root, "snapshot.json");
|
||||
const encoded = `${" ".repeat(paddingBytes)}${JSON.stringify(document)}`;
|
||||
await writeFile(path, encoded);
|
||||
const loaded = await readAnalysisSnapshot(path);
|
||||
if (loaded.ok) throw new Error("Expected invalid snapshot refusal");
|
||||
expect(loaded.error.cause).toMatchObject({ name: "ZodError" });
|
||||
const projected = projectAnalysisError(loaded.error);
|
||||
expect(projected).toMatchObject({
|
||||
code: "evidence_integrity_mismatch",
|
||||
category: "integrity_mismatch",
|
||||
retryable: false,
|
||||
details: { reason: expect.stringContaining(`at ${field}:`) },
|
||||
});
|
||||
expect(projected).not.toHaveProperty("cause");
|
||||
expect(projected).not.toHaveProperty("stack");
|
||||
expect(await readFile(path, "utf8")).toBe(encoded);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["[]", "null", "5", '"x"', "true"])(
|
||||
"classifies a readable %s root as invalid content, not an I/O failure",
|
||||
async (document) => {
|
||||
@@ -72,6 +117,29 @@ describe.each([
|
||||
});
|
||||
});
|
||||
|
||||
it("retains serialization schema diagnostics without creating a snapshot file", async () => {
|
||||
const root = await createTestTempDirectory("rea-snapshot-write-diagnostic-");
|
||||
const path = join(root, "snapshot.json");
|
||||
const snapshot = {
|
||||
target: snapshotTarget(ANALYSIS_SNAPSHOT_TARGET),
|
||||
binding: snapshotBinding(ANALYSIS_SNAPSHOT_PROFILE),
|
||||
entries: [],
|
||||
workflow_entries: [],
|
||||
evidence_bundle: createEvidenceBundle([]),
|
||||
};
|
||||
Reflect.deleteProperty(snapshot, "binding");
|
||||
const written = await withArtifactResourceScope((resources) =>
|
||||
writeAnalysisSnapshot(snapshot, path, false, resources),
|
||||
);
|
||||
if (written.ok) throw new Error("Expected invalid snapshot refusal");
|
||||
expect(written.error.cause).toMatchObject({ name: "ZodError" });
|
||||
expect(projectAnalysisError(written.error)).toMatchObject({
|
||||
code: "evidence_integrity_mismatch",
|
||||
details: { reason: expect.stringContaining("at binding:") },
|
||||
});
|
||||
await expect(stat(path)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("keeps file-selection failures distinct from invalid snapshot contents", async () => {
|
||||
const root = await createTestTempDirectory("rea-snapshot-selection-");
|
||||
for (const [path, reason] of [
|
||||
|
||||
Reference in new issue
Block a user