diff --git a/src/tools/agent-teams/team-task-store.test.ts b/src/tools/agent-teams/team-task-store.test.ts
index 9314f3b94..c17b642e3 100644
--- a/src/tools/agent-teams/team-task-store.test.ts
+++ b/src/tools/agent-teams/team-task-store.test.ts
@@ -1,44 +1,460 @@
///
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
-import { mkdtempSync, rmSync } from "node:fs"
+import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
+import { dirname } from "node:path"
+import { ensureDir } from "../../features/claude-tasks/storage"
import { tmpdir } from "node:os"
import { join } from "node:path"
-import { createTeamConfig, deleteTeamData } from "./team-config-store"
-import { createTeamTask, deleteTeamTaskFile, readTeamTask } from "./team-task-store"
+import {
+ getTeamTaskPath,
+ readTeamTask,
+ writeTeamTask,
+ listTeamTasks,
+ deleteTeamTask,
+} from "./team-task-store"
+import type { TeamTask } from "./types"
-describe("agent-teams task store", () => {
+describe("getTeamTaskPath", () => {
+ test("returns correct file path for team task", () => {
+ //#given
+ const teamName = "my-team"
+ const taskId = "T-abc123"
+
+ //#when
+ const result = getTeamTaskPath(teamName, taskId)
+
+ //#then
+ expect(result).toContain("my-team")
+ expect(result).toContain("T-abc123.json")
+ })
+})
+
+describe("readTeamTask", () => {
let originalCwd: string
let tempProjectDir: string
beforeEach(() => {
originalCwd = process.cwd()
- tempProjectDir = mkdtempSync(join(tmpdir(), "agent-teams-task-store-"))
+ tempProjectDir = mkdtempSync(join(tmpdir(), "team-task-store-test-"))
process.chdir(tempProjectDir)
- createTeamConfig("core", "Core team", "ses-main", tempProjectDir, "sisyphus")
})
afterEach(() => {
- deleteTeamData("core")
process.chdir(originalCwd)
- rmSync(tempProjectDir, { recursive: true, force: true })
+ if (existsSync(tempProjectDir)) {
+ rmSync(tempProjectDir, { recursive: true, force: true })
+ }
})
- test("creates and reads a task", () => {
+ test("returns null when task file does not exist", () => {
//#given
- const created = createTeamTask("core", "Subject", "Description")
+ const teamName = "nonexistent-team"
+ const taskId = "T-does-not-exist"
//#when
- const loaded = readTeamTask("core", created.id)
+ const result = readTeamTask(teamName, taskId)
//#then
- expect(loaded?.id).toBe(created.id)
- expect(loaded?.subject).toBe("Subject")
+ expect(result).toBeNull()
})
- test("rejects invalid team name and task id", () => {
+ test("returns task when valid task file exists", () => {
+ //#given
+ const task: TeamTask = {
+ id: "T-existing-task",
+ subject: "Test task",
+ description: "Test description",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_test",
+ }
+ writeTeamTask("test-team", "T-existing-task", task)
+
+ //#when
+ const result = readTeamTask("test-team", "T-existing-task")
+
//#then
- expect(() => readTeamTask("../../etc", "T-1")).toThrow("team_name_invalid")
- expect(() => readTeamTask("core", "../../passwd")).toThrow("task_id_invalid")
- expect(() => deleteTeamTaskFile("core", "../../passwd")).toThrow("task_id_invalid")
+ expect(result).not.toBeNull()
+ expect(result?.id).toBe("T-existing-task")
+ expect(result?.subject).toBe("Test task")
+ })
+
+ test("returns null when task file contains invalid JSON", () => {
+ //#given
+ const taskPath = getTeamTaskPath("invalid-team", "T-invalid-json")
+ const parentDir = dirname(taskPath)
+ rmSync(parentDir, { recursive: true, force: true })
+ ensureDir(parentDir)
+ writeFileSync(taskPath, "{ invalid json }")
+
+ //#when
+ const result = readTeamTask("invalid-team", "T-invalid-json")
+
+ //#then
+ expect(result).toBeNull()
+ })
+
+ test("returns null when task file does not match schema", () => {
+ //#given
+ const taskPath = getTeamTaskPath("invalid-schema-team", "T-bad-schema")
+ const parentDir = dirname(taskPath)
+ rmSync(parentDir, { recursive: true, force: true })
+ ensureDir(parentDir)
+ const invalidData = { id: "T-bad-schema" }
+ writeFileSync(taskPath, JSON.stringify(invalidData))
+
+ //#when
+ const result = readTeamTask("invalid-schema-team", "T-bad-schema")
+
+ //#then
+ expect(result).toBeNull()
+ })
+})
+
+describe("writeTeamTask", () => {
+ let originalCwd: string
+ let tempProjectDir: string
+
+ beforeEach(() => {
+ originalCwd = process.cwd()
+ tempProjectDir = mkdtempSync(join(tmpdir(), "team-task-store-test-"))
+ process.chdir(tempProjectDir)
+ })
+
+ afterEach(() => {
+ process.chdir(originalCwd)
+ if (existsSync(tempProjectDir)) {
+ rmSync(tempProjectDir, { recursive: true, force: true })
+ }
+ })
+
+ test("creates task file in team namespace", () => {
+ //#given
+ const task: TeamTask = {
+ id: "T-write-test",
+ subject: "Write test task",
+ description: "Test writing task",
+ status: "in_progress",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_write",
+ }
+
+ //#when
+ writeTeamTask("write-team", "T-write-test", task)
+
+ //#then
+ const taskPath = getTeamTaskPath("write-team", "T-write-test")
+ expect(existsSync(taskPath)).toBe(true)
+ })
+
+ test("overwrites existing task file", () => {
+ //#given
+ const task: TeamTask = {
+ id: "T-overwrite-test",
+ subject: "Original subject",
+ description: "Original description",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_original",
+ }
+ writeTeamTask("overwrite-team", "T-overwrite-test", task)
+
+ const updatedTask: TeamTask = {
+ ...task,
+ subject: "Updated subject",
+ status: "completed",
+ }
+
+ //#when
+ writeTeamTask("overwrite-team", "T-overwrite-test", updatedTask)
+
+ //#then
+ const result = readTeamTask("overwrite-team", "T-overwrite-test")
+ expect(result?.subject).toBe("Updated subject")
+ expect(result?.status).toBe("completed")
+ })
+
+ test("creates team directory if it does not exist", () => {
+ //#given
+ const task: TeamTask = {
+ id: "T-new-dir",
+ subject: "New directory test",
+ description: "Test",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_newdir",
+ }
+
+ //#when
+ writeTeamTask("new-team-directory", "T-new-dir", task)
+
+ //#then
+ const taskPath = getTeamTaskPath("new-team-directory", "T-new-dir")
+ expect(existsSync(taskPath)).toBe(true)
+ })
+})
+
+describe("listTeamTasks", () => {
+ let originalCwd: string
+ let tempProjectDir: string
+
+ beforeEach(() => {
+ originalCwd = process.cwd()
+ tempProjectDir = mkdtempSync(join(tmpdir(), "team-task-store-test-"))
+ process.chdir(tempProjectDir)
+ })
+
+ afterEach(() => {
+ process.chdir(originalCwd)
+ if (existsSync(tempProjectDir)) {
+ rmSync(tempProjectDir, { recursive: true, force: true })
+ }
+ })
+
+ test("returns empty array when team has no tasks", () => {
+ //#given
+ // No tasks written
+
+ //#when
+ const result = listTeamTasks("empty-team")
+
+ //#then
+ expect(result).toEqual([])
+ })
+
+ test("returns all tasks for a team", () => {
+ //#given
+ const task1: TeamTask = {
+ id: "T-task-1",
+ subject: "Task 1",
+ description: "First task",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_1",
+ }
+ const task2: TeamTask = {
+ id: "T-task-2",
+ subject: "Task 2",
+ description: "Second task",
+ status: "in_progress",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_2",
+ }
+ writeTeamTask("list-test-team", "T-task-1", task1)
+ writeTeamTask("list-test-team", "T-task-2", task2)
+
+ //#when
+ const result = listTeamTasks("list-test-team")
+
+ //#then
+ expect(result).toHaveLength(2)
+ expect(result.some((t) => t.id === "T-task-1")).toBe(true)
+ expect(result.some((t) => t.id === "T-task-2")).toBe(true)
+ })
+
+ test("includes tasks with all statuses", () => {
+ //#given
+ const pendingTask: TeamTask = {
+ id: "T-pending",
+ subject: "Pending task",
+ description: "Test",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_pending",
+ }
+ const inProgressTask: TeamTask = {
+ id: "T-in-progress",
+ subject: "In progress task",
+ description: "Test",
+ status: "in_progress",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_inprogress",
+ }
+ const completedTask: TeamTask = {
+ id: "T-completed",
+ subject: "Completed task",
+ description: "Test",
+ status: "completed",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_completed",
+ }
+ const deletedTask: TeamTask = {
+ id: "T-deleted",
+ subject: "Deleted task",
+ description: "Test",
+ status: "deleted",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_deleted",
+ }
+ writeTeamTask("status-test-team", "T-pending", pendingTask)
+ writeTeamTask("status-test-team", "T-in-progress", inProgressTask)
+ writeTeamTask("status-test-team", "T-completed", completedTask)
+ writeTeamTask("status-test-team", "T-deleted", deletedTask)
+
+ //#when
+ const result = listTeamTasks("status-test-team")
+
+ //#then
+ expect(result).toHaveLength(4)
+ const statuses = result.map((t) => t.status)
+ expect(statuses).toContain("pending")
+ expect(statuses).toContain("in_progress")
+ expect(statuses).toContain("completed")
+ expect(statuses).toContain("deleted")
+ })
+
+ test("does not include tasks from other teams", () => {
+ //#given
+ const taskTeam1: TeamTask = {
+ id: "T-team1-task",
+ subject: "Team 1 task",
+ description: "Test",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_team1",
+ }
+ const taskTeam2: TeamTask = {
+ id: "T-team2-task",
+ subject: "Team 2 task",
+ description: "Test",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_team2",
+ }
+ writeTeamTask("team-1", "T-team1-task", taskTeam1)
+ writeTeamTask("team-2", "T-team2-task", taskTeam2)
+
+ //#when
+ const result = listTeamTasks("team-1")
+
+ //#then
+ expect(result).toHaveLength(1)
+ expect(result[0].id).toBe("T-team1-task")
+ })
+})
+
+describe("deleteTeamTask", () => {
+ let originalCwd: string
+ let tempProjectDir: string
+
+ beforeEach(() => {
+ originalCwd = process.cwd()
+ tempProjectDir = mkdtempSync(join(tmpdir(), "team-task-store-test-"))
+ process.chdir(tempProjectDir)
+ })
+
+ afterEach(() => {
+ process.chdir(originalCwd)
+ if (existsSync(tempProjectDir)) {
+ rmSync(tempProjectDir, { recursive: true, force: true })
+ }
+ })
+
+ test("deletes existing task file", () => {
+ //#given
+ const task: TeamTask = {
+ id: "T-delete-me",
+ subject: "Delete this task",
+ description: "Test",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_delete",
+ }
+ writeTeamTask("delete-test-team", "T-delete-me", task)
+ const taskPath = getTeamTaskPath("delete-test-team", "T-delete-me")
+
+ //#when
+ deleteTeamTask("delete-test-team", "T-delete-me")
+
+ //#then
+ expect(existsSync(taskPath)).toBe(false)
+ })
+
+ test("does not throw when task does not exist", () => {
+ //#given
+ // Task does not exist
+
+ //#when
+ expect(() => deleteTeamTask("nonexistent-team", "T-does-not-exist")).not.toThrow()
+
+ //#then
+ // No exception thrown
+ })
+
+ test("does not affect other tasks in same team", () => {
+ //#given
+ const task1: TeamTask = {
+ id: "T-keep-me",
+ subject: "Keep this task",
+ description: "Test",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_keep",
+ }
+ const task2: TeamTask = {
+ id: "T-delete-me",
+ subject: "Delete this task",
+ description: "Test",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_delete",
+ }
+ writeTeamTask("mixed-test-team", "T-keep-me", task1)
+ writeTeamTask("mixed-test-team", "T-delete-me", task2)
+
+ //#when
+ deleteTeamTask("mixed-test-team", "T-delete-me")
+
+ //#then
+ const remaining = listTeamTasks("mixed-test-team")
+ expect(remaining).toHaveLength(1)
+ expect(remaining[0].id).toBe("T-keep-me")
+ })
+
+ test("does not affect tasks from other teams", () => {
+ //#given
+ const task1: TeamTask = {
+ id: "T-task-1",
+ subject: "Task 1",
+ description: "Test",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_1",
+ }
+ const task2: TeamTask = {
+ id: "T-task-2",
+ subject: "Task 2",
+ description: "Test",
+ status: "pending",
+ blocks: [],
+ blockedBy: [],
+ threadID: "ses_2",
+ }
+ writeTeamTask("team-a", "T-task-1", task1)
+ writeTeamTask("team-b", "T-task-2", task2)
+
+ //#when
+ deleteTeamTask("team-a", "T-task-1")
+
+ //#then
+ const remainingInTeamB = listTeamTasks("team-b")
+ expect(remainingInTeamB).toHaveLength(1)
+ expect(remainingInTeamB[0].id).toBe("T-task-2")
})
})
diff --git a/src/tools/agent-teams/team-task-store.ts b/src/tools/agent-teams/team-task-store.ts
index 6fb599b6d..10895eb4f 100644
--- a/src/tools/agent-teams/team-task-store.ts
+++ b/src/tools/agent-teams/team-task-store.ts
@@ -41,6 +41,8 @@ function withTaskLock(teamName: string, operation: () => T): T {
}
}
+export { getTeamTaskPath } from "./paths"
+
export function readTeamTask(teamName: string, taskId: string): TeamTask | null {
assertValidTeamName(teamName)
assertValidTaskId(taskId)
@@ -94,32 +96,33 @@ export function createTeamTask(
}
return withTaskLock(teamName, () => {
+ const taskId = generateTaskId()
const task: TeamTask = {
- id: generateTaskId(),
+ id: taskId,
subject,
description,
activeForm,
status: "pending",
blocks: [],
blockedBy: [],
+ threadID: `unknown_${taskId}`,
...(metadata ? { metadata } : {}),
}
const validated = TeamTaskSchema.parse(task)
- writeJsonAtomic(getTeamTaskPath(teamName, validated.id), validated)
+ writeJsonAtomic(getTeamTaskPath(teamName, taskId), validated)
return validated
})
}
-export function writeTeamTask(teamName: string, task: TeamTask): TeamTask {
+export function writeTeamTask(teamName: string, taskId: string, task: TeamTask): void {
assertValidTeamName(teamName)
- assertValidTaskId(task.id)
+ assertValidTaskId(taskId)
const validated = TeamTaskSchema.parse(task)
- writeJsonAtomic(getTeamTaskPath(teamName, validated.id), validated)
- return validated
+ writeJsonAtomic(getTeamTaskPath(teamName, taskId), validated)
}
-export function deleteTeamTaskFile(teamName: string, taskId: string): void {
+export function deleteTeamTask(teamName: string, taskId: string): void {
assertValidTeamName(teamName)
assertValidTaskId(taskId)
const taskPath = getTeamTaskPath(teamName, taskId)
@@ -128,6 +131,11 @@ export function deleteTeamTaskFile(teamName: string, taskId: string): void {
}
}
+// Backward compatibility alias
+export function deleteTeamTaskFile(teamName: string, taskId: string): void {
+ deleteTeamTask(teamName, taskId)
+}
+
export function readTaskFromDirectory(taskDir: string, taskId: string): TeamTask | null {
assertValidTaskId(taskId)
return readJsonSafe(join(taskDir, `${taskId}.json`), TeamTaskSchema)
@@ -146,7 +154,7 @@ export function resetOwnerTasks(teamName: string, ownerName: string): void {
owner: undefined,
status: task.status === "completed" ? "completed" : "pending",
}
- writeTeamTask(teamName, next)
+ writeTeamTask(teamName, next.id, next)
}
})
}