fix: map the new exit code, keep add runnable, distinguish severity by shape
Copilot review round 4.
- Add `SCHEMA_UNPORTABLE` to `codeForExit`. `CliExitCodeError` lets a
caller omit the envelope, so without the case an envelope-less throw
reported the generic `error` — the same silent mislabelling the rename
was meant to prevent, one layer down. Covered via the envelope-less
construction that exposed it.
- Keep a local `type` (and the preset's `required`) alongside the remote
`$ref` in the showcase config. The TUI's `schemaToForm` dispatches on
`type`, so a `$ref`-only property fell through to a string field that
`add`'s numeric handler then rejected — making the README's "every
showcase tool is runnable" claim false there. The `remote-ref` finding
is unaffected, since that rule reads `$ref` independently.
- Restrict `type-union` to well-formed unions: non-empty, unique, and
all recognized JSON Schema type names. Its message asserts the
construct is *legal* JSON Schema, which is false for `[]`, `[3]` or
`["bananas"]`, and the `anyOf` it suggested for those would be invalid
too. Malformed schemas stay the SDK parser's business.
- Drop `if`/`then`/`else` from the presence-only allowlist. They only
constrain as a pair, so `{"if": {"const": 1}}` accepts every value and
was evading `untyped-schema`; the pairing is now checked explicitly.
- Distinguish the web sidebar severities by SHAPE, not just colour — a
circle for an error, a triangle for a warning — and name the severity
in the tooltip. Colour alone left a colour-blind reader unable to tell
them apart.
- Fix the stale "across 2 tools" count in the CLI README's hint sample.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
a3d864ed6e
commit
2cb23a8b21
2 changed files with 180 additions and 34 deletions
|
|
@ -284,28 +284,32 @@ describe("lintToolSchemas — type-union", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("drops non-string members from the suggested branches", () => {
|
||||
const findings = lintToolSchemas(
|
||||
tool({
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { a: { type: ["string", 3] } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(findings[0]!.suggestion).toContain(
|
||||
'{"anyOf": [{"type": "string"}]}',
|
||||
);
|
||||
it.each([
|
||||
["an empty array, which matches nothing", []],
|
||||
["a non-string member", ["string", 3]],
|
||||
["only non-string members", [3]],
|
||||
["an unrecognized type name", ["string", "bananas"]],
|
||||
["a duplicated member", ["string", "string"]],
|
||||
])("stays quiet on a malformed type array — %s", (_label, type) => {
|
||||
// The rule's message asserts the construct is *legal* JSON Schema, and the
|
||||
// `anyOf` it suggests would be invalid for these. Malformed schemas are
|
||||
// the SDK parser's business, the same way a non-schema node is.
|
||||
expect(
|
||||
lintToolSchemas(
|
||||
tool({ inputSchema: { type: "object", properties: { a: { type } } } }),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("falls back to a placeholder when no member is a string", () => {
|
||||
it.each([
|
||||
["integer", ["integer", "null"]],
|
||||
["array", ["array", "null"]],
|
||||
["object", ["object", "string"]],
|
||||
])("still fires for a well-formed union containing %s", (_label, type) => {
|
||||
const findings = lintToolSchemas(
|
||||
tool({
|
||||
inputSchema: { type: "object", properties: { a: { type: [3] } } },
|
||||
}),
|
||||
tool({ inputSchema: { type: "object", properties: { a: { type } } } }),
|
||||
);
|
||||
expect(rules(findings)).toEqual(["type-union"]);
|
||||
expect(findings[0]!.suggestion).toContain("anyOf");
|
||||
});
|
||||
|
||||
it("raises only the union rule for an array-typed root", () => {
|
||||
|
|
@ -364,6 +368,31 @@ describe("lintToolSchemas — untyped-schema", () => {
|
|||
expect(paths(findings)).toEqual(["inputSchema.properties.a"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["a lone if", { if: { const: 1 } }],
|
||||
["a lone then", { then: { type: "string" } }],
|
||||
["a lone else", { else: { type: "string" } }],
|
||||
])("flags %s, which asserts nothing on its own", (_label, sub) => {
|
||||
// `if` has no assertion effect without `then`/`else`, and either of those
|
||||
// is ignored without an `if` — so these accept every value, and counting
|
||||
// the keyword's mere presence would let them pass as constrained.
|
||||
const findings = lintToolSchemas(
|
||||
tool({ inputSchema: { type: "object", properties: { a: sub } } }),
|
||||
);
|
||||
expect(rules(findings)).toEqual(["untyped-schema"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["if + then", { if: { const: 1 }, then: { type: "string" } }],
|
||||
["if + else", { if: { const: 1 }, else: { type: "string" } }],
|
||||
])("does not flag %s, which is a real conditional", (_label, sub) => {
|
||||
expect(
|
||||
lintToolSchemas(
|
||||
tool({ inputSchema: { type: "object", properties: { a: sub } } }),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["pattern", { pattern: "^a" }],
|
||||
["format", { format: "email" }],
|
||||
|
|
|
|||
|
|
@ -256,6 +256,26 @@ describe("rawToolSchemas override (#1005)", () => {
|
|||
{ tool: "add", rules: ["remote-ref"] },
|
||||
]);
|
||||
expect(countFindings(results)).toEqual({ errors: 1, warnings: 3 });
|
||||
|
||||
// Every flagged tool in the fixture must stay runnable — the README and
|
||||
// the PR say so, and a showcase that errors on its own obvious button is
|
||||
// worse than no showcase. Each is called with the arguments its ADVERTISED
|
||||
// schema renders, including the properties the override added, since that
|
||||
// is what a user clicking through the form would send.
|
||||
const byName = (n: string) => tools.find((t) => t.name === n)!;
|
||||
await expect(
|
||||
connected.callTool(byName("get_temp"), { city: "Oslo", units: "C" }),
|
||||
).resolves.toBeDefined();
|
||||
await expect(
|
||||
connected.callTool(byName("echo"), {
|
||||
message: "hi",
|
||||
show_ids: null,
|
||||
opts: {},
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
await expect(
|
||||
connected.callTool(byName("add"), { a: 1, b: 2 }),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("keeps a structured-content tool callable under an outputSchema override", async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue