fix: address Copilot review round 34 — structured constants, non-required const controls

- A `const` may be an object or an array, with or without a `type`, and
  `String({...})` is "[object Object]" — which no CLI argument can equal, so
  the only value such a schema accepts never matched. Supplied text is parsed
  and compared structurally through the resolver's own comparison (exported as
  `sameJsonValue` so the two cannot disagree), for both branch selection and
  the substitution.
- The TUI's one-option const control is never marked required: its option may
  legitimately be the empty string, which ink-form's required gate can never
  accept, so submission would not even reach `decodeFormValues`. The value is
  fixed by the schema, and `missingRequiredFields` still validates the call.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-26 13:39:16 -04:00
commit ba91db97e2
2 changed files with 79 additions and 3 deletions

View file

@ -503,6 +503,24 @@ describe("schemaToForm", () => {
});
});
it("never marks a const control required", () => {
// Its one option may be the empty string, which ink-form's required gate
// can never accept — the call would not even reach `decodeFormValues`.
const form = schemaToForm(
{
type: "object",
properties: { kind: { type: "string", const: "" } },
required: ["kind"],
},
"empty_const",
);
expect(form.sections[0]!.fields[0]).toMatchObject({
name: "kind",
type: "select",
required: false,
});
});
it("renders a const outside a union the same way", () => {
const form = schemaToForm(
{

View file

@ -357,6 +357,12 @@ function buildFields(schema: JsonSchemaObject): FormField[] {
fields.push({
type: "select",
...baseField,
// Never required: the one option may legitimately be the empty string,
// which ink-form's required gate can never accept — submission would
// not even reach `decodeFormValues`, which reapplies the constant. The
// value is fixed by the schema, and `missingRequiredFields` still
// validates the decoded call.
required: false,
initialValue: String(pinned),
options: [{ label: String(pinned), value: String(pinned) }],
} as FormField);