/// import type { Task } from "../../features/claude-tasks/types"; import { syncTaskToTodo, syncAllTasksToTodos, syncTaskTodoUpdate, type TodoInfo, } from "./todo-sync"; describe("syncTaskToTodo", () => { it("converts pending task to pending todo", () => { // given const task: Task = { id: "T-123", subject: "Fix bug", description: "Fix critical bug", status: "pending", blocks: [], blockedBy: [], }; // when const result = syncTaskToTodo(task); // then expect(result).toEqual({ id: "T-123", content: "Fix bug", status: "pending", priority: "medium", }); }); it("converts in_progress task to in_progress todo", () => { // given const task: Task = { id: "T-456", subject: "Implement feature", description: "Add new feature", status: "in_progress", blocks: [], blockedBy: [], }; // when const result = syncTaskToTodo(task); // then expect(result?.status).toBe("in_progress"); expect(result?.content).toBe("Implement feature"); }); it("converts completed task to completed todo", () => { // given const task: Task = { id: "T-789", subject: "Review PR", description: "Review pull request", status: "completed", blocks: [], blockedBy: [], }; // when const result = syncTaskToTodo(task); // then expect(result?.status).toBe("completed"); }); it("returns null for deleted task", () => { // given const task: Task = { id: "T-del", subject: "Deleted task", description: "This task is deleted", status: "deleted", blocks: [], blockedBy: [], }; // when const result = syncTaskToTodo(task); // then expect(result).toBeNull(); }); it("extracts priority from metadata", () => { // given const task: Task = { id: "T-high", subject: "Critical task", description: "High priority task", status: "pending", blocks: [], blockedBy: [], metadata: { priority: "high" }, }; // when const result = syncTaskToTodo(task); // then expect(result?.priority).toBe("high"); }); it("handles medium priority", () => { // given const task: Task = { id: "T-med", subject: "Medium task", description: "Medium priority", status: "pending", blocks: [], blockedBy: [], metadata: { priority: "medium" }, }; // when const result = syncTaskToTodo(task); // then expect(result?.priority).toBe("medium"); }); it("handles low priority", () => { // given const task: Task = { id: "T-low", subject: "Low task", description: "Low priority", status: "pending", blocks: [], blockedBy: [], metadata: { priority: "low" }, }; // when const result = syncTaskToTodo(task); // then expect(result?.priority).toBe("low"); }); it("ignores invalid priority values", () => { // given const task: Task = { id: "T-invalid", subject: "Invalid priority", description: "Invalid priority value", status: "pending", blocks: [], blockedBy: [], metadata: { priority: "urgent" }, }; // when const result = syncTaskToTodo(task); // then expect(result?.priority).toBe("medium"); }); it("handles missing metadata", () => { // given const task: Task = { id: "T-no-meta", subject: "No metadata", description: "Task without metadata", status: "pending", blocks: [], blockedBy: [], }; // when const result = syncTaskToTodo(task); // then expect(result?.priority).toBe("medium"); }); it("uses subject as todo content", () => { // given const task: Task = { id: "T-content", subject: "This is the subject", description: "This is the description", status: "pending", blocks: [], blockedBy: [], }; // when const result = syncTaskToTodo(task); // then expect(result?.content).toBe("This is the subject"); }); }); describe("syncTaskTodoUpdate", () => { let mockCtx: any; beforeEach(() => { mockCtx = { client: { session: { todo: vi.fn(), }, }, }; }); it("writes updated todo and preserves existing items", async () => { // given const task: Task = { id: "T-1", subject: "Updated task", description: "", status: "in_progress", blocks: [], blockedBy: [], }; const currentTodos: TodoInfo[] = [ { id: "T-1", content: "Old task", status: "pending" }, { id: "T-2", content: "Keep task", status: "pending" }, ]; mockCtx.client.session.todo.mockResolvedValue({ data: currentTodos }); let called = false; const writer = async (input: { sessionID: string; todos: TodoInfo[] }) => { called = true; expect(input.sessionID).toBe("session-1"); expect(input.todos.length).toBe(2); expect( input.todos.find((todo: TodoInfo) => todo.id === "T-1")?.content, ).toBe("Updated task"); expect(input.todos.some((todo: TodoInfo) => todo.id === "T-2")).toBe( true, ); }; // when await syncTaskTodoUpdate(mockCtx, task, "session-1", writer); // then expect(called).toBe(true); }); it("removes deleted task from todos", async () => { // given const task: Task = { id: "T-1", subject: "Deleted task", description: "", status: "deleted", blocks: [], blockedBy: [], }; const currentTodos: TodoInfo[] = [ { id: "T-1", content: "Old task", status: "pending" }, { id: "T-2", content: "Keep task", status: "pending" }, ]; mockCtx.client.session.todo.mockResolvedValue(currentTodos); let called = false; const writer = async (input: { sessionID: string; todos: TodoInfo[] }) => { called = true; expect(input.todos.length).toBe(1); expect(input.todos.some((todo: TodoInfo) => todo.id === "T-1")).toBe( false, ); expect(input.todos.some((todo: TodoInfo) => todo.id === "T-2")).toBe( true, ); }; // when await syncTaskTodoUpdate(mockCtx, task, "session-1", writer); // then expect(called).toBe(true); }); }); describe("syncAllTasksToTodos", () => { let mockCtx: any; beforeEach(() => { mockCtx = { client: { session: { todo: vi.fn(), }, }, }; }); it("fetches current todos from OpenCode", async () => { // given const tasks: Task[] = [ { id: "T-1", subject: "Task 1", description: "Description 1", status: "pending", blocks: [], blockedBy: [], }, ]; const currentTodos: TodoInfo[] = [ { id: "T-existing", content: "Existing todo", status: "pending", }, ]; mockCtx.client.session.todo.mockResolvedValue(currentTodos); // when await syncAllTasksToTodos(mockCtx, tasks, "session-1"); // then expect(mockCtx.client.session.todo).toHaveBeenCalledWith({ path: { id: "session-1" }, }); }); it("handles API response with data property", async () => { // given const tasks: Task[] = []; const currentTodos: TodoInfo[] = [ { id: "T-1", content: "Todo 1", status: "pending", }, ]; mockCtx.client.session.todo.mockResolvedValue({ data: currentTodos, }); // when await syncAllTasksToTodos(mockCtx, tasks, "session-1"); // then expect(mockCtx.client.session.todo).toHaveBeenCalled(); }); it("gracefully handles fetch failure", async () => { // given const tasks: Task[] = [ { id: "T-1", subject: "Task 1", description: "Description 1", status: "pending", blocks: [], blockedBy: [], }, ]; mockCtx.client.session.todo.mockRejectedValue(new Error("API error")); // when const result = await syncAllTasksToTodos(mockCtx, tasks, "session-1"); // then expect(result).toBeUndefined(); }); it("converts multiple tasks to todos", async () => { // given const tasks: Task[] = [ { id: "T-1", subject: "Task 1", description: "Description 1", status: "pending", blocks: [], blockedBy: [], metadata: { priority: "high" }, }, { id: "T-2", subject: "Task 2", description: "Description 2", status: "in_progress", blocks: [], blockedBy: [], metadata: { priority: "low" }, }, ]; mockCtx.client.session.todo.mockResolvedValue([]); // when await syncAllTasksToTodos(mockCtx, tasks, "session-1"); // then expect(mockCtx.client.session.todo).toHaveBeenCalled(); }); it("removes deleted tasks from todo list", async () => { // given const tasks: Task[] = [ { id: "T-1", subject: "Task 1", description: "Description 1", status: "deleted", blocks: [], blockedBy: [], }, ]; const currentTodos: TodoInfo[] = [ { id: "T-1", content: "Task 1", status: "pending", }, ]; mockCtx.client.session.todo.mockResolvedValue(currentTodos); let writtenTodos: TodoInfo[] = []; const writer = async (input: { sessionID: string; todos: TodoInfo[] }) => { writtenTodos = input.todos; }; // when await syncAllTasksToTodos(mockCtx, tasks, "session-1", writer); // then expect(writtenTodos.some((t: TodoInfo) => t.id === "T-1")).toBe(false); }); it("preserves existing todos not in task list", async () => { // given const tasks: Task[] = [ { id: "T-1", subject: "Task 1", description: "Description 1", status: "pending", blocks: [], blockedBy: [], }, ]; const currentTodos: TodoInfo[] = [ { id: "T-1", content: "Task 1", status: "pending", }, { id: "T-existing", content: "Existing todo", status: "pending", }, ]; mockCtx.client.session.todo.mockResolvedValue(currentTodos); let writtenTodos: TodoInfo[] = []; const writer = async (input: { sessionID: string; todos: TodoInfo[] }) => { writtenTodos = input.todos; }; // when await syncAllTasksToTodos(mockCtx, tasks, "session-1", writer); // then expect(writtenTodos.some((t: TodoInfo) => t.id === "T-existing")).toBe(true); expect(writtenTodos.some((t: TodoInfo) => t.content === "Task 1")).toBe(true); }); it("handles empty task list", async () => { // given const tasks: Task[] = []; mockCtx.client.session.todo.mockResolvedValue([]); // when await syncAllTasksToTodos(mockCtx, tasks, "session-1"); // then expect(mockCtx.client.session.todo).toHaveBeenCalled(); }); it("calls writer with final todos", async () => { // given const tasks: Task[] = [ { id: "T-1", subject: "Task 1", description: "Description 1", status: "pending", blocks: [], blockedBy: [], }, ]; mockCtx.client.session.todo.mockResolvedValue([]); let writerCalled = false; const writer = async (input: { sessionID: string; todos: TodoInfo[] }) => { writerCalled = true; expect(input.sessionID).toBe("session-1"); expect(input.todos.length).toBe(1); expect(input.todos[0].content).toBe("Task 1"); }; // when await syncAllTasksToTodos(mockCtx, tasks, "session-1", writer); // then expect(writerCalled).toBe(true); }); it("deduplicates no-id todos when task replaces existing content", async () => { // given const tasks: Task[] = [ { id: "T-1", subject: "Task 1 (updated)", description: "Description 1", status: "in_progress", blocks: [], blockedBy: [], }, ]; const currentTodos: TodoInfo[] = [ { content: "Task 1 (updated)", status: "pending", }, ]; mockCtx.client.session.todo.mockResolvedValue(currentTodos); let writtenTodos: TodoInfo[] = []; const writer = async (input: { sessionID: string; todos: TodoInfo[] }) => { writtenTodos = input.todos; }; // when await syncAllTasksToTodos(mockCtx, tasks, "session-1", writer); // then — no duplicates const matching = writtenTodos.filter((t: TodoInfo) => t.content === "Task 1 (updated)"); expect(matching.length).toBe(1); expect(matching[0].status).toBe("in_progress"); }); it("preserves todos without id field", async () => { // given const tasks: Task[] = [ { id: "T-1", subject: "Task 1", description: "Description 1", status: "pending", blocks: [], blockedBy: [], }, ]; const currentTodos: TodoInfo[] = [ { id: "T-1", content: "Task 1", status: "pending", }, { content: "Todo without id", status: "pending", }, ]; mockCtx.client.session.todo.mockResolvedValue(currentTodos); // when await syncAllTasksToTodos(mockCtx, tasks, "session-1"); // then expect(mockCtx.client.session.todo).toHaveBeenCalled(); }); });