1e439a05d4
- Updated todo start handling to set the requested task to in_progress while demoting all other in_progress tasks to pending. - Added task note rendering in summary output by prefixing each note line with "Note:". - Set GIT_OPTIONAL_LOCKS to 0 in git execution options and added tests for out-of-order start jumps and note summaries.
214 lines
6.9 KiB
TypeScript
214 lines
6.9 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
|
|
import { type TodoPhase, TodoWriteTool } from "@oh-my-pi/pi-coding-agent/tools";
|
|
|
|
function createSession(initialPhases: TodoPhase[] = []): ToolSession {
|
|
let phases = initialPhases;
|
|
return {
|
|
cwd: "/tmp/test",
|
|
hasUI: false,
|
|
getSessionFile: () => null,
|
|
getSessionSpawns: () => "*",
|
|
settings: Settings.isolated(),
|
|
getTodoPhases: () => phases,
|
|
setTodoPhases: next => {
|
|
phases = next;
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("TodoWriteTool auto-start behavior", () => {
|
|
it("auto-starts the first task after replace", async () => {
|
|
const tool = new TodoWriteTool(createSession());
|
|
const result = await tool.execute("call-1", {
|
|
phases: [
|
|
{
|
|
name: "Execution",
|
|
tasks: [{ content: "status" }, { content: "diagnostics" }],
|
|
},
|
|
],
|
|
});
|
|
|
|
const tasks = result.details?.phases[0]?.tasks ?? [];
|
|
expect(tasks.map(task => task.status)).toEqual(["in_progress", "pending"]);
|
|
const summary = result.content.find(part => part.type === "text");
|
|
if (!summary || summary.type !== "text") throw new Error("Expected text summary from todo_write");
|
|
expect(summary.text).toContain("Remaining items (2):");
|
|
expect(summary.text).toContain("task-1 status [in_progress] (Execution)");
|
|
expect(summary.text).toContain("task-2 diagnostics [pending] (Execution)");
|
|
});
|
|
|
|
it("auto-promotes the next pending task when current task is completed", async () => {
|
|
const tool = new TodoWriteTool(createSession());
|
|
await tool.execute("call-1", {
|
|
phases: [
|
|
{
|
|
name: "Execution",
|
|
tasks: [{ content: "status" }, { content: "diagnostics" }],
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await tool.execute("call-2", {
|
|
complete: ["task-1"],
|
|
});
|
|
|
|
const tasks = result.details?.phases[0]?.tasks ?? [];
|
|
expect(tasks.map(task => task.status)).toEqual(["completed", "in_progress"]);
|
|
const summary = result.content.find(part => part.type === "text");
|
|
if (!summary || summary.type !== "text") throw new Error("Expected text summary from todo_write");
|
|
expect(summary.text).toContain("Remaining items (1):");
|
|
expect(summary.text).toContain("task-2 diagnostics [in_progress] (Execution)");
|
|
|
|
const completedResult = await tool.execute("call-3", {
|
|
complete: ["task-2"],
|
|
});
|
|
const completedSummary = completedResult.content.find(part => part.type === "text");
|
|
if (!completedSummary || completedSummary.type !== "text") {
|
|
throw new Error("Expected text summary from todo_write");
|
|
}
|
|
expect(completedSummary.text).toContain("Remaining items: none.");
|
|
});
|
|
|
|
it("keeps only one in_progress task when replace input contains multiples", async () => {
|
|
const tool = new TodoWriteTool(createSession());
|
|
const result = await tool.execute("call-1", {
|
|
phases: [
|
|
{
|
|
name: "Execution",
|
|
tasks: [
|
|
{ content: "status", status: "in_progress" },
|
|
{ content: "diagnostics", status: "in_progress" },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const tasks = result.details?.phases[0]?.tasks ?? [];
|
|
expect(tasks.map(task => task.status)).toEqual(["in_progress", "pending"]);
|
|
});
|
|
});
|
|
|
|
describe("TodoWriteTool start behavior", () => {
|
|
it("jumps to a specific task out of order", async () => {
|
|
const tool = new TodoWriteTool(createSession());
|
|
await tool.execute("call-1", {
|
|
phases: [
|
|
{
|
|
name: "Phase A",
|
|
tasks: [{ content: "first" }, { content: "second" }, { content: "third" }],
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await tool.execute("call-2", {
|
|
start: "task-3",
|
|
});
|
|
|
|
const tasks = result.details?.phases[0]?.tasks ?? [];
|
|
expect(tasks.map(task => task.status)).toEqual(["pending", "pending", "in_progress"]);
|
|
});
|
|
|
|
it("demotes the current in_progress task when starting another", async () => {
|
|
const tool = new TodoWriteTool(createSession());
|
|
await tool.execute("call-1", {
|
|
phases: [
|
|
{ name: "A", tasks: [{ content: "a1" }, { content: "a2" }] },
|
|
{ name: "B", tasks: [{ content: "b1" }] },
|
|
],
|
|
});
|
|
|
|
// task-1 is auto-promoted; now jump to task-3 in phase B
|
|
const result = await tool.execute("call-2", {
|
|
start: "task-3",
|
|
});
|
|
|
|
const allTasks = result.details?.phases.flatMap(p => p.tasks) ?? [];
|
|
expect(allTasks.map(t => t.status)).toEqual(["pending", "pending", "in_progress"]);
|
|
});
|
|
});
|
|
|
|
describe("TodoWriteTool details field", () => {
|
|
it("preserves details through replace", async () => {
|
|
const tool = new TodoWriteTool(createSession());
|
|
const result = await tool.execute("call-1", {
|
|
phases: [
|
|
{
|
|
name: "Work",
|
|
tasks: [{ content: "Fix parser", details: "Update src/parser.ts line 42" }, { content: "Add tests" }],
|
|
},
|
|
],
|
|
});
|
|
|
|
const tasks = result.details?.phases[0]?.tasks ?? [];
|
|
expect(tasks[0].details).toBe("Update src/parser.ts line 42");
|
|
expect(tasks[1].details).toBeUndefined();
|
|
});
|
|
|
|
it("preserves details through add_tasks", async () => {
|
|
const tool = new TodoWriteTool(createSession());
|
|
await tool.execute("call-1", {
|
|
phases: [{ name: "Work", tasks: [{ content: "First" }] }],
|
|
});
|
|
|
|
const result = await tool.execute("call-2", {
|
|
add_tasks: [{ phase: "phase-1", content: "Second", details: "Check edge cases" }],
|
|
});
|
|
|
|
const tasks = result.details?.phases[0]?.tasks ?? [];
|
|
expect(tasks[1].details).toBe("Check edge cases");
|
|
});
|
|
|
|
it("appends notes via add_notes", async () => {
|
|
const tool = new TodoWriteTool(createSession());
|
|
await tool.execute("call-1", {
|
|
phases: [{ name: "Work", tasks: [{ content: "Fix bug", details: "Old details" }] }],
|
|
});
|
|
|
|
const result = await tool.execute("call-2", {
|
|
add_notes: [{ id: "task-1", notes: "New observation" }],
|
|
});
|
|
|
|
const task = result.details?.phases[0]?.tasks[0];
|
|
expect(task?.notes).toBe("New observation");
|
|
});
|
|
|
|
it("includes notes in summary output", async () => {
|
|
const tool = new TodoWriteTool(createSession());
|
|
await tool.execute("call-1", {
|
|
phases: [
|
|
{
|
|
name: "Work",
|
|
tasks: [{ content: "Fix bug" }, { content: "Add tests" }],
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await tool.execute("call-2", {
|
|
add_notes: [{ id: "task-1", notes: "Found edge case in parser" }],
|
|
});
|
|
|
|
const summary = result.content.find(part => part.type === "text");
|
|
if (!summary || summary.type !== "text") throw new Error("Expected text summary");
|
|
expect(summary.text).toContain("Note: Found edge case in parser");
|
|
});
|
|
|
|
it("includes details in summary for in_progress tasks", async () => {
|
|
const tool = new TodoWriteTool(createSession());
|
|
const result = await tool.execute("call-1", {
|
|
phases: [
|
|
{
|
|
name: "Work",
|
|
tasks: [{ content: "Fix parser", details: "Edit src/parser.ts" }],
|
|
},
|
|
],
|
|
});
|
|
|
|
const summary = result.content.find(part => part.type === "text");
|
|
if (!summary || summary.type !== "text") throw new Error("Expected text summary");
|
|
// Task is auto-promoted to in_progress, so details should appear in summary
|
|
expect(summary.text).toContain("Edit src/parser.ts");
|
|
});
|
|
});
|