fix(snapshot): locate entry diagnostics by collection and index

Each snapshot entry is validated alone, so its schema issue path started at
the entry. Prefix it with entries or workflow_entries and the entry index so
the reason names the entry to repair, and match the header case's issue
count without freezing the whole sentence.

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
gensokyo-devandgensokyo-dev authored and Ysopwnable committed 2026-11-14 14:06:23 +01:00
1 parent 612b330b3b
commit 4ad1eb5beb
3 files changed
+30 -12

No files matched your search

@@ -90,7 +90,23 @@ export const readAnalysisSnapshot = async (
}
};
/** Locate an entry's schema issues within its snapshot collection. */
const locateEntryFailure = (
cause: unknown,
collection: string,
index: number,
): unknown =>
cause instanceof z.ZodError
? new z.ZodError(
cause.issues.map((issue) => ({
...issue,
path: [collection, index, ...issue.path],
})),
)
: cause;
const snapshotEntryReader = (kind: "provider" | "workflow") => {
const collection = kind === "provider" ? "entries" : "workflow_entries";
let seen = false;
let entries: PreparedSnapshotEntry[] = [];
let failure: { readonly cause: unknown } | undefined;
@@ -102,8 +118,12 @@ const snapshotEntryReader = (kind: "provider" | "workflow") => {
failure = undefined;
},
accept: (value) => {
if (failure === undefined)
if (failure !== undefined) return;
try {
entries.push(prepareSnapshotEntry(value, kind));
} catch (cause: unknown) {
throw locateEntryFailure(cause, collection, entries.length);
}
},
reject: (cause) => {
failure ??= { cause };
@@ -113,9 +133,7 @@ const snapshotEntryReader = (kind: "provider" | "workflow") => {
reader,
finish: (): PreparedSnapshotEntry[] => {
if (!seen)
throw new TypeError(
`Analysis snapshot ${kind === "provider" ? "entries" : "workflow_entries"} is missing`,
);
throw new TypeError(`Analysis snapshot ${collection} is missing`);
if (failure !== undefined) throw failure.cause;
return entries;
},
@@ -24,8 +24,8 @@ cliTest(
const call = await cli.sharedMcpCall!();
for (const [document, field] of [
[{ entries: [], workflow_entries: [] }, "target"],
[{ entries: [{}], workflow_entries: [] }, "query_id"],
[{ entries: [], workflow_entries: [{}] }, "query_id"],
[{ entries: [{}], workflow_entries: [] }, "entries.0.query_id"],
[{ entries: [], workflow_entries: [{}] }, "workflow_entries.0.query_id"],
] as const) {
const encoded = JSON.stringify(document);
await writeFile(snapshot, encoded);
@@ -60,8 +60,8 @@ cliTest(
expect(error).not.toHaveProperty("cause");
expect(error).not.toHaveProperty("stack");
if (field === "target")
expect(error.details?.reason).toBe(
"Analysis snapshot validation failed at target: Invalid input: expected object, received undefined (2 more issues)",
expect(error.details?.reason).toMatch(
/ at target: .+ \(2 more issues\)$/,
);
}
// MCP adds open-session facts; the snapshot reason must match exactly.
@@ -33,12 +33,12 @@ describe.each([
{
label: "provider entry",
document: { entries: [{}], workflow_entries: [] },
field: "query_id",
field: "entries.0.query_id",
},
{
label: "workflow entry",
document: { entries: [], workflow_entries: [{}] },
field: "query_id",
field: "workflow_entries.0.query_id",
},
])(
"retains the first $label schema diagnostic through the file wrapper",
@@ -60,8 +60,8 @@ describe.each([
details: { reason: expect.stringContaining(`at ${field}:`) },
});
if (field === "target")
expect(projected.details?.reason).toBe(
"Analysis snapshot validation failed at target: Invalid input: expected object, received undefined (2 more issues)",
expect(projected.details?.reason).toMatch(
/ at target: .+ \(2 more issues\)$/,
);
expect(projected).not.toHaveProperty("cause");
expect(projected).not.toHaveProperty("stack");