diff --git a/src/core/tools/__tests__/updateTodoListTool.spec.ts b/src/core/tools/__tests__/updateTodoListTool.spec.ts new file mode 100644 index 00000000000..0b7e8105724 --- /dev/null +++ b/src/core/tools/__tests__/updateTodoListTool.spec.ts @@ -0,0 +1,243 @@ +import { describe, it, expect, beforeEach, vi } from "vitest" +import { parseMarkdownChecklist } from "../updateTodoListTool" +import { TodoItem } from "@roo-code/types" + +describe("parseMarkdownChecklist", () => { + describe("standard checkbox format (without dash prefix)", () => { + it("should parse pending tasks", () => { + const md = `[ ] Task 1 +[ ] Task 2` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(2) + expect(result[0].content).toBe("Task 1") + expect(result[0].status).toBe("pending") + expect(result[1].content).toBe("Task 2") + expect(result[1].status).toBe("pending") + }) + + it("should parse completed tasks with lowercase x", () => { + const md = `[x] Completed task 1 +[x] Completed task 2` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(2) + expect(result[0].content).toBe("Completed task 1") + expect(result[0].status).toBe("completed") + expect(result[1].content).toBe("Completed task 2") + expect(result[1].status).toBe("completed") + }) + + it("should parse completed tasks with uppercase X", () => { + const md = `[X] Completed task 1 +[X] Completed task 2` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(2) + expect(result[0].content).toBe("Completed task 1") + expect(result[0].status).toBe("completed") + expect(result[1].content).toBe("Completed task 2") + expect(result[1].status).toBe("completed") + }) + + it("should parse in-progress tasks with dash", () => { + const md = `[-] In progress task 1 +[-] In progress task 2` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(2) + expect(result[0].content).toBe("In progress task 1") + expect(result[0].status).toBe("in_progress") + expect(result[1].content).toBe("In progress task 2") + expect(result[1].status).toBe("in_progress") + }) + + it("should parse in-progress tasks with tilde", () => { + const md = `[~] In progress task 1 +[~] In progress task 2` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(2) + expect(result[0].content).toBe("In progress task 1") + expect(result[0].status).toBe("in_progress") + expect(result[1].content).toBe("In progress task 2") + expect(result[1].status).toBe("in_progress") + }) + }) + + describe("dash-prefixed checkbox format", () => { + it("should parse pending tasks with dash prefix", () => { + const md = `- [ ] Task 1 +- [ ] Task 2` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(2) + expect(result[0].content).toBe("Task 1") + expect(result[0].status).toBe("pending") + expect(result[1].content).toBe("Task 2") + expect(result[1].status).toBe("pending") + }) + + it("should parse completed tasks with dash prefix and lowercase x", () => { + const md = `- [x] Completed task 1 +- [x] Completed task 2` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(2) + expect(result[0].content).toBe("Completed task 1") + expect(result[0].status).toBe("completed") + expect(result[1].content).toBe("Completed task 2") + expect(result[1].status).toBe("completed") + }) + + it("should parse completed tasks with dash prefix and uppercase X", () => { + const md = `- [X] Completed task 1 +- [X] Completed task 2` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(2) + expect(result[0].content).toBe("Completed task 1") + expect(result[0].status).toBe("completed") + expect(result[1].content).toBe("Completed task 2") + expect(result[1].status).toBe("completed") + }) + + it("should parse in-progress tasks with dash prefix and dash marker", () => { + const md = `- [-] In progress task 1 +- [-] In progress task 2` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(2) + expect(result[0].content).toBe("In progress task 1") + expect(result[0].status).toBe("in_progress") + expect(result[1].content).toBe("In progress task 2") + expect(result[1].status).toBe("in_progress") + }) + + it("should parse in-progress tasks with dash prefix and tilde marker", () => { + const md = `- [~] In progress task 1 +- [~] In progress task 2` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(2) + expect(result[0].content).toBe("In progress task 1") + expect(result[0].status).toBe("in_progress") + expect(result[1].content).toBe("In progress task 2") + expect(result[1].status).toBe("in_progress") + }) + }) + + describe("mixed formats", () => { + it("should parse mixed formats correctly", () => { + const md = `[ ] Task without dash +- [ ] Task with dash +[x] Completed without dash +- [X] Completed with dash +[-] In progress without dash +- [~] In progress with dash` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(6) + + expect(result[0].content).toBe("Task without dash") + expect(result[0].status).toBe("pending") + + expect(result[1].content).toBe("Task with dash") + expect(result[1].status).toBe("pending") + + expect(result[2].content).toBe("Completed without dash") + expect(result[2].status).toBe("completed") + + expect(result[3].content).toBe("Completed with dash") + expect(result[3].status).toBe("completed") + + expect(result[4].content).toBe("In progress without dash") + expect(result[4].status).toBe("in_progress") + + expect(result[5].content).toBe("In progress with dash") + expect(result[5].status).toBe("in_progress") + }) + }) + + describe("edge cases", () => { + it("should handle empty strings", () => { + const result = parseMarkdownChecklist("") + expect(result).toEqual([]) + }) + + it("should handle non-string input", () => { + const result = parseMarkdownChecklist(null as any) + expect(result).toEqual([]) + }) + + it("should handle undefined input", () => { + const result = parseMarkdownChecklist(undefined as any) + expect(result).toEqual([]) + }) + + it("should ignore non-checklist lines", () => { + const md = `This is not a checklist +[ ] Valid task +Just some text +- Not a checklist item +- [x] Valid completed task +[not valid] Invalid format` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(2) + expect(result[0].content).toBe("Valid task") + expect(result[0].status).toBe("pending") + expect(result[1].content).toBe("Valid completed task") + expect(result[1].status).toBe("completed") + }) + + it("should handle extra spaces", () => { + const md = ` [ ] Task with spaces +- [ ] Task with dash and spaces + [x] Completed with spaces +- [X] Completed with dash and spaces` + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(4) + expect(result[0].content).toBe("Task with spaces") + expect(result[1].content).toBe("Task with dash and spaces") + expect(result[2].content).toBe("Completed with spaces") + expect(result[3].content).toBe("Completed with dash and spaces") + }) + + it("should handle Windows line endings", () => { + const md = "[ ] Task 1\r\n- [x] Task 2\r\n[-] Task 3" + const result = parseMarkdownChecklist(md) + expect(result).toHaveLength(3) + expect(result[0].content).toBe("Task 1") + expect(result[0].status).toBe("pending") + expect(result[1].content).toBe("Task 2") + expect(result[1].status).toBe("completed") + expect(result[2].content).toBe("Task 3") + expect(result[2].status).toBe("in_progress") + }) + }) + + describe("ID generation", () => { + it("should generate consistent IDs for the same content and status", () => { + const md1 = `[ ] Task 1 +[x] Task 2` + const md2 = `[ ] Task 1 +[x] Task 2` + const result1 = parseMarkdownChecklist(md1) + const result2 = parseMarkdownChecklist(md2) + + expect(result1[0].id).toBe(result2[0].id) + expect(result1[1].id).toBe(result2[1].id) + }) + + it("should generate different IDs for different content", () => { + const md = `[ ] Task 1 +[ ] Task 2` + const result = parseMarkdownChecklist(md) + expect(result[0].id).not.toBe(result[1].id) + }) + + it("should generate different IDs for same content but different status", () => { + const md = `[ ] Task 1 +[x] Task 1` + const result = parseMarkdownChecklist(md) + expect(result[0].id).not.toBe(result[1].id) + }) + + it("should generate same IDs regardless of dash prefix", () => { + const md1 = `[ ] Task 1` + const md2 = `- [ ] Task 1` + const result1 = parseMarkdownChecklist(md1) + const result2 = parseMarkdownChecklist(md2) + expect(result1[0].id).toBe(result2[0].id) + }) + }) +}) diff --git a/src/core/tools/updateTodoListTool.ts b/src/core/tools/updateTodoListTool.ts index de96c3cc765..fcd41914a88 100644 --- a/src/core/tools/updateTodoListTool.ts +++ b/src/core/tools/updateTodoListTool.ts @@ -108,7 +108,8 @@ export function parseMarkdownChecklist(md: string): TodoItem[] { .filter(Boolean) const todos: TodoItem[] = [] for (const line of lines) { - const match = line.match(/^\[\s*([ xX\-~])\s*\]\s+(.+)$/) + // Support both "[ ] Task" and "- [ ] Task" formats + const match = line.match(/^(?:-\s*)?\[\s*([ xX\-~])\s*\]\s+(.+)$/) if (!match) continue let status: TodoStatus = "pending" if (match[1] === "x" || match[1] === "X") status = "completed"