Implement unified Claude Tasks system with single multi-action tool (#1356)
* chore: pin bun-types to 1.3.6 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * chore: exclude test files and script from tsconfig 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * refactor: remove sisyphus-swarm feature Remove mailbox types and swarm config schema. Update docs. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * refactor: remove legacy sisyphus-tasks feature Remove old storage and types implementation, replaced by claude-tasks. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(claude-tasks): add task schema and storage utilities - Task schema with Zod validation (pending, in_progress, completed, deleted) - Storage utilities: getTaskDir, readJsonSafe, writeJsonAtomic, acquireLock - Atomic writes with temp file + rename - File-based locking with 30s stale threshold 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(tools/task): add task object schemas Add Zod schemas for task CRUD operations input validation. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(tools): add TaskCreate tool Create new tasks with sequential ID generation and lock-based concurrency. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(tools): add TaskGet tool Retrieve task by ID with null-safe handling. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(tools): add TaskUpdate tool with claim validation Update tasks with status transitions and owner claim validation. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(tools): add TaskList tool and exports - TaskList for summary view of all tasks - Export all claude-tasks tool factories from index 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(hooks): add task-reminder hook Remind agents to use task tools after 10 turns without task operations. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(config): add disabled_tools setting and tasks-todowrite-disabler hook - Add disabled_tools config option to disable specific tools by name - Register tasks-todowrite-disabler hook name in schema 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(config-handler): add task_* and teammate tool permissions Grant task_* and teammate permissions to atlas, sisyphus, prometheus, and sisyphus-junior agents. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(delegate-task): add execute option for task execution Add optional execute field with task_id and task_dir for task-based delegation. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * fix(truncator): add type guard for non-string outputs Prevent crashes when output is not a string by adding typeof checks. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * chore: export config types and update task-resume-info - Export SisyphusConfig and SisyphusTasksConfig types - Add task_tool to TARGET_TOOLS list 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * refactor(storage): remove team namespace, use flat task directory * feat(task): implement unified task tool with all 5 actions * fix(hooks): update task-reminder to track unified task tool * refactor(tools): register unified task tool, remove 4 separate tools * chore(cleanup): remove old 4-tool task implementation * refactor(config): use new_task_system_enabled as top-level flag - Add new_task_system_enabled to OhMyOpenCodeConfigSchema - Remove enabled from SisyphusTasksConfigSchema (keep storage_path, claude_code_compat) - Update index.ts to gate on new_task_system_enabled - Update plugin-config.ts default for config initialization - Update test configs in task.test.ts and storage.test.ts Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix: resolve typecheck and test failures - Add explicit ToolDefinition return type to createTask function - Fix planDemoteConfig to use 'subagent' mode instead of 'all' --------- Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,768 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import { existsSync, rmSync, mkdirSync, writeFileSync, readdirSync } from "fs"
|
||||
import { join } from "path"
|
||||
import type { TaskObject } from "./types"
|
||||
import { createTask } from "./task"
|
||||
|
||||
const TEST_STORAGE = ".test-task-tool"
|
||||
const TEST_DIR = join(process.cwd(), TEST_STORAGE)
|
||||
const TEST_CONFIG = {
|
||||
new_task_system_enabled: true,
|
||||
sisyphus: {
|
||||
tasks: {
|
||||
storage_path: TEST_STORAGE,
|
||||
claude_code_compat: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
const TEST_SESSION_ID = "test-session-123"
|
||||
const TEST_ABORT_CONTROLLER = new AbortController()
|
||||
const TEST_CONTEXT = {
|
||||
sessionID: TEST_SESSION_ID,
|
||||
messageID: "test-message-123",
|
||||
agent: "test-agent",
|
||||
abort: TEST_ABORT_CONTROLLER.signal,
|
||||
}
|
||||
|
||||
describe("task_tool", () => {
|
||||
let taskTool: ReturnType<typeof createTask>
|
||||
|
||||
beforeEach(() => {
|
||||
if (existsSync(TEST_STORAGE)) {
|
||||
rmSync(TEST_STORAGE, { recursive: true, force: true })
|
||||
}
|
||||
mkdirSync(TEST_DIR, { recursive: true })
|
||||
taskTool = createTask(TEST_CONFIG)
|
||||
})
|
||||
|
||||
async function createTestTask(title: string, overrides: Partial<Parameters<typeof taskTool.execute>[0]> = {}): Promise<string> {
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title,
|
||||
...overrides,
|
||||
}
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
return (result as { task: TaskObject }).task.id
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(TEST_STORAGE)) {
|
||||
rmSync(TEST_STORAGE, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// CREATE ACTION TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe("create action", () => {
|
||||
test("creates task with required title field", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Implement authentication",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveProperty("task")
|
||||
expect(result.task).toHaveProperty("id")
|
||||
expect(result.task.title).toBe("Implement authentication")
|
||||
expect(result.task.status).toBe("open")
|
||||
})
|
||||
|
||||
test("auto-generates T-{uuid} format ID", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Test task",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task.id).toMatch(/^T-[a-f0-9-]+$/)
|
||||
})
|
||||
|
||||
test("auto-records threadID from session context", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Test task",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task).toHaveProperty("threadID")
|
||||
expect(typeof result.task.threadID).toBe("string")
|
||||
})
|
||||
|
||||
test("sets status to open by default", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Test task",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task.status).toBe("open")
|
||||
})
|
||||
|
||||
test("stores optional description field", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Test task",
|
||||
description: "Detailed description of the task",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task.description).toBe("Detailed description of the task")
|
||||
})
|
||||
|
||||
test("stores dependsOn array", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Test task",
|
||||
dependsOn: ["T-dep1", "T-dep2"],
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task.dependsOn).toEqual(["T-dep1", "T-dep2"])
|
||||
})
|
||||
|
||||
test("stores parentID when provided", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Subtask",
|
||||
parentID: "T-parent123",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task.parentID).toBe("T-parent123")
|
||||
})
|
||||
|
||||
test("stores repoURL when provided", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Test task",
|
||||
repoURL: "https://github.com/code-yeongyu/oh-my-opencode",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task.repoURL).toBe("https://github.com/code-yeongyu/oh-my-opencode")
|
||||
})
|
||||
|
||||
test("returns result as JSON string with task property", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Test task",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
|
||||
//#then
|
||||
expect(typeof resultStr).toBe("string")
|
||||
const result = JSON.parse(resultStr)
|
||||
expect(result).toHaveProperty("task")
|
||||
})
|
||||
|
||||
test("initializes dependsOn as empty array when not provided", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Test task",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task.dependsOn).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// LIST ACTION TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe("list action", () => {
|
||||
test("returns all non-completed tasks by default", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "list" as const,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveProperty("tasks")
|
||||
expect(Array.isArray(result.tasks)).toBe(true)
|
||||
})
|
||||
|
||||
test("excludes completed tasks from list", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "list" as const,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
const completedTasks = result.tasks.filter((t: TaskObject) => t.status === "completed")
|
||||
expect(completedTasks.length).toBe(0)
|
||||
})
|
||||
|
||||
test("applies ready filter when requested", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "list" as const,
|
||||
ready: true,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveProperty("tasks")
|
||||
expect(Array.isArray(result.tasks)).toBe(true)
|
||||
})
|
||||
|
||||
test("respects limit parameter", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "list" as const,
|
||||
limit: 5,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.tasks.length).toBeLessThanOrEqual(5)
|
||||
})
|
||||
|
||||
test("returns result as JSON string with tasks array", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "list" as const,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
|
||||
//#then
|
||||
expect(typeof resultStr).toBe("string")
|
||||
const result = JSON.parse(resultStr)
|
||||
expect(Array.isArray(result.tasks)).toBe(true)
|
||||
})
|
||||
|
||||
test("filters by status when provided", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "list" as const,
|
||||
status: "in_progress" as const,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
const allInProgress = result.tasks.every((t: TaskObject) => t.status === "in_progress")
|
||||
expect(allInProgress).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// GET ACTION TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe("get action", () => {
|
||||
test("returns task by ID", async () => {
|
||||
//#given
|
||||
const testId = await createTestTask("Test task")
|
||||
const args = {
|
||||
action: "get" as const,
|
||||
id: testId,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveProperty("task")
|
||||
})
|
||||
|
||||
test("returns null for non-existent task", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "get" as const,
|
||||
id: "T-nonexistent",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task).toBeNull()
|
||||
})
|
||||
|
||||
test("returns result as JSON string with task property", async () => {
|
||||
//#given
|
||||
const testId = await createTestTask("Test task")
|
||||
const args = {
|
||||
action: "get" as const,
|
||||
id: testId,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
|
||||
//#then
|
||||
expect(typeof resultStr).toBe("string")
|
||||
const result = JSON.parse(resultStr)
|
||||
expect(result).toHaveProperty("task")
|
||||
})
|
||||
|
||||
test("returns complete task object with all fields", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "get" as const,
|
||||
id: "T-test123",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
if (result.task !== null) {
|
||||
expect(result.task).toHaveProperty("id")
|
||||
expect(result.task).toHaveProperty("title")
|
||||
expect(result.task).toHaveProperty("status")
|
||||
expect(result.task).toHaveProperty("threadID")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// UPDATE ACTION TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe("update action", () => {
|
||||
test("updates task title", async () => {
|
||||
//#given
|
||||
const testId = await createTestTask("Test task")
|
||||
const args = {
|
||||
action: "update" as const,
|
||||
id: testId,
|
||||
title: "Updated title",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveProperty("task")
|
||||
expect(result.task.title).toBe("Updated title")
|
||||
})
|
||||
|
||||
test("updates task description", async () => {
|
||||
//#given
|
||||
const testId = await createTestTask("Test task", { description: "Initial description" })
|
||||
const args = {
|
||||
action: "update" as const,
|
||||
id: testId,
|
||||
description: "Updated description",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task.description).toBe("Updated description")
|
||||
})
|
||||
|
||||
test("updates task status", async () => {
|
||||
//#given
|
||||
const testId = await createTestTask("Test task")
|
||||
const args = {
|
||||
action: "update" as const,
|
||||
id: testId,
|
||||
status: "in_progress" as const,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task.status).toBe("in_progress")
|
||||
})
|
||||
|
||||
test("updates dependsOn array", async () => {
|
||||
//#given
|
||||
const testId = await createTestTask("Test task")
|
||||
const args = {
|
||||
action: "update" as const,
|
||||
id: testId,
|
||||
dependsOn: ["T-dep1", "T-dep2", "T-dep3"],
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task.dependsOn).toEqual(["T-dep1", "T-dep2", "T-dep3"])
|
||||
})
|
||||
|
||||
test("returns error for non-existent task", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "update" as const,
|
||||
id: "T-nonexistent",
|
||||
title: "New title",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveProperty("error")
|
||||
expect(result.error).toBe("task_not_found")
|
||||
})
|
||||
|
||||
test("returns result as JSON string with task property", async () => {
|
||||
//#given
|
||||
const testId = await createTestTask("Test task")
|
||||
const args = {
|
||||
action: "update" as const,
|
||||
id: testId,
|
||||
title: "Updated",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
|
||||
//#then
|
||||
expect(typeof resultStr).toBe("string")
|
||||
const result = JSON.parse(resultStr)
|
||||
expect(result).toHaveProperty("task")
|
||||
})
|
||||
|
||||
test("updates multiple fields at once", async () => {
|
||||
//#given
|
||||
const testId = await createTestTask("Test task")
|
||||
const args = {
|
||||
action: "update" as const,
|
||||
id: testId,
|
||||
title: "New title",
|
||||
description: "New description",
|
||||
status: "completed" as const,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task.title).toBe("New title")
|
||||
expect(result.task.description).toBe("New description")
|
||||
expect(result.task.status).toBe("completed")
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// DELETE ACTION TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe("delete action", () => {
|
||||
test("removes task file physically", async () => {
|
||||
//#given
|
||||
const testId = await createTestTask("Test task")
|
||||
const args = {
|
||||
action: "delete" as const,
|
||||
id: testId,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveProperty("success")
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
test("returns success true on successful deletion", async () => {
|
||||
//#given
|
||||
const testId = await createTestTask("Test task")
|
||||
const args = {
|
||||
action: "delete" as const,
|
||||
id: testId,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
test("returns error for non-existent task", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "delete" as const,
|
||||
id: "T-nonexistent",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveProperty("error")
|
||||
expect(result.error).toBe("task_not_found")
|
||||
})
|
||||
|
||||
test("returns result as JSON string", async () => {
|
||||
//#given
|
||||
const testId = await createTestTask("Test task")
|
||||
const args = {
|
||||
action: "delete" as const,
|
||||
id: testId,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
|
||||
//#then
|
||||
expect(typeof resultStr).toBe("string")
|
||||
const result = JSON.parse(resultStr)
|
||||
expect(result).toHaveProperty("success")
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// EDGE CASE TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe("edge cases", () => {
|
||||
test("detects circular dependency (A depends on B, B depends on A)", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Task A",
|
||||
dependsOn: ["T-taskB"],
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
// Should either prevent creation or mark as circular
|
||||
expect(result).toHaveProperty("task")
|
||||
})
|
||||
|
||||
test("handles task depending on non-existent ID", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Task with missing dependency",
|
||||
dependsOn: ["T-nonexistent"],
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
// Should either allow or return error
|
||||
expect(result).toHaveProperty("task")
|
||||
})
|
||||
|
||||
test("ready filter returns true for empty dependsOn", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "list" as const,
|
||||
ready: true,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
const tasksWithNoDeps = result.tasks.filter((t: TaskObject) => t.dependsOn.length === 0)
|
||||
expect(tasksWithNoDeps.length).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
|
||||
test("ready filter includes tasks with all completed dependencies", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "list" as const,
|
||||
ready: true,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(Array.isArray(result.tasks)).toBe(true)
|
||||
})
|
||||
|
||||
test("ready filter excludes tasks with incomplete dependencies", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "list" as const,
|
||||
ready: true,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(Array.isArray(result.tasks)).toBe(true)
|
||||
})
|
||||
|
||||
test("handles empty title gracefully", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
// Should either reject or handle empty title
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
|
||||
test("handles very long title", async () => {
|
||||
//#given
|
||||
const longTitle = "A".repeat(1000)
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: longTitle,
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
|
||||
test("handles special characters in title", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Task with special chars: !@#$%^&*()",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
|
||||
test("handles unicode characters in title", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "任務 🚀 Tâche",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
|
||||
test("preserves all TaskObject fields in round-trip", async () => {
|
||||
//#given
|
||||
const args = {
|
||||
action: "create" as const,
|
||||
title: "Test task",
|
||||
description: "Test description",
|
||||
dependsOn: ["T-dep1"],
|
||||
parentID: "T-parent",
|
||||
repoURL: "https://example.com",
|
||||
}
|
||||
|
||||
//#when
|
||||
const resultStr = await taskTool.execute(args, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.task).toHaveProperty("id")
|
||||
expect(result.task).toHaveProperty("title")
|
||||
expect(result.task).toHaveProperty("description")
|
||||
expect(result.task).toHaveProperty("status")
|
||||
expect(result.task).toHaveProperty("dependsOn")
|
||||
expect(result.task).toHaveProperty("parentID")
|
||||
expect(result.task).toHaveProperty("repoURL")
|
||||
expect(result.task).toHaveProperty("threadID")
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user