{"version":3,"file":"task-tools.test.d.ts","sourceRoot":"","sources":["../../../src/core/tools/task-tools.test.ts"],"names":[],"mappings":"","sourcesContent":["import { describe, expect, it, vi } from \"vitest\";\nimport type { Task } from \"../memory.js\";\nimport { formatTaskDetail, formatTaskList, generateTaskId, parseTasks, SESSION_TASKS_CUSTOM_TYPE } from \"../memory.js\";\nimport { SessionManager } from \"../session-manager.js\";\nimport { createTaskCreateTool, createTaskGetTool, createTaskListTool, createTaskUpdateTool } from \"./task-tools.js\";\n\ndescribe(\"task model\", () => {\n\tdescribe(\"generateTaskId\", () => {\n\t\tit(\"generates unique IDs\", () => {\n\t\t\tconst id1 = generateTaskId();\n\t\t\tconst id2 = generateTaskId();\n\t\t\texpect(id1).not.toBe(id2);\n\t\t});\n\n\t\tit(\"generates IDs with task_ prefix\", () => {\n\t\t\tconst id = generateTaskId();\n\t\t\texpect(id.startsWith(\"task_\")).toBe(true);\n\t\t});\n\t});\n\n\tdescribe(\"parseTasks\", () => {\n\t\tit(\"parses valid task entries\", () => {\n\t\t\tconst data = [\n\t\t\t\t{ id: \"task_1\", subject: \"Fix bug\", description: \"Fix the login bug\", status: \"pending\" },\n\t\t\t\t{ id: \"task_2\", subject: \"Add tests\", description: \"Add unit tests\", status: \"completed\" },\n\t\t\t];\n\t\t\tconst tasks = parseTasks(data);\n\t\t\texpect(tasks).toHaveLength(2);\n\t\t\texpect(tasks[0].id).toBe(\"task_1\");\n\t\t\texpect(tasks[0].subject).toBe(\"Fix bug\");\n\t\t\texpect(tasks[0].status).toBe(\"pending\");\n\t\t\texpect(tasks[1].status).toBe(\"completed\");\n\t\t});\n\n\t\tit(\"parses optional fields\", () => {\n\t\t\tconst data = [\n\t\t\t\t{\n\t\t\t\t\tid: \"task_1\",\n\t\t\t\t\tsubject: \"Refactor\",\n\t\t\t\t\tdescription: \"Refactor the module\",\n\t\t\t\t\tstatus: \"in_progress\",\n\t\t\t\t\tactiveForm: \"Refactoring the module\",\n\t\t\t\t\tmetadata: { priority: \"high\" },\n\t\t\t\t},\n\t\t\t];\n\t\t\tconst tasks = parseTasks(data);\n\t\t\texpect(tasks).toHaveLength(1);\n\t\t\texpect(tasks[0].activeForm).toBe(\"Refactoring the module\");\n\t\t\texpect(tasks[0].metadata).toEqual({ priority: \"high\" });\n\t\t});\n\n\t\tit(\"skips invalid entries\", () => {\n\t\t\tconst data = [\n\t\t\t\t{ id: \"task_1\", subject: \"Valid\", description: \"Valid task\", status: \"pending\" },\n\t\t\t\t{ id: 123, subject: \"Invalid id\" },\n\t\t\t\t{ subject: \"Missing id\" },\n\t\t\t\t{ id: \"task_2\", subject: \"Invalid status\", description: \"x\", status: \"invalid\" },\n\t\t\t];\n\t\t\tconst tasks = parseTasks(data);\n\t\t\texpect(tasks).toHaveLength(1);\n\t\t\texpect(tasks[0].id).toBe(\"task_1\");\n\t\t});\n\n\t\tit(\"returns empty array for non-array input\", () => {\n\t\t\texpect(parseTasks(null)).toHaveLength(0);\n\t\t\texpect(parseTasks(\"not an array\")).toHaveLength(0);\n\t\t\texpect(parseTasks(undefined)).toHaveLength(0);\n\t\t});\n\t});\n\n\tdescribe(\"formatTaskList\", () => {\n\t\tit(\"returns 'No tasks.' for empty list\", () => {\n\t\t\texpect(formatTaskList([])).toBe(\"No tasks.\");\n\t\t});\n\n\t\tit(\"groups tasks by status\", () => {\n\t\t\tconst tasks: Task[] = [\n\t\t\t\t{ id: \"task_1\", subject: \"Pending task\", description: \"\", status: \"pending\" },\n\t\t\t\t{\n\t\t\t\t\tid: \"task_2\",\n\t\t\t\t\tsubject: \"In progress task\",\n\t\t\t\t\tdescription: \"\",\n\t\t\t\t\tstatus: \"in_progress\",\n\t\t\t\t\tactiveForm: \"Working on it\",\n\t\t\t\t},\n\t\t\t\t{ id: \"task_3\", subject: \"Done task\", description: \"\", status: \"completed\" },\n\t\t\t];\n\t\t\tconst output = formatTaskList(tasks);\n\t\t\texpect(output).toContain(\"Pending (1):\");\n\t\t\texpect(output).toContain(\"In Progress (1):\");\n\t\t\texpect(output).toContain(\"Completed (1):\");\n\t\t\texpect(output).toContain(\"[task_1] Pending task\");\n\t\t\texpect(output).toContain(\"[task_2] In progress task — Working on it\");\n\t\t});\n\t});\n\n\tdescribe(\"formatTaskDetail\", () => {\n\t\tit(\"formats a task with all fields\", () => {\n\t\t\tconst task: Task = {\n\t\t\t\tid: \"task_1\",\n\t\t\t\tsubject: \"Build feature\",\n\t\t\t\tdescription: \"Build the new feature end-to-end\",\n\t\t\t\tstatus: \"in_progress\",\n\t\t\t\tactiveForm: \"Building the feature\",\n\t\t\t\tmetadata: { estimate: \"2h\" },\n\t\t\t};\n\t\t\tconst output = formatTaskDetail(task);\n\t\t\texpect(output).toContain(\"Task: Build feature\");\n\t\t\texpect(output).toContain(\"ID: task_1\");\n\t\t\texpect(output).toContain(\"Status: in_progress\");\n\t\t\texpect(output).toContain(\"Active: Building the feature\");\n\t\t\texpect(output).toContain(\"Build the new feature end-to-end\");\n\t\t});\n\n\t\tit(\"omits activeForm when not set\", () => {\n\t\t\tconst task: Task = { id: \"task_1\", subject: \"Simple\", description: \"desc\", status: \"pending\" };\n\t\t\tconst output = formatTaskDetail(task);\n\t\t\texpect(output).not.toContain(\"Active:\");\n\t\t});\n\t});\n\n\tdescribe(\"task persistence via SessionManager\", () => {\n\t\tit(\"persists and restores tasks\", () => {\n\t\t\tconst session = SessionManager.inMemory(\"/tmp/project\");\n\t\t\tconst tasks: Task[] = [{ id: \"task_1\", subject: \"Persisted task\", description: \"desc\", status: \"pending\" }];\n\t\t\tsession.appendCustomEntry(SESSION_TASKS_CUSTOM_TYPE, tasks);\n\n\t\t\tconst restored = session.getLatestSessionTasks();\n\t\t\texpect(restored).toHaveLength(1);\n\t\t\texpect(restored[0].id).toBe(\"task_1\");\n\t\t\texpect(restored[0].subject).toBe(\"Persisted task\");\n\t\t});\n\n\t\tit(\"getLatestSessionTasks returns empty array when no tasks exist\", () => {\n\t\t\tconst session = SessionManager.inMemory(\"/tmp/project\");\n\t\t\tconst tasks = session.getLatestSessionTasks();\n\t\t\texpect(tasks).toHaveLength(0);\n\t\t});\n\t});\n});\n\ndescribe(\"task_create tool\", () => {\n\tit(\"creates a task with auto-generated id and pending status\", async () => {\n\t\tlet captured: Task[] = [];\n\t\tconst getTasks = () => captured;\n\t\tconst setTasks = (tasks: Task[]) => {\n\t\t\tcaptured = tasks;\n\t\t};\n\t\tconst tool = createTaskCreateTool(getTasks, setTasks);\n\n\t\tconst result = await tool.execute(\"call_1\", {\n\t\t\tsubject: \"Implement feature X\",\n\t\t\tdescription: \"Build the feature from scratch\",\n\t\t\tactiveForm: \"Implementing feature X\",\n\t\t});\n\n\t\texpect(result.content[0]).toEqual({ type: \"text\", text: expect.stringContaining(\"Implement feature X\") });\n\t\texpect(result.content[0]).toEqual({ type: \"text\", text: expect.stringContaining(\"pending\") });\n\t\texpect(result.details?.task).toMatchObject({ subject: \"Implement feature X\", status: \"pending\" });\n\t\texpect(captured).toHaveLength(1);\n\t\texpect(captured[0].id).toMatch(/^task_/);\n\t\texpect(captured[0].subject).toBe(\"Implement feature X\");\n\t\texpect(captured[0].status).toBe(\"pending\");\n\t\texpect(captured[0].activeForm).toBe(\"Implementing feature X\");\n\t});\n\n\tit(\"creates a task without optional fields\", async () => {\n\t\tlet captured: Task[] = [];\n\t\tconst tool = createTaskCreateTool(\n\t\t\t() => captured,\n\t\t\t(t) => {\n\t\t\t\tcaptured = t;\n\t\t\t},\n\t\t);\n\n\t\tconst result = await tool.execute(\"call_2\", {\n\t\t\tsubject: \"Simple task\",\n\t\t\tdescription: \"A simple task\",\n\t\t});\n\n\t\texpect(result.content[0]).toEqual({ type: \"text\", text: expect.stringContaining(\"Simple task\") });\n\t\texpect(captured[0].activeForm).toBeUndefined();\n\t\texpect(captured[0].metadata).toBeUndefined();\n\t});\n\n\tit(\"rejects empty subject\", async () => {\n\t\tconst tool = createTaskCreateTool(\n\t\t\t() => [],\n\t\t\t() => {},\n\t\t);\n\t\tconst result = await tool.execute(\"call_3\", { subject: \"   \", description: \"\" });\n\t\texpect(result.content[0]).toEqual({ type: \"text\", text: expect.stringContaining(\"subject\") });\n\t\texpect(result.details).toBeUndefined();\n\t});\n});\n\ndescribe(\"task_list tool\", () => {\n\tit(\"lists all tasks grouped by status\", async () => {\n\t\tconst tasks: Task[] = [\n\t\t\t{ id: \"task_1\", subject: \"First\", description: \"\", status: \"pending\" },\n\t\t\t{ id: \"task_2\", subject: \"Second\", description: \"\", status: \"completed\" },\n\t\t];\n\t\tconst tool = createTaskListTool(\n\t\t\t() => tasks,\n\t\t\t() => {},\n\t\t);\n\n\t\tconst result = await tool.execute(\"call_1\", {});\n\n\t\texpect(result.content[0]).toEqual({ type: \"text\", text: expect.stringContaining(\"Pending (1)\") });\n\t\texpect(result.content[0]).toEqual({ type: \"text\", text: expect.stringContaining(\"Completed (1)\") });\n\t\texpect(result.details?.tasks).toHaveLength(2);\n\t});\n\n\tit(\"returns 'No tasks.' for empty list\", async () => {\n\t\tconst tool = createTaskListTool(\n\t\t\t() => [],\n\t\t\t() => {},\n\t\t);\n\t\tconst result = await tool.execute(\"call_2\", {});\n\t\texpect(result.content[0]).toEqual({ type: \"text\", text: \"No tasks.\" });\n\t});\n\n\tit(\"includes task IDs in output\", async () => {\n\t\tconst tasks: Task[] = [{ id: \"task_1\", subject: \"Task\", description: \"\", status: \"pending\" }];\n\t\tconst tool = createTaskListTool(\n\t\t\t() => tasks,\n\t\t\t() => {},\n\t\t);\n\t\tconst result = await tool.execute(\"call_3\", {});\n\t\texpect((result.content[0] as any).text).toContain(\"[task_1] Task\");\n\t});\n});\n\ndescribe(\"task_get tool\", () => {\n\tit(\"returns full task detail for valid ID\", async () => {\n\t\tconst tasks: Task[] = [\n\t\t\t{ id: \"task_1\", subject: \"Feature\", description: \"Build it\", status: \"in_progress\", activeForm: \"Building\" },\n\t\t];\n\t\tconst tool = createTaskGetTool(\n\t\t\t() => tasks,\n\t\t\t() => {},\n\t\t);\n\n\t\tconst result = await tool.execute(\"call_1\", { taskId: \"task_1\" });\n\n\t\texpect((result.content[0] as any).text).toContain(\"Feature\");\n\t\texpect((result.content[0] as any).text).toContain(\"task_1\");\n\t\texpect((result.content[0] as any).text).toContain(\"in_progress\");\n\t\texpect(result.details?.task).toMatchObject({ id: \"task_1\", subject: \"Feature\" });\n\t});\n\n\tit(\"returns null details for unknown ID\", async () => {\n\t\tconst tasks: Task[] = [{ id: \"task_1\", subject: \"Task\", description: \"\", status: \"pending\" }];\n\t\tconst tool = createTaskGetTool(\n\t\t\t() => tasks,\n\t\t\t() => {},\n\t\t);\n\n\t\tconst result = await tool.execute(\"call_2\", { taskId: \"unknown\" });\n\n\t\texpect((result.content[0] as any).text).toContain(\"not found\");\n\t\texpect(result.details?.task).toBeNull();\n\t});\n\n\tit(\"rejects empty taskId\", async () => {\n\t\tconst tool = createTaskGetTool(\n\t\t\t() => [],\n\t\t\t() => {},\n\t\t);\n\t\tconst result = await tool.execute(\"call_3\", { taskId: \"   \" });\n\t\texpect((result.content[0] as any).text).toContain(\"taskId\");\n\t});\n});\n\ndescribe(\"task_update tool\", () => {\n\tit(\"updates task status to in_progress\", async () => {\n\t\tconst tasks: Task[] = [{ id: \"task_1\", subject: \"Feature\", description: \"desc\", status: \"pending\" }];\n\t\tlet captured = [...tasks];\n\t\tconst tool = createTaskUpdateTool(\n\t\t\t() => captured,\n\t\t\t(t) => {\n\t\t\t\tcaptured = t;\n\t\t\t},\n\t\t);\n\n\t\tconst result = await tool.execute(\"call_1\", { taskId: \"task_1\", status: \"in_progress\" });\n\n\t\texpect((result.content[0] as any).text).toContain(\"Feature\");\n\t\texpect((result.content[0] as any).text).toContain(\"in_progress\");\n\t\texpect(captured[0].status).toBe(\"in_progress\");\n\t});\n\n\tit(\"updates multiple fields at once\", async () => {\n\t\tconst tasks: Task[] = [{ id: \"task_1\", subject: \"Old\", description: \"Old desc\", status: \"pending\" }];\n\t\tlet captured = [...tasks];\n\t\tconst tool = createTaskUpdateTool(\n\t\t\t() => captured,\n\t\t\t(t) => {\n\t\t\t\tcaptured = t;\n\t\t\t},\n\t\t);\n\n\t\tawait tool.execute(\"call_2\", {\n\t\t\ttaskId: \"task_1\",\n\t\t\tsubject: \"New subject\",\n\t\t\tdescription: \"New description\",\n\t\t\tstatus: \"completed\",\n\t\t});\n\n\t\texpect(captured[0].subject).toBe(\"New subject\");\n\t\texpect(captured[0].description).toBe(\"New description\");\n\t\texpect(captured[0].status).toBe(\"completed\");\n\t});\n\n\tit(\"updates activeForm when provided\", async () => {\n\t\tconst tasks: Task[] = [{ id: \"task_1\", subject: \"Feature\", description: \"\", status: \"pending\" }];\n\t\tlet captured = [...tasks];\n\t\tconst tool = createTaskUpdateTool(\n\t\t\t() => captured,\n\t\t\t(t) => {\n\t\t\t\tcaptured = t;\n\t\t\t},\n\t\t);\n\n\t\tawait tool.execute(\"call_3\", { taskId: \"task_1\", status: \"in_progress\", activeForm: \"Working on feature\" });\n\n\t\texpect(captured[0].activeForm).toBe(\"Working on feature\");\n\t});\n\n\tit(\"returns error for unknown taskId\", async () => {\n\t\tconst tool = createTaskUpdateTool(\n\t\t\t() => [],\n\t\t\t() => {},\n\t\t);\n\t\tconst result = await tool.execute(\"call_4\", { taskId: \"ghost\" });\n\t\texpect((result.content[0] as any).text).toContain(\"not found\");\n\t});\n\n\tit(\"rejects empty taskId\", async () => {\n\t\tconst tool = createTaskUpdateTool(\n\t\t\t() => [],\n\t\t\t() => {},\n\t\t);\n\t\tconst result = await tool.execute(\"call_5\", { taskId: \"\" });\n\t\texpect((result.content[0] as any).text).toContain(\"taskId\");\n\t});\n\n\tit(\"partial update preserves unchanged fields\", async () => {\n\t\tconst tasks: Task[] = [\n\t\t\t{ id: \"task_1\", subject: \"Original\", description: \"Original desc\", status: \"pending\", activeForm: \"Working\" },\n\t\t];\n\t\tlet captured = [...tasks];\n\t\tconst tool = createTaskUpdateTool(\n\t\t\t() => captured,\n\t\t\t(t) => {\n\t\t\t\tcaptured = t;\n\t\t\t},\n\t\t);\n\n\t\tawait tool.execute(\"call_6\", { taskId: \"task_1\", status: \"completed\" });\n\n\t\texpect(captured[0].subject).toBe(\"Original\");\n\t\texpect(captured[0].description).toBe(\"Original desc\");\n\t\texpect(captured[0].activeForm).toBe(\"Working\");\n\t\texpect(captured[0].status).toBe(\"completed\");\n\t});\n});\n\ndescribe(\"task_update emits task_update event\", () => {\n\tit(\"calls setTasks with the updated task list\", async () => {\n\t\tconst tasks: Task[] = [{ id: \"task_1\", subject: \"Task\", description: \"\", status: \"pending\" }];\n\t\tconst setTasks = vi.fn();\n\t\tconst tool = createTaskUpdateTool(() => tasks, setTasks);\n\n\t\tawait tool.execute(\"call_1\", { taskId: \"task_1\", status: \"completed\" });\n\n\t\texpect(setTasks).toHaveBeenCalledTimes(1);\n\t\tconst updated = setTasks.mock.calls[0][0] as Task[];\n\t\texpect(updated).toHaveLength(1);\n\t\texpect(updated[0].status).toBe(\"completed\");\n\t});\n});\n"]}