* test(cli,tui): add in-process source coverage gates (#1484) CLI and TUI source had no coverage gate: CLI tests spawned the built binary out-of-process (coverage never saw clients/cli/src) and the TUI had only two smoke tests. Regressions there were invisible to CI. CLI: - Rewrite __tests__/helpers/cli-runner.ts to invoke runCli() in-process (capture process.std*.write, map thrown errors to an exit code), preserving the CliResult shape so all existing tests run unchanged and clients/cli/src is now measured under v8 instrumentation. - cli.ts: program.exitOverride() so a commander usage error throws (catchable in-process) instead of tearing down the worker; --help / --version still exit cleanly. Drop the dead, never-imported main(). - Add a thin out-of-process e2e.test.ts that spawns the built binary for the shebang / process.exit paths the in-process runner can't reach. - Add @vitest/coverage-v8, a per-file gate (90/85/80/50) excluding only src/index.ts (binary bootstrap), and pool: 'forks' for capture isolation. cli.ts lands at ~98% lines / 100% functions. TUI: - Add @vitest/coverage-v8 and an interim per-file gate over the non-React logic (logger.ts, components/tabsConfig.ts, utils/*); the Ink components, App.tsx, and hooks/ are excluded with a documented follow-up (#1501). Add tests covering the included modules (100%). Wiring + docs: - CI runs each client's test:coverage; root test:cli / test:tui (used by validate) and root test:coverage now enforce the gates. - gitignore clients/**/coverage; update AGENTS.md and client READMEs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(cli): clarify in-process runner footguns from PR #1502 review Review notes on the in-process CLI test runner (no behavior change): - Warn in runCli's docblock that --help/--version must NOT go through the in-process runner: exitOverride only re-throws non-zero exits, so exitCode 0 falls through to process.exit(0) and would kill the vitest worker. Those belong in the out-of-process e2e.test.ts layer. - Document that a timeout orphans the uncancellable invokeCli promise (its stdio subprocess stays alive; later writes hit the restored stdout) — a known, acceptable trade-off on the hang-only failure path. - Fix a stale reference to the removed main() (errors now route through index.ts). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(cli): fix remaining stale main() reference in vitest exclusion comment PR #1502 re-review caught a second stale `main()` mention (the first was fixed in 52c23e41). src/index.ts inlines the bootstrap as runCli(process.argv).then(...).catch(handleError) — there is no main(). Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: compose root test:coverage from per-client scripts Replace the brittle inline cd-chain with a composition of the existing test:cli / test:tui scripts plus a new matching test:web. Behavior is unchanged (web → cli → tui coverage gates) but there's a single source of truth for each client's coverage invocation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 lines
2.9 KiB
TypeScript
92 lines
2.9 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("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", () => {
|
|
const form = schemaToForm(
|
|
{
|
|
properties: {
|
|
// The array branch is nested under the outer `enum` guard, so a
|
|
// top-level `enum` must also be present for it to be reached; the
|
|
// options are taken from `items.enum`.
|
|
tags: {
|
|
type: "array",
|
|
enum: ["a", "b"],
|
|
items: { enum: ["a", "b"] },
|
|
},
|
|
},
|
|
},
|
|
"arrayEnum",
|
|
);
|
|
expect(form.sections[0]!.fields[0]).toMatchObject({
|
|
type: "select",
|
|
options: [
|
|
{ label: "a", value: "a" },
|
|
{ label: "b", value: "b" },
|
|
],
|
|
});
|
|
});
|
|
|
|
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" });
|
|
});
|
|
});
|