Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
Closes #1689. Typecheck cli/tui src, add a core/ format+lint gate, and gate core/json + core/client coverage. See PR #1766 for the full review history.
216 lines
6.3 KiB
TypeScript
216 lines
6.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { schemaToForm } from "../src/utils/schemaToForm.js";
|
|
|
|
describe("schemaToForm", () => {
|
|
it("returns an empty Parameters section when there is no schema", () => {
|
|
const form = schemaToForm(null, "noParams");
|
|
expect(form.title).toBe("Test Tool: noParams");
|
|
expect(form.sections).toEqual([{ title: "Parameters", fields: [] }]);
|
|
});
|
|
|
|
it("returns an empty Parameters section when properties are absent", () => {
|
|
const form = schemaToForm({}, "empty");
|
|
expect(form.sections[0]?.fields).toEqual([]);
|
|
});
|
|
|
|
it("treats a non-object property value as an empty schema instead of throwing", () => {
|
|
// A malformed server schema whose property value is null/primitive must not
|
|
// crash — the field degrades to a plain string input labelled by its key.
|
|
const form = schemaToForm(
|
|
{ properties: { bad: null, worse: 42 } },
|
|
"malformed",
|
|
);
|
|
const fields = form.sections[0]?.fields ?? [];
|
|
expect(fields).toHaveLength(2);
|
|
expect(
|
|
fields.map((f) => ({ name: f.name, type: f.type, label: f.label })),
|
|
).toEqual([
|
|
{ name: "bad", type: "string", label: "bad" },
|
|
{ name: "worse", type: "string", label: "worse" },
|
|
]);
|
|
});
|
|
|
|
it("maps each JSON Schema type to the matching ink-form field type", () => {
|
|
const form = schemaToForm(
|
|
{
|
|
properties: {
|
|
name: { type: "string", title: "Name" },
|
|
age: { type: "integer", minimum: 0, maximum: 120 },
|
|
ratio: { type: "number", minimum: 0, maximum: 1 },
|
|
active: { type: "boolean" },
|
|
mystery: { type: "object" },
|
|
},
|
|
required: ["name"],
|
|
},
|
|
"typed",
|
|
);
|
|
|
|
const fields = form.sections[0]!.fields;
|
|
const byName = Object.fromEntries(fields.map((f) => [f.name, f]));
|
|
|
|
expect(byName.name).toMatchObject({
|
|
type: "string",
|
|
label: "Name",
|
|
required: true,
|
|
});
|
|
expect(byName.age).toMatchObject({ type: "integer", min: 0, max: 120 });
|
|
expect(byName.ratio).toMatchObject({ type: "float", min: 0, max: 1 });
|
|
expect(byName.active).toMatchObject({ type: "boolean" });
|
|
// Unknown types fall back to string, and label falls back to the key.
|
|
expect(byName.mystery).toMatchObject({ type: "string", label: "mystery" });
|
|
});
|
|
|
|
it("builds a select field from an enum", () => {
|
|
const form = schemaToForm(
|
|
{ properties: { color: { type: "string", enum: ["red", "blue"] } } },
|
|
"enum",
|
|
);
|
|
expect(form.sections[0]!.fields[0]).toMatchObject({
|
|
type: "select",
|
|
options: [
|
|
{ label: "red", value: "red" },
|
|
{ label: "blue", value: "blue" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("builds a select field from an array-of-enum on items.enum alone", () => {
|
|
const form = schemaToForm(
|
|
{
|
|
properties: {
|
|
// Standard array-of-enums shape: options come from `items.enum` with
|
|
// NO top-level `enum`. The array branch keys on `items.enum` alone
|
|
// (matching the web guard), so this renders as a select.
|
|
tags: {
|
|
type: "array",
|
|
items: { enum: ["a", "b"] },
|
|
},
|
|
},
|
|
},
|
|
"arrayEnum",
|
|
);
|
|
expect(form.sections[0]!.fields[0]).toMatchObject({
|
|
type: "select",
|
|
options: [
|
|
{ label: "a", value: "a" },
|
|
{ label: "b", value: "b" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("still builds a select for an array-of-enum that also carries a top-level enum", () => {
|
|
const form = schemaToForm(
|
|
{
|
|
properties: {
|
|
tags: {
|
|
type: "array",
|
|
enum: ["a", "b"],
|
|
items: { enum: ["a", "b"] },
|
|
},
|
|
},
|
|
},
|
|
"arrayEnumRedundant",
|
|
);
|
|
expect(form.sections[0]!.fields[0]).toMatchObject({
|
|
type: "select",
|
|
options: [
|
|
{ label: "a", value: "a" },
|
|
{ label: "b", value: "b" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("uses enumNames as single-select labels while keeping raw values", () => {
|
|
const form = schemaToForm(
|
|
{
|
|
properties: {
|
|
pet: {
|
|
type: "string",
|
|
enum: ["pet-1", "pet-2"],
|
|
enumNames: ["Cats", "Dogs"],
|
|
},
|
|
},
|
|
},
|
|
"titledEnum",
|
|
);
|
|
expect(form.sections[0]!.fields[0]).toMatchObject({
|
|
type: "select",
|
|
options: [
|
|
{ label: "Cats", value: "pet-1" },
|
|
{ label: "Dogs", value: "pet-2" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("falls back to raw single-select labels when enumNames length mismatches", () => {
|
|
const form = schemaToForm(
|
|
{
|
|
properties: {
|
|
pet: {
|
|
type: "string",
|
|
enum: ["pet-1", "pet-2"],
|
|
// Only one name for two values — a wrong-length zip would
|
|
// mislabel, so the raw values are used as labels.
|
|
enumNames: ["Cats"],
|
|
},
|
|
},
|
|
},
|
|
"mismatchedEnum",
|
|
);
|
|
expect(form.sections[0]!.fields[0]).toMatchObject({
|
|
options: [
|
|
{ label: "pet-1", value: "pet-1" },
|
|
{ label: "pet-2", value: "pet-2" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("uses items.enumNames as array-of-enum labels while keeping raw values", () => {
|
|
const form = schemaToForm(
|
|
{
|
|
properties: {
|
|
pets: {
|
|
type: "array",
|
|
items: { enum: ["pet-1", "pet-2"], enumNames: ["Cats", "Dogs"] },
|
|
},
|
|
},
|
|
},
|
|
"titledArrayEnum",
|
|
);
|
|
expect(form.sections[0]!.fields[0]).toMatchObject({
|
|
type: "select",
|
|
options: [
|
|
{ label: "Cats", value: "pet-1" },
|
|
{ label: "Dogs", value: "pet-2" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("falls back to raw array-of-enum labels when items.enumNames length mismatches", () => {
|
|
const form = schemaToForm(
|
|
{
|
|
properties: {
|
|
pets: {
|
|
type: "array",
|
|
items: { enum: ["pet-1", "pet-2"], enumNames: ["Cats"] },
|
|
},
|
|
},
|
|
},
|
|
"mismatchedArrayEnum",
|
|
);
|
|
expect(form.sections[0]!.fields[0]).toMatchObject({
|
|
options: [
|
|
{ label: "pet-1", value: "pet-1" },
|
|
{ label: "pet-2", value: "pet-2" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("carries a JSON Schema default through as the field's initialValue", () => {
|
|
const form = schemaToForm(
|
|
{ properties: { greeting: { type: "string", default: "hi" } } },
|
|
"withDefault",
|
|
);
|
|
expect(form.sections[0]!.fields[0]).toMatchObject({ initialValue: "hi" });
|
|
});
|
|
});
|