192 lines
6.0 KiB
TypeScript
192 lines
6.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import piTodoHerdr from "../index.ts";
|
|
import {
|
|
COMPACTION_REMINDER,
|
|
COMPACTION_REMINDER_TYPE,
|
|
formatCompactTaskTree,
|
|
hasUnfinishedTasks,
|
|
TASK_PROMPT_GUIDELINES,
|
|
} from "../src/prompt.ts";
|
|
import { TaskState } from "../src/state.ts";
|
|
|
|
test("registers the conflict-free /todo command", async () => {
|
|
type RegisteredCommand = {
|
|
description: string;
|
|
handler(args: string, ctx: unknown): Promise<void>;
|
|
};
|
|
const commands = new Map<string, RegisteredCommand>();
|
|
const pi = {
|
|
on() {},
|
|
registerTool() {},
|
|
registerCommand(name: string, command: RegisteredCommand) {
|
|
commands.set(name, command);
|
|
},
|
|
};
|
|
|
|
piTodoHerdr(pi as never);
|
|
|
|
assert.deepEqual([...commands.keys()], ["todo"]);
|
|
const todo = commands.get("todo");
|
|
assert.equal(todo?.description, "Show or hide the todo tree widget");
|
|
|
|
const notifications: string[] = [];
|
|
await todo?.handler("", {
|
|
ui: {
|
|
setWidget() {},
|
|
notify(message: string) {
|
|
notifications.push(message);
|
|
},
|
|
},
|
|
});
|
|
assert.deepEqual(notifications, ["No todos yet."]);
|
|
});
|
|
|
|
test("persists automatic clears before deletion, retries failures, and cancels on shutdown", async (context) => {
|
|
context.mock.timers.enable({ apis: ["setTimeout"] });
|
|
context.mock.method(console, "error", () => {});
|
|
const handlers = new Map<string, (event: any, ctx: any) => unknown>();
|
|
const tools = new Map<string, any>();
|
|
const entries: Array<{ customType: string; data: unknown }> = [];
|
|
let persistAttempts = 0;
|
|
const pi = {
|
|
on(name: string, handler: (event: any, ctx: any) => unknown) {
|
|
handlers.set(name, handler);
|
|
},
|
|
registerTool(tool: any) {
|
|
tools.set(tool.name, tool);
|
|
},
|
|
registerCommand() {},
|
|
appendEntry(customType: string, data: unknown) {
|
|
persistAttempts += 1;
|
|
if (persistAttempts === 1) throw new Error("disk unavailable");
|
|
entries.push({ customType, data });
|
|
},
|
|
};
|
|
const ctx = { ui: { setWidget() {} } };
|
|
piTodoHerdr(pi as never);
|
|
|
|
await tools.get("set_tasks").execute(
|
|
"set",
|
|
{ mode: "append", tasks: [{ name: "Done", status: "done" }], force: false },
|
|
undefined,
|
|
undefined,
|
|
ctx,
|
|
);
|
|
context.mock.timers.tick(10_000);
|
|
assert.equal((await tools.get("list_task").execute()).content[0].text, "1 [done] Done");
|
|
|
|
context.mock.timers.tick(10_000);
|
|
assert.equal((await tools.get("list_task").execute()).content[0].text, "No tasks.");
|
|
assert.deepEqual(entries, [{ customType: "pi-todo-herdr-state", data: { tasks: [], nextId: 1 } }]);
|
|
|
|
await tools.get("set_tasks").execute(
|
|
"set",
|
|
{ mode: "append", tasks: [{ name: "Done again", status: "done" }], force: false },
|
|
undefined,
|
|
undefined,
|
|
ctx,
|
|
);
|
|
await handlers.get("session_shutdown")?.({ reason: "reload" }, ctx);
|
|
context.mock.timers.tick(10_000);
|
|
assert.equal((await tools.get("list_task").execute()).content[0].text, "1 [done] Done again");
|
|
});
|
|
|
|
test("keeps task guidance short with planning after exploration", () => {
|
|
assert.equal(TASK_PROMPT_GUIDELINES.length, 2);
|
|
const prompt = TASK_PROMPT_GUIDELINES.join("\n");
|
|
assert.match(prompt, /work with 3\+ distinct steps/);
|
|
assert.match(prompt, /explore enough to understand the scope/);
|
|
assert.match(prompt, /then call set_tasks before implementation/);
|
|
assert.match(prompt, /otherwise skip tracking/);
|
|
assert.match(prompt, /Start one task in-progress/);
|
|
assert.match(prompt, /patch_tasks when tasks start, block, or finish/);
|
|
assert.match(prompt, /keep statuses accurate before the final response/);
|
|
assert.ok(prompt.split(/\s+/).length < 40);
|
|
});
|
|
|
|
test("uses a compact one-shot recovery reminder without embedding task data", () => {
|
|
assert.equal(
|
|
COMPACTION_REMINDER,
|
|
"Active tasks exist. Call list_task now; treat its result as authoritative.",
|
|
);
|
|
assert.equal(COMPACTION_REMINDER.split(/\s+/).length, 11);
|
|
assert.doesNotMatch(COMPACTION_REMINDER, /pending|in-progress|blocked|done/);
|
|
});
|
|
|
|
test("reminds after compaction only while unfinished tasks exist", () => {
|
|
const state = new TaskState();
|
|
assert.equal(hasUnfinishedTasks(state.list()), false);
|
|
|
|
state.add([{ name: "Pending" }]);
|
|
assert.equal(hasUnfinishedTasks(state.list()), true);
|
|
|
|
state.update([{ id: 1, status: "done" }]);
|
|
assert.equal(hasUnfinishedTasks(state.list()), false);
|
|
});
|
|
|
|
test("formats the compact authoritative tree without descriptions", () => {
|
|
const state = new TaskState();
|
|
state.add([
|
|
{
|
|
name: "Parent",
|
|
description: "Detailed context that should not appear.",
|
|
status: "in-progress",
|
|
children: [{ name: "Child" }],
|
|
},
|
|
]);
|
|
assert.equal(
|
|
formatCompactTaskTree(state.list()),
|
|
"1 [in-progress] Parent\n└─ 2 [pending] Child",
|
|
);
|
|
});
|
|
|
|
test("session_compact sends one hidden reminder only for unfinished tasks", async () => {
|
|
const handlers = new Map<string, (event: any, ctx: any) => unknown>();
|
|
const tools = new Map<string, any>();
|
|
const sent: Array<{ message: any; options: any }> = [];
|
|
const pi = {
|
|
on(name: string, handler: (event: any, ctx: any) => unknown) {
|
|
handlers.set(name, handler);
|
|
},
|
|
registerTool(tool: any) {
|
|
tools.set(tool.name, tool);
|
|
},
|
|
registerCommand() {},
|
|
sendMessage(message: any, options: any) {
|
|
sent.push({ message, options });
|
|
},
|
|
};
|
|
piTodoHerdr(pi as never);
|
|
const ctx = { ui: { setWidget() {} } };
|
|
|
|
await tools.get("set_tasks").execute(
|
|
"set",
|
|
{ mode: "append", tasks: [{ name: "Continue work", status: "in-progress" }] },
|
|
undefined,
|
|
undefined,
|
|
ctx,
|
|
);
|
|
handlers.get("session_compact")?.({}, ctx);
|
|
assert.deepEqual(sent, [
|
|
{
|
|
message: {
|
|
customType: COMPACTION_REMINDER_TYPE,
|
|
content: COMPACTION_REMINDER,
|
|
display: false,
|
|
},
|
|
options: { deliverAs: "steer" },
|
|
},
|
|
]);
|
|
|
|
await tools.get("patch_tasks").execute(
|
|
"patch",
|
|
{ tasks: [{ id: 1, status: "done" }] },
|
|
undefined,
|
|
undefined,
|
|
ctx,
|
|
);
|
|
handlers.get("session_compact")?.({}, ctx);
|
|
assert.equal(sent.length, 1);
|
|
});
|