Harden task state and terminal rendering

This commit is contained in:
jb
2026-08-26 15:13:19 -04:00
parent 6826854b66
commit ae0ca47236
10 changed files with 452 additions and 48 deletions
+66
View File
@@ -51,6 +51,72 @@ test("set_tasks schema hoists $defs to the root with #/$defs/ refs", () => {
assert.ok(!/"\$ref":"(?!\#)/.test(serialized), "no bare $ref values remain");
});
test("task schemas publish text and batch limits", () => {
const { tools } = registeredTools();
const setParams = tools.get("set_tasks").parameters;
const addTask = setParams.$defs.AddTask;
const updateTask = tools.get("update_task").parameters.properties.tasks.items;
assert.equal(setParams.properties.tasks.maxItems, 50);
assert.equal(addTask.properties.children.maxItems, 50);
assert.equal(addTask.properties.name.maxLength, 200);
assert.match(addTask.properties.name.pattern, /061C/i);
assert.equal(addTask.properties.description.anyOf[0].maxLength, 4_000);
assert.match(addTask.properties.description.anyOf[0].pattern, /061C/i);
assert.equal(tools.get("update_task").parameters.properties.tasks.maxItems, 50);
assert.equal(updateTask.properties.name.anyOf[0].maxLength, 200);
assert.equal(updateTask.properties.description.anyOf[0].maxLength, 4_000);
assert.equal(tools.get("rm_task").parameters.properties.ids.maxItems, 50);
assert.equal(tools.get("get_task").parameters.properties.ids.maxItems, 50);
const setTool = tools.get("set_tasks");
const validUnicode = setTool.prepareArguments({
mode: "append",
tasks: [{ name: "😀".repeat(200), description: "line one\nline two" }],
});
assert.doesNotThrow(() =>
validateToolArguments(setTool, { name: setTool.name, arguments: validUnicode } as never),
);
const unsafeText = setTool.prepareArguments({ mode: "append", tasks: [{ name: "unsafe\u061cname" }] });
assert.throws(
() => validateToolArguments(setTool, { name: setTool.name, arguments: unsafeText } as never),
/validation failed/i,
);
});
test("set_tasks preparation bounds hostile recursive input before validation", () => {
const { tools } = registeredTools();
const setTool = tools.get("set_tasks");
let nested: Record<string, unknown> = { name: "Leaf" };
for (let depth = 0; depth < 10_000; depth += 1) nested = { name: `Level ${depth}`, children: [nested] };
const prepared = setTool.prepareArguments({ mode: "append", tasks: [nested] });
assert.throws(
() => validateToolArguments(setTool, { name: setTool.name, arguments: prepared } as never),
/validation failed/i,
);
const oversized = setTool.prepareArguments({
mode: "append",
tasks: Array.from({ length: 1_000 }, (_, index) => ({ name: `Task ${index}` })),
});
assert.equal(oversized.tasks.length, 51);
assert.throws(
() => validateToolArguments(setTool, { name: setTool.name, arguments: oversized } as never),
/validation failed/i,
);
const updateTool = tools.get("update_task");
const oversizedUpdate = updateTool.prepareArguments({
tasks: Array.from({ length: 1_000 }, (_, index) => ({ id: index + 1 })),
});
assert.equal(oversizedUpdate.tasks.length, 51);
assert.throws(
() => validateToolArguments(updateTool, { name: updateTool.name, arguments: oversizedUpdate } as never),
/validation failed/i,
);
});
test("strict task schemas normalize legacy optional arguments", () => {
const { tools } = registeredTools();
const setTool = tools.get("set_tasks");