Make task schema fields explicit with null sentinels

This commit is contained in:
lesetong
2026-08-11 13:36:09 +08:00
parent 28d7e66d8a
commit 8af1c53c83
8 changed files with 270 additions and 75 deletions
+85 -1
View File
@@ -1,5 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
import { validateToolArguments } from "../node_modules/@earendil-works/pi-ai/dist/utils/validation.js";
import { TaskState } from "../src/state.ts";
import { registerTaskTools } from "../src/tools.ts";
@@ -18,7 +19,9 @@ function registeredTools(state = new TaskState()) {
}
async function execute(tool: any, params: unknown) {
return tool.execute("call", params, undefined, undefined, {});
const prepared = tool.prepareArguments?.(params) ?? params;
const validated = validateToolArguments(tool, { name: tool.name, arguments: prepared } as never);
return tool.execute("call", validated, undefined, undefined, {});
}
test("registers five focused tools without the old add_task alias", () => {
@@ -38,6 +41,8 @@ test("set_tasks schema hoists $defs to the root with #/$defs/ refs", () => {
// $defs must live at the document root: root-style refs resolve from there,
// and moonshot-flavored validators reject nested definitions (Console Go).
assert.ok(params.$defs.AddTask, "$defs hoisted to the parameter schema root");
assert.deepEqual(params.required, ["mode", "tasks", "force"]);
assert.deepEqual(params.$defs.AddTask.required, ["name", "description", "status", "parentId", "children"]);
assert.equal(itemSchema.$defs, undefined, "no $defs nested inside tasks items");
assert.equal(itemSchema.$ref, "#/$defs/AddTask");
assert.equal(params.$defs.AddTask.properties.children.items.$ref, "#/$defs/AddTask");
@@ -46,6 +51,85 @@ test("set_tasks schema hoists $defs to the root with #/$defs/ refs", () => {
assert.ok(!/"\$ref":"(?!\#)/.test(serialized), "no bare $ref values remain");
});
test("strict task schemas normalize legacy optional arguments", () => {
const { tools } = registeredTools();
const setTool = tools.get("set_tasks");
const updateTool = tools.get("update_task");
assert.deepEqual(updateTool.parameters.properties.tasks.items.required, [
"id",
"name",
"description",
"status",
"parentId",
"position",
]);
assert.deepEqual(setTool.prepareArguments({
mode: "append",
tasks: [{ name: "Parent", children: [{ name: "Child" }] }],
}), {
mode: "append",
tasks: [
{
name: "Parent",
description: null,
status: "pending",
parentId: null,
children: [
{
name: "Child",
description: null,
status: "pending",
parentId: null,
children: [],
},
],
},
],
force: false,
});
assert.deepEqual(updateTool.prepareArguments({ tasks: [{ id: 1, status: "done" }] }), {
tasks: [
{
id: 1,
name: null,
description: null,
status: "done",
parentId: -1,
position: -1,
},
],
});
assert.equal(updateTool.prepareArguments({ tasks: [{ id: 1, parentId: null }] }).tasks[0].parentId, 0);
const validated = validateToolArguments(updateTool, {
name: "update_task",
arguments: updateTool.prepareArguments({ tasks: [{ id: 1, status: "done" }] }),
} as never);
assert.equal(validated.tasks[0].parentId, -1);
assert.equal(validated.tasks[0].position, -1);
});
test("validated update sentinels preserve order and map legacy null parent to root", async () => {
const harness = registeredTools();
await execute(harness.tools.get("set_tasks"), {
mode: "append",
tasks: [
{ name: "Parent", children: [{ name: "Child" }] },
{ name: "Sibling" },
],
});
await execute(harness.tools.get("update_task"), { tasks: [{ id: 3, status: "blocked" }] });
assert.deepEqual(
harness.state.list().filter((task) => task.parentId === null).map((task) => task.id),
[1, 3],
);
await execute(harness.tools.get("update_task"), { tasks: [{ id: 2, parentId: null }] });
assert.equal(harness.state.get([2])[0]?.parentId, null);
});
test("set_tasks appends optional-description tasks and persists a complete snapshot", async () => {
const harness = registeredTools();
const result = await execute(harness.tools.get("set_tasks"), {