feat(team-mode): add team tasklist store with tests
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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<number> {
|
||||||
|
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<Task, "id" | "createdAt" | "updatedAt" | "version">,
|
||||||
|
config: TeamModeConfig,
|
||||||
|
): Promise<Task> {
|
||||||
|
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}` })
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user