fix: address Copilot review round 41 — const does not demand a property, required-only branches

- `const` constrains a property that is PRESENT; it neither requires the
  property nor acts as a default. It is now supplied automatically only where
  the schema also requires the field — in `collectSchemaDefaults`,
  `applySchemaConstants` and the TUI's initial values alike — so an optional
  `dryRun: { const: true }` no longer turns a valid `{}` call into one that
  asks the server to do something. A value that IS present is still corrected
  to the constant, whatever supplied it, and the field stays read-only.
- Offerability is judged on the base+member merge rather than the raw member,
  so `anyOf: [{ required: ["email"] }, { required: ["phone"] }]` over
  root-declared properties is offered. It was declined, which left the gate
  checking the base alone and accepting `{}` — which that schema rejects.
- The TUI's branch sections keep the schema's own `required` for the seeding
  decision while still rendering every branch field non-required, which is what
  keeps a static form submittable.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-26 16:08:24 -04:00
commit b0b252a5c0

View file

@ -375,15 +375,32 @@ describe("root composition (#2123)", () => {
expect(collectSchemaDefaults(UNION)).not.toHaveProperty("phone");
});
it("seeds a const property on an ordinary schema too", () => {
it("seeds a required const property on an ordinary schema too", () => {
expect(
collectSchemaDefaults({
type: "object",
properties: { version: { type: "string", const: "1" } },
required: ["version"],
}),
).toEqual({ version: "1" });
});
it("leaves an optional const out", () => {
// `const` constrains a present value; it does not require the property or
// act as a default, so seeding it would turn a valid `{}` call into one
// that asks the server to do something.
const optional: InspectorFormSchema = {
type: "object",
properties: { dryRun: { type: "boolean", const: true } },
};
expect(collectSchemaDefaults(optional)).toEqual({});
expect(applySchemaConstants(optional, {})).toEqual({});
// …but a value that IS present must be the one the schema fixes.
expect(applySchemaConstants(optional, { dryRun: false })).toEqual({
dryRun: true,
});
});
it("prefers a const over a conflicting default", () => {
// `default` is an annotation, not a constraint, so a schema may advertise
// one its own `const` rejects — seeding it would submit an invalid value
@ -394,6 +411,7 @@ describe("root composition (#2123)", () => {
properties: {
v: { type: "string", const: "a", default: "b" },
},
required: ["v"],
}),
).toEqual({ v: "a" });
});
@ -454,6 +472,7 @@ describe("root composition (#2123)", () => {
properties: Object.fromEntries([
["__proto__", { type: "string", const: "kept" }],
]),
required: ["__proto__"],
});
expect(Object.hasOwn(seeded, "__proto__")).toBe(true);
});
@ -474,6 +493,7 @@ describe("root composition (#2123)", () => {
kind: { type: "string", const: "email" },
address: { type: "string" },
},
required: ["kind"],
},
{
type: "object",
@ -481,6 +501,7 @@ describe("root composition (#2123)", () => {
kind: { type: "string", const: "sms" },
phone: { type: "string" },
},
required: ["kind"],
},
],
},