From 4d95f2b2e7a6fdc1ba9dfcbd90578f6bbc5e5e1e Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:15:11 +0800 Subject: [PATCH] fix(validation): retain unmatched union correction details Preserve actionable field paths and schema-declared variants for unknown or missing discriminators. Closes #1421. Refs #1065. --- src/domain/inputIssueProjection.ts | 19 ++++++++- tests/boundary/cli/analysisViewCli.test.ts | 45 ++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/domain/inputIssueProjection.ts b/src/domain/inputIssueProjection.ts index 25798721c9..1d5db83b65 100644 --- a/src/domain/inputIssueProjection.ts +++ b/src/domain/inputIssueProjection.ts @@ -35,7 +35,23 @@ const projectIssue = ( typeof part === "string" || typeof part === "number" ? [part] : [], ); // Branch issue paths are relative to the union; branches often agree. - if (issue.code === "invalid_union") + if (issue.code === "invalid_union") { + // Unmatched discriminators and ambiguous unions have no branch issues. + // Keep their outer failure instead of flattening it into an empty list. + if (issue.errors.length === 0) { + const expected = + "options" in issue ? issue.options?.filter(isSafeExpected) : undefined; + return [ + { + path, + reason: + valueAtPath(input, path) === undefined + ? "missing_argument" + : "invalid_value", + ...(expected === undefined ? {} : { expected }), + }, + ]; + } return uniqueIssues( issue.errors.flatMap((branch) => branch.flatMap((branchIssue) => @@ -46,6 +62,7 @@ const projectIssue = ( ), ), ); + } if (issue.code === "invalid_key") return issue.issues.flatMap((keyIssue) => projectIssue( diff --git a/tests/boundary/cli/analysisViewCli.test.ts b/tests/boundary/cli/analysisViewCli.test.ts index e6e6fb51d7..86e0d9b271 100644 --- a/tests/boundary/cli/analysisViewCli.test.ts +++ b/tests/boundary/cli/analysisViewCli.test.ts @@ -80,3 +80,48 @@ cliTest("rejects a request that omits source", async ({ cli }) => { details: { operation: "inspect-analysis-view" }, }); }); + +for (const [view, reason] of [ + [{ kind: "unknown-view" }, "invalid_value"], + [{}, "missing_argument"], +] as const) { + cliTest( + `reports correction details for ${reason} view kind`, + async ({ cli }) => { + const root = await createTestTempDirectory("rea-view-variant-"); + const input = join(root, "view.json"); + await writeFile( + input, + JSON.stringify({ + source: { + kind: "inline", + evidence: analysisViewLayoutEvidence(), + }, + view, + }), + ); + const result = await cli.run({ + arguments: ["inspect-analysis-view", input, "--json"], + environment: { + HOME: root, + XDG_CONFIG_HOME: root, + XDG_CACHE_HOME: root, + }, + }); + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { + issues: [ + { + path: ["view", "kind"], + reason, + expected: expect.arrayContaining(["summary", "page"]), + }, + ], + }, + }); + expect(JSON.stringify(result.json)).not.toContain("unknown-view"); + }, + ); +}