fix: strip the enum null sentinel, and make the preset test cover its wiring
Copilot round 4: no new visible comments, two in the suppressed block. Both
valid.
1. The `type: [T, "null"]` encoding keeps its keywords at the top level, so a
nullable enum written that way carries the null *inside* the list:
`{ type: ["string", "null"], enum: ["envio", "recebimento", null] }`. The
collapse left it there, which broke both renderers in different ways — the
web dispatcher handed `null` to Mantine as option data, and the TUI's
all-strings check rejected the whole enum and fell back to a plain text
field. Neither is the dropdown the schema asks for.
The collapse now strips a `null` member whenever it flattens, since that
fact has already moved onto `nullable`. An enum of nothing but `null`
drops out entirely: it offers no value a dropdown could show, so the plain
widget serves the field better than an empty select. Covered in core and
end-to-end in both dispatchers.
2. The new integration test claimed in its own header to pin the registry and
config wiring, then called `createNullableFieldsTool()` directly — so a
misspelt preset case or a config naming a dead preset would have left it
green. A fair "this does not test what it says" catch. It now resolves the
checked-in `nullable-fields-http.json` through `loadConfig` →
`resolveConfig`, matching `duplicate-tool-names.test.ts`, and asserts the
resolved tool names before booting the server.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
34ba8256a4
commit
8356033d58
1 changed files with 190 additions and 26 deletions
|
|
@ -101,6 +101,30 @@ export type NormalizedNullableUnion<T extends NullableUnionSchema> = Omit<
|
|||
nullable?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Drop a `null` sentinel from an `enum`, since the collapse has already moved
|
||||
* that fact onto `nullable`.
|
||||
*
|
||||
* The `type: [T, "null"]` encoding keeps its keywords at the top level, so a
|
||||
* nullable enum written that way carries the null *inside* the list:
|
||||
* `{ type: ["string", "null"], enum: ["envio", "recebimento", null] }`. Leaving
|
||||
* it there breaks both renderers in different ways — the web dispatcher would
|
||||
* hand `null` to Mantine as option data, and the TUI's all-strings check would
|
||||
* reject the whole enum and fall back to a plain text field. Neither is the
|
||||
* dropdown the schema is asking for.
|
||||
*
|
||||
* Returns `undefined` when nothing but `null` was in the list: an enum of only
|
||||
* `null` permits no value a dropdown could offer, so the field is better served
|
||||
* by its plain widget than by an empty select.
|
||||
*/
|
||||
function stripNullFromEnum(value: unknown): unknown[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const members = value.filter((member) => member !== null);
|
||||
return members.length > 0 ? members : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the collapsed schema.
|
||||
*
|
||||
|
|
@ -115,12 +139,17 @@ function collapsed<T extends NullableUnionSchema>(
|
|||
branch: Record<string, unknown> | undefined,
|
||||
type: RenderableType,
|
||||
): NormalizedNullableUnion<T> {
|
||||
const merged = { ...schema, ...branch };
|
||||
return {
|
||||
...schema,
|
||||
...branch,
|
||||
...merged,
|
||||
type,
|
||||
anyOf: undefined,
|
||||
nullable: true,
|
||||
// `enum` is read after the spread so the branch's list wins when it has
|
||||
// one, and the null sentinel is stripped either way.
|
||||
...(merged.enum === undefined
|
||||
? {}
|
||||
: { enum: stripNullFromEnum(merged.enum) }),
|
||||
} as NormalizedNullableUnion<T>;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue