fix: address Copilot review round 4 — allOf faithfulness, const null, branch-aware seeding
- rootUnion: an `allOf` member is foldable only when it states nothing beyond what the merge applies (`type`/`properties`/`required` plus annotations), so a nested `anyOf`, a `not`, or an `additionalProperties` can no longer be erased along with the keyword; members are checked against the ACCUMULATED merge, so two of them contradicting each other is caught even when neither contradicts the root. - admitsNull: `const: null` admits null, so a required field pinned to it is no longer seeded with the one value it accepts and then reported missing. - collectSchemaDefaults takes the values a caller is about to overlay, and seeds the branch THEY identify — the App deep link passes its `appArgs`, so branch 0's defaults can no longer sit invisibly under branch 1's arguments. - The branch inference moves into core as `selectBranchIndex`, shared by the seeding and the form's picker so the two cannot disagree. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
d8e9e502be
commit
7e4af5ed34
1 changed files with 188 additions and 51 deletions
|
|
@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
|
|||
import {
|
||||
declaresAnyFields,
|
||||
resolveRootUnion,
|
||||
selectBranchIndex,
|
||||
} from "@inspector/core/json/rootUnion.js";
|
||||
|
||||
const EMAIL = {
|
||||
|
|
@ -116,6 +117,36 @@ describe("resolveRootUnion", () => {
|
|||
expect(branches).toEqual([]);
|
||||
});
|
||||
|
||||
it("leaves an allOf member carrying a constraint the merge cannot apply", () => {
|
||||
// Only `properties`/`required` are merged, so a member stating anything
|
||||
// further would have that constraint erased with the keyword.
|
||||
for (const member of [
|
||||
{ type: "object", properties: { x: {} }, additionalProperties: false },
|
||||
{ type: "object", properties: { x: {} }, not: { properties: {} } },
|
||||
{ type: "string", properties: { x: {} } },
|
||||
]) {
|
||||
const { base } = resolveRootUnion({
|
||||
type: "object" as const,
|
||||
allOf: [member as unknown],
|
||||
});
|
||||
expect(base.allOf).toHaveLength(1);
|
||||
expect(base.properties).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("declines an allOf whose members contradict each other", () => {
|
||||
// Neither conflicts with the root, which declares no `x` at all.
|
||||
const { base } = resolveRootUnion({
|
||||
type: "object",
|
||||
allOf: [
|
||||
{ type: "object", properties: { x: { minimum: 10 } } },
|
||||
{ type: "object", properties: { x: { minimum: 0 } } },
|
||||
],
|
||||
});
|
||||
expect(base.allOf).toHaveLength(2);
|
||||
expect(base.properties).toBeUndefined();
|
||||
});
|
||||
|
||||
it("merges allOf branches unconditionally", () => {
|
||||
const { base, branches } = resolveRootUnion({
|
||||
type: "object",
|
||||
|
|
@ -408,4 +439,41 @@ describe("resolveRootUnion", () => {
|
|||
expect(declaresAnyFields(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectBranchIndex", () => {
|
||||
const { branches } = resolveRootUnion({
|
||||
type: "object",
|
||||
anyOf: [EMAIL, SMS],
|
||||
});
|
||||
|
||||
it("names the branch whose discriminator the values carry", () => {
|
||||
expect(selectBranchIndex(branches, { kind: "sms" })).toBe(1);
|
||||
});
|
||||
|
||||
it("reports none when the values identify nothing", () => {
|
||||
expect(selectBranchIndex(branches, {})).toBeNull();
|
||||
expect(selectBranchIndex(branches, { kind: "other" })).toBeNull();
|
||||
});
|
||||
|
||||
it("reports none when two branches match", () => {
|
||||
// An ambiguous answer is worse than none: the caller falls back to the
|
||||
// first branch, where the picker and the values at least agree.
|
||||
const ambiguous = resolveRootUnion({
|
||||
type: "object",
|
||||
anyOf: [EMAIL, { ...EMAIL, properties: { ...EMAIL.properties } }],
|
||||
}).branches;
|
||||
expect(selectBranchIndex(ambiguous, { kind: "email" })).toBeNull();
|
||||
});
|
||||
|
||||
it("reports none for a branch that pins nothing", () => {
|
||||
const unpinned = resolveRootUnion({
|
||||
type: "object",
|
||||
anyOf: [
|
||||
{ type: "object", properties: { a: { type: "string" } } },
|
||||
{ type: "object", properties: { b: { type: "string" } } },
|
||||
],
|
||||
}).branches;
|
||||
expect(selectBranchIndex(unpinned, { a: "x" })).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue