From 3f6933393935d726b478b59d830d5b5cb12ce615 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 28 Apr 2026 10:46:24 +0900 Subject: [PATCH] feat(team-mode): add team mailbox ack with tests --- .../team-mode/team-mailbox/ack.test.ts | 45 +++++++++++++++++++ src/features/team-mode/team-mailbox/ack.ts | 34 ++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/features/team-mode/team-mailbox/ack.test.ts create mode 100644 src/features/team-mode/team-mailbox/ack.ts diff --git a/src/features/team-mode/team-mailbox/ack.test.ts b/src/features/team-mode/team-mailbox/ack.test.ts new file mode 100644 index 000000000..4c337c1dd --- /dev/null +++ b/src/features/team-mode/team-mailbox/ack.test.ts @@ -0,0 +1,45 @@ +/// + +import { describe, expect, test } from "bun:test" +import { mkdtemp, readdir } from "node:fs/promises" +import { randomUUID } from "node:crypto" +import { tmpdir } from "node:os" +import path from "node:path" + +import { TeamModeConfigSchema } from "../../../config/schema/team-mode" +import { getInboxDir, resolveBaseDir } from "../team-registry/paths" +import { ackMessages } from "./ack" +import { sendMessage } from "./send" + +async function createBaseDirectory(): Promise { + return await mkdtemp(path.join(tmpdir(), "team-mailbox-ack-")) +} + +describe("ackMessages", () => { + test("moves inbox files into processed and stays idempotent", async () => { + // given + const config = TeamModeConfigSchema.parse({ base_dir: await createBaseDirectory() }) + const teamRunId = randomUUID() + const messageId = randomUUID() + await sendMessage({ + version: 1, + messageId, + from: "lead", + to: "m1", + kind: "message", + body: "hello", + timestamp: 100, + }, teamRunId, config, { isLead: true, activeMembers: ["m1"] }) + + // when + await ackMessages(teamRunId, "m1", [messageId], config) + await ackMessages(teamRunId, "m1", [messageId], config) + + // then + const inboxDir = getInboxDir(resolveBaseDir(config), teamRunId, "m1") + const inboxEntries = await readdir(inboxDir) + const processedEntries = await readdir(path.join(inboxDir, "processed")) + expect(inboxEntries).not.toContain(`${messageId}.json`) + expect(processedEntries).toContain(`${messageId}.json`) + }) +}) diff --git a/src/features/team-mode/team-mailbox/ack.ts b/src/features/team-mode/team-mailbox/ack.ts new file mode 100644 index 000000000..9428f7ae5 --- /dev/null +++ b/src/features/team-mode/team-mailbox/ack.ts @@ -0,0 +1,34 @@ +import { mkdir, rename } from "node:fs/promises" +import path from "node:path" + +import type { TeamModeConfig } from "../../../config/schema/team-mode" +import { getInboxDir, resolveBaseDir } from "../team-registry/paths" + +export async function ackMessages( + teamRunId: string, + memberName: string, + messageIds: string[], + config: TeamModeConfig, +): Promise { + const baseDir = resolveBaseDir(config) + const inboxDir = getInboxDir(baseDir, teamRunId, memberName) + const processedDir = path.join(inboxDir, "processed") + await mkdir(processedDir, { recursive: true, mode: 0o700 }) + + for (const messageId of messageIds) { + const messageFileName = `${messageId}.json` + const sourcePath = path.join(inboxDir, messageFileName) + const targetPath = path.join(processedDir, messageFileName) + + try { + await rename(sourcePath, targetPath) + } catch (error) { + const err = error as NodeJS.ErrnoException + if (err.code === "ENOENT") { + continue + } + + throw error + } + } +}