fix: address Copilot review round 3 — conjunctive keywords, unflattenable allOf, branch inference
- rootUnion: a branch restating a constraint the root states differently is declined (keywords at one level are conjunctive and no intersection is computed here), while annotations may disagree freely. An `allOf` with a member it cannot fold in — JSON Schema's boolean form, or a $ref whose constraints are unknown rather than absent — is left intact instead of merged-and-stripped, which would turn an unsatisfiable schema into a fillable form. - rootUnion: `declaresAnyFields` counts the raw composition members, so `hasInputFields` reports fields for a union the resolver declines and an App tool is never auto-invoked with empty arguments. - SchemaForm: the picker opens on the branch the supplied values identify (an App deep link overlays its args, so they can name another branch), and a branch switch no longer carries a value the outgoing branch declared — the branches may type the same name differently. - TUI: a base property some branch redeclares is offered in EVERY branch's section under that branch's inherited or specialized declaration, so a branch that does not redeclare it can still supply a required root argument. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
84cd0c10f0
commit
1a271e4931
1 changed files with 442 additions and 69 deletions
|
|
@ -1,5 +1,8 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { resolveRootUnion } from "@inspector/core/json/rootUnion.js";
|
||||
import {
|
||||
declaresAnyFields,
|
||||
resolveRootUnion,
|
||||
} from "@inspector/core/json/rootUnion.js";
|
||||
|
||||
const EMAIL = {
|
||||
type: "object",
|
||||
|
|
@ -76,6 +79,43 @@ describe("resolveRootUnion", () => {
|
|||
expect(Object.keys(base.properties ?? {})).toEqual(["note"]);
|
||||
});
|
||||
|
||||
it("leaves an unsatisfiable allOf alone rather than dropping it", () => {
|
||||
// `allOf: [false, …]` admits nothing. Treating the boolean member as a
|
||||
// no-op and stripping the keyword would render a fillable form for a schema
|
||||
// that can never be satisfied.
|
||||
const schema = {
|
||||
type: "object" as const,
|
||||
allOf: [
|
||||
false as unknown,
|
||||
{ type: "object", properties: { x: { type: "string" } } },
|
||||
],
|
||||
};
|
||||
const { base, branches } = resolveRootUnion(schema);
|
||||
expect(branches).toEqual([]);
|
||||
expect(base.properties).toBeUndefined();
|
||||
expect(base.allOf).toBe(schema.allOf);
|
||||
});
|
||||
|
||||
it("leaves an allOf carrying a $ref alone", () => {
|
||||
// The referent is not resolved here, so its constraints are unknown rather
|
||||
// than absent.
|
||||
const { base } = resolveRootUnion({
|
||||
type: "object",
|
||||
allOf: [{ $ref: "#/$defs/Thing" }],
|
||||
});
|
||||
expect(base.allOf).toHaveLength(1);
|
||||
expect(base.properties).toBeUndefined();
|
||||
});
|
||||
|
||||
it("declines a union when the allOf beneath it could not be flattened", () => {
|
||||
const { branches } = resolveRootUnion({
|
||||
type: "object",
|
||||
allOf: [{ $ref: "#/$defs/Thing" }],
|
||||
anyOf: [EMAIL, SMS],
|
||||
});
|
||||
expect(branches).toEqual([]);
|
||||
});
|
||||
|
||||
it("merges allOf branches unconditionally", () => {
|
||||
const { base, branches } = resolveRootUnion({
|
||||
type: "object",
|
||||
|
|
@ -128,6 +168,40 @@ describe("resolveRootUnion", () => {
|
|||
expect(branches[0].declaredFields).toEqual(["id"]);
|
||||
});
|
||||
|
||||
it("declines a union whose branch restates a constraint differently", () => {
|
||||
// Both apply, so root `minimum: 10` under branch `minimum: 0` is still 10.
|
||||
// Rendering either side would accept a value the schema rejects.
|
||||
const { branches } = resolveRootUnion({
|
||||
type: "object",
|
||||
properties: { x: { type: "number", minimum: 10 } },
|
||||
anyOf: [
|
||||
{ type: "object", properties: { x: { minimum: 0 } } },
|
||||
{ type: "object", properties: { other: { type: "string" } } },
|
||||
],
|
||||
});
|
||||
expect(branches).toEqual([]);
|
||||
});
|
||||
|
||||
it("tolerates a branch disagreeing only about annotations", () => {
|
||||
const { branches } = resolveRootUnion({
|
||||
type: "object",
|
||||
properties: { x: { type: "number", description: "root" } },
|
||||
anyOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: { x: { description: "branch", maximum: 3 } },
|
||||
},
|
||||
{ type: "object", properties: { other: { type: "string" } } },
|
||||
],
|
||||
});
|
||||
expect(branches).toHaveLength(2);
|
||||
expect(branches[0].schema.properties?.x).toEqual({
|
||||
type: "number",
|
||||
description: "branch",
|
||||
maximum: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it("declines a union whose branch contradicts the root's type for a field", () => {
|
||||
// `string` under a base `number` describes a value that cannot exist, so
|
||||
// flattening it would render one type and accept what the schema rejects.
|
||||
|
|
@ -280,4 +354,31 @@ describe("resolveRootUnion", () => {
|
|||
expect(Object.keys(base.properties ?? {})).toEqual(["a"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("declaresAnyFields", () => {
|
||||
it("sees fields on a union the resolver declines", () => {
|
||||
// A declined union still HAS fields — reporting none would auto-invoke an
|
||||
// App tool with `{}` instead of asking for them.
|
||||
const schema = {
|
||||
type: "object" as const,
|
||||
anyOf: [EMAIL, { $ref: "#/$defs/SMS" }],
|
||||
};
|
||||
expect(resolveRootUnion(schema).branches).toEqual([]);
|
||||
expect(declaresAnyFields(schema)).toBe(true);
|
||||
});
|
||||
|
||||
it("sees fields nested a level down", () => {
|
||||
expect(
|
||||
declaresAnyFields({
|
||||
type: "object",
|
||||
allOf: [{ type: "object", anyOf: [EMAIL, SMS] }],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("reports none for a bare object schema", () => {
|
||||
expect(declaresAnyFields({ type: "object" })).toBe(false);
|
||||
expect(declaresAnyFields(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue