oh-my-opencode/src/tools/task/todo-sync.test.ts
YeonGyu-Kim 92639ca38f feat(task): refactor to Claude Code style individual tools
- Split unified Task tool into individual tools (TaskCreate, TaskGet, TaskList, TaskUpdate)
- Update schema to Claude Code field names (subject, blockedBy, blocks, activeForm, owner, metadata)
- Add OpenCode Todo API sync layer (todo-sync.ts)
- Implement Todo sync on task create/update for continuation enforcement
- Add comprehensive tests for all tools (96 tests total)
- Update AGENTS.md documentation

Breaking Changes:
- Field names changed: title→subject, dependsOn→blockedBy, open→pending
- Tool names changed: task→task_create, task_get, task_list, task_update

Closes: todo-continuation-enforcer now sees Task-created items
2026-02-02 13:13:06 +09:00

409 lines
8.7 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from "bun:test"
import type { Task } from "../../features/claude-tasks/types"
import { syncTaskToTodo, syncAllTasksToTodos, 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: undefined,
})
})
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).toBeUndefined()
})
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).toBeUndefined()
})
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("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)
// when
await syncAllTasksToTodos(mockCtx, tasks, "session-1")
// then
expect(mockCtx.client.session.todo).toHaveBeenCalled()
})
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)
// when
await syncAllTasksToTodos(mockCtx, tasks, "session-1")
// then
expect(mockCtx.client.session.todo).toHaveBeenCalled()
})
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("handles undefined sessionID", async () => {
// given
const tasks: Task[] = [
{
id: "T-1",
subject: "Task 1",
description: "Description 1",
status: "pending",
blocks: [],
blockedBy: [],
},
]
mockCtx.client.session.todo.mockResolvedValue([])
// when
await syncAllTasksToTodos(mockCtx, tasks)
// then
expect(mockCtx.client.session.todo).toHaveBeenCalledWith({
path: { id: "" },
})
})
})