From 6e0f6aefcc292238e66d6d0ac48b23dec043ded3 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 28 Apr 2026 10:46:04 +0900 Subject: [PATCH] feat(team-mode): add team tasklist store with tests --- .../team-mode/team-tasklist/store.test.ts | 32 +++++++++++ src/features/team-mode/team-tasklist/store.ts | 53 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/features/team-mode/team-tasklist/store.test.ts create mode 100644 src/features/team-mode/team-tasklist/store.ts diff --git a/src/features/team-mode/team-tasklist/store.test.ts b/src/features/team-mode/team-tasklist/store.test.ts new file mode 100644 index 000000000..f23fa74e6 --- /dev/null +++ b/src/features/team-mode/team-tasklist/store.test.ts @@ -0,0 +1,32 @@ +/// + +import { expect, test } from "bun:test" +import { readFile } 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" + +test("createTask assigns distinct ids during concurrent creation", async () => { + // given + const fixture = await createTasklistFixture() + + try { + // when + const [firstTask, secondTask] = await Promise.all([ + createTask(fixture.teamRunId, createTaskInput({ subject: "first task" }), fixture.config), + createTask(fixture.teamRunId, createTaskInput({ subject: "second task" }), fixture.config), + ]) + + const tasksDirectory = getTasksDir(resolveBaseDir(fixture.config), fixture.teamRunId) + const watermarkContent = await readFile(path.join(tasksDirectory, ".highwatermark"), "utf8") + const sortedIds = [firstTask.id, secondTask.id].sort((leftId, rightId) => Number(leftId) - Number(rightId)) + + // then + expect(sortedIds).toEqual(["1", "2"]) + expect(watermarkContent.trim()).toBe("2") + } finally { + await fixture.cleanup() + } +}) diff --git a/src/features/team-mode/team-tasklist/store.ts b/src/features/team-mode/team-tasklist/store.ts new file mode 100644 index 000000000..9a703839b --- /dev/null +++ b/src/features/team-mode/team-tasklist/store.ts @@ -0,0 +1,53 @@ +import { mkdir, readFile } from "node:fs/promises" +import path from "node:path" + +import type { TeamModeConfig } from "../../../config/schema/team-mode" +import { getTasksDir, resolveBaseDir } from "../team-registry" +import { atomicWrite, withLock } from "../team-state-store/locks" +import { TaskSchema } from "../types" +import type { Task } from "../types" + +const HIGH_WATERMARK_FILE = ".highwatermark" + +async function readHighWatermark(watermarkPath: string): Promise { + try { + const watermarkContent = (await readFile(watermarkPath, "utf8")).trim() + const parsedWatermark = Number.parseInt(watermarkContent, 10) + return Number.isInteger(parsedWatermark) && parsedWatermark >= 0 ? parsedWatermark : 0 + } catch { + await atomicWrite(watermarkPath, "0") + return 0 + } +} + +export async function createTask( + teamRunId: string, + taskInput: Omit, + config: TeamModeConfig, +): Promise { + const tasksDirectory = getTasksDir(resolveBaseDir(config), teamRunId) + await mkdir(tasksDirectory, { recursive: true, mode: 0o700 }) + await mkdir(path.join(tasksDirectory, "claims"), { recursive: true, mode: 0o700 }) + + return withLock(path.join(tasksDirectory, ".lock"), async () => { + const watermarkPath = path.join(tasksDirectory, HIGH_WATERMARK_FILE) + const nextTaskId = (await readHighWatermark(watermarkPath)) + 1 + await atomicWrite(watermarkPath, String(nextTaskId)) + + const now = Date.now() + const task = TaskSchema.parse({ + ...taskInput, + version: 1, + id: String(nextTaskId), + createdAt: now, + updatedAt: now, + }) + + await atomicWrite( + path.join(tasksDirectory, `${task.id}.json`), + `${JSON.stringify(task, null, 2)}\n`, + ) + + return task + }, { ownerTag: `create-task:${teamRunId}` }) +}