fix: address Copilot review round 11 — recurse constants into nested objects

applySchemaConstants only corrected top-level pinned fields, but the deep
link's overlay replaces a nested object wholesale rather than merging into it —
so `{ config: { kind: "sms" } }` slipped past a nested read-only
`kind: { const: "email" }` the form was displaying. It now recurses into
object-valued properties, resolving each one's own root union the same way.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-26 04:36:13 -04:00
commit a69cb58451
2 changed files with 49 additions and 8 deletions

View file

@ -445,6 +445,27 @@ describe("root composition (#2123)", () => {
}); });
}); });
it("re-applies a nested object's constants", () => {
// The overlay replaces the whole nested object rather than merging into it,
// so a link naming `{ config: { kind: "sms" } }` would otherwise slip past
// the pinned `kind` the nested form displays.
const schema: InspectorFormSchema = {
type: "object",
properties: {
config: {
type: "object",
properties: {
kind: { type: "string", const: "email" },
to: { type: "string" },
},
},
},
};
expect(
applySchemaConstants(schema, { config: { kind: "sms", to: "a@b.c" } }),
).toEqual({ config: { kind: "email", to: "a@b.c" } });
});
it("leaves values alone when nothing is pinned", () => { it("leaves values alone when nothing is pinned", () => {
const values = { a: 1 }; const values = { a: 1 };
expect( expect(

View file

@ -194,14 +194,34 @@ export function applySchemaConstants(
const { base, branches } = resolveRootUnion(schema); const { base, branches } = resolveRootUnion(schema);
const selected = selectBranchIndex(branches, values) ?? 0; const selected = selectBranchIndex(branches, values) ?? 0;
const properties = (branches[selected]?.schema ?? base).properties ?? {}; const properties = (branches[selected]?.schema ?? base).properties ?? {};
const pinned = Object.entries(properties).filter(
([, fieldSchema]) => fieldSchema.const !== undefined, const corrections: [string, unknown][] = [];
); for (const [name, rawSchema] of Object.entries(properties)) {
if (pinned.length === 0) return values; const fieldSchema = normalizeNullableUnion(rawSchema);
return Object.fromEntries([ if (fieldSchema.const !== undefined) {
...Object.entries(values), corrections.push([name, fieldSchema.const]);
...pinned.map(([name, fieldSchema]) => [name, fieldSchema.const]), continue;
]); }
// Recurse: a nested object renders its own read-only fields, and the
// overlay replaces the whole object rather than merging into it — so a
// link naming `{ config: { kind: "sms" } }` would otherwise slip a value
// past the pinned `kind` the nested form is displaying.
const nested = values[name];
if (
typeof nested === "object" &&
nested !== null &&
!Array.isArray(nested)
) {
const corrected = applySchemaConstants(
fieldSchema,
nested as Record<string, unknown>,
);
if (corrected !== nested) corrections.push([name, corrected]);
}
}
if (corrections.length === 0) return values;
return Object.fromEntries([...Object.entries(values), ...corrections]);
} }
/** /**