From 043348442de70a862d18c649cd854165f08d3b58 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 28 Apr 2026 10:46:07 +0900 Subject: [PATCH] feat(team-mode): add team tasklist list with tests --- .../team-mode/team-tasklist/list.test.ts | 63 ++++++++++++++++++ src/features/team-mode/team-tasklist/list.ts | 65 +++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/features/team-mode/team-tasklist/list.test.ts create mode 100644 src/features/team-mode/team-tasklist/list.ts diff --git a/src/features/team-mode/team-tasklist/list.test.ts b/src/features/team-mode/team-tasklist/list.test.ts new file mode 100644 index 000000000..0541ff9ad --- /dev/null +++ b/src/features/team-mode/team-tasklist/list.test.ts @@ -0,0 +1,63 @@ +/// + +import { expect, test } from "bun:test" +import { writeFile } from "node:fs/promises" +import path from "node:path" + +import { getTasksDir, resolveBaseDir } from "../team-registry" +import { createTask } from "./store" +import { createTaskInput, createTasklistFixture } from "./test-support" +import { updateTaskStatus } from "./update" +import { listTasks } from "./list" + +test("listTasks returns tasks sorted ascending and honors filters", async () => { + // given + const fixture = await createTasklistFixture() + + try { + const firstTask = await createTask( + fixture.teamRunId, + createTaskInput({ subject: "one", status: "claimed", owner: "member-a", claimedAt: Date.now() }), + fixture.config, + ) + await createTask(fixture.teamRunId, createTaskInput({ subject: "two" }), fixture.config) + const thirdTask = await createTask( + fixture.teamRunId, + createTaskInput({ subject: "three", status: "claimed", owner: "member-a", claimedAt: Date.now() }), + fixture.config, + ) + await updateTaskStatus(fixture.teamRunId, thirdTask.id, "in_progress", "member-a", fixture.config) + + // when + const allTasks = await listTasks(fixture.teamRunId, fixture.config) + const claimedTasks = await listTasks(fixture.teamRunId, fixture.config, { status: "claimed", owner: "member-a" }) + + // then + expect(allTasks.map((task) => task.id)).toEqual([firstTask.id, "2", thirdTask.id]) + expect(claimedTasks).toHaveLength(1) + expect(claimedTasks[0]?.id).toBe(firstTask.id) + } finally { + await fixture.cleanup() + } +}) + +test("listTasks skips malformed task files", async () => { + // given + const fixture = await createTasklistFixture() + + try { + const validTask = await createTask(fixture.teamRunId, createTaskInput(), fixture.config) + const tasksDirectory = getTasksDir(resolveBaseDir(fixture.config), fixture.teamRunId) + await writeFile(path.join(tasksDirectory, "bad.json"), "{not-json") + await writeFile(path.join(tasksDirectory, ".highwatermark"), "1") + + // when + const listedTasks = await listTasks(fixture.teamRunId, fixture.config) + + // then + expect(listedTasks).toHaveLength(1) + expect(listedTasks[0]?.id).toBe(validTask.id) + } finally { + await fixture.cleanup() + } +}) diff --git a/src/features/team-mode/team-tasklist/list.ts b/src/features/team-mode/team-tasklist/list.ts new file mode 100644 index 000000000..d462a655e --- /dev/null +++ b/src/features/team-mode/team-tasklist/list.ts @@ -0,0 +1,65 @@ +import type { Dirent } from "node:fs" +import { readdir, readFile } from "node:fs/promises" +import path from "node:path" + +import type { TeamModeConfig } from "../../../config/schema/team-mode" +import { log } from "../../../shared/logger" +import { getTasksDir, resolveBaseDir } from "../team-registry" +import { TaskSchema } from "../types" +import type { Task } from "../types" + +type TaskListFilter = { + status?: Task["status"] + owner?: string +} + +export async function listTasks( + teamRunId: string, + config: TeamModeConfig, + filter?: TaskListFilter, +): Promise { + const tasksDirectory = getTasksDir(resolveBaseDir(config), teamRunId) + + let entries: Dirent[] + try { + entries = await readdir(tasksDirectory, { withFileTypes: true }) + } catch { + return [] + } + + const parsedTasks: Task[] = [] + for (const entry of entries) { + if (entry.isDirectory() || entry.name.startsWith(".") || !entry.name.endsWith(".json")) continue + + const taskPath = path.join(tasksDirectory, entry.name) + try { + const taskContent = await readFile(taskPath, "utf8") + const parsedTask = TaskSchema.safeParse(JSON.parse(taskContent)) + if (!parsedTask.success) { + log("team-tasklist skipped malformed task", { + event: "team-tasklist-malformed-task", + taskPath, + issues: parsedTask.error.issues, + }) + continue + } + parsedTasks.push(parsedTask.data) + } catch (error) { + log("team-tasklist skipped malformed task", { + event: "team-tasklist-malformed-task", + taskPath, + error: error instanceof Error ? error.message : String(error), + }) + } + } + + return parsedTasks + .filter((task) => { + if (filter?.status !== undefined && task.status !== filter.status) { + return false + } + + return filter?.owner === undefined || task.owner === filter.owner + }) + .sort((leftTask, rightTask) => Number.parseInt(leftTask.id, 10) - Number.parseInt(rightTask.id, 10)) +}