diff --git a/src/tools/agent-teams/inbox-store.test.ts b/src/tools/agent-teams/inbox-store.test.ts new file mode 100644 index 00000000..9a419500 --- /dev/null +++ b/src/tools/agent-teams/inbox-store.test.ts @@ -0,0 +1,59 @@ +/// +import { afterEach, beforeEach, describe, expect, test } from "bun:test" +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { appendInboxMessage, ensureInbox, readInbox } from "./inbox-store" +import { getTeamInboxPath } from "./paths" + +describe("agent-teams inbox store", () => { + let originalCwd: string + let tempProjectDir: string + + beforeEach(() => { + originalCwd = process.cwd() + tempProjectDir = mkdtempSync(join(tmpdir(), "agent-teams-inbox-store-")) + process.chdir(tempProjectDir) + }) + + afterEach(() => { + process.chdir(originalCwd) + rmSync(tempProjectDir, { recursive: true, force: true }) + }) + + test("readInbox fails on malformed inbox JSON without overwriting file", () => { + //#given + ensureInbox("core", "team-lead") + const inboxPath = getTeamInboxPath("core", "team-lead") + writeFileSync(inboxPath, "{", "utf-8") + + //#when + const readMalformedInbox = () => readInbox("core", "team-lead", false, false) + + //#then + expect(readMalformedInbox).toThrow("team_inbox_parse_failed") + expect(readFileSync(inboxPath, "utf-8")).toBe("{") + }) + + test("appendInboxMessage fails on schema-invalid inbox JSON without overwriting file", () => { + //#given + ensureInbox("core", "team-lead") + const inboxPath = getTeamInboxPath("core", "team-lead") + writeFileSync(inboxPath, JSON.stringify({ invalid: true }), "utf-8") + + //#when + const appendIntoInvalidInbox = () => { + appendInboxMessage("core", "team-lead", { + from: "team-lead", + text: "hello", + timestamp: new Date().toISOString(), + read: false, + summary: "note", + }) + } + + //#then + expect(appendIntoInvalidInbox).toThrow("team_inbox_schema_invalid") + expect(readFileSync(inboxPath, "utf-8")).toBe(JSON.stringify({ invalid: true })) + }) +}) diff --git a/src/tools/agent-teams/inbox-store.ts b/src/tools/agent-teams/inbox-store.ts index 4b9fbfdb..3a6b1019 100644 --- a/src/tools/agent-teams/inbox-store.ts +++ b/src/tools/agent-teams/inbox-store.ts @@ -42,13 +42,20 @@ function withInboxLock(teamName: string, operation: () => T): T { } function parseInboxFile(content: string): TeamInboxMessage[] { + let parsed: unknown + try { - const parsed = JSON.parse(content) - const result = TeamInboxListSchema.safeParse(parsed) - return result.success ? result.data : [] + parsed = JSON.parse(content) } catch { - return [] + throw new Error("team_inbox_parse_failed") } + + const result = TeamInboxListSchema.safeParse(parsed) + if (!result.success) { + throw new Error("team_inbox_schema_invalid") + } + + return result.data } function readInboxMessages(teamName: string, agentName: string): TeamInboxMessage[] { diff --git a/src/tools/agent-teams/team-config-store.test.ts b/src/tools/agent-teams/team-config-store.test.ts index 254056ab..7b6a50e0 100644 --- a/src/tools/agent-teams/team-config-store.test.ts +++ b/src/tools/agent-teams/team-config-store.test.ts @@ -1,10 +1,10 @@ /// import { afterEach, beforeEach, describe, expect, test } from "bun:test" -import { mkdtempSync, readFileSync, rmSync } from "node:fs" +import { chmodSync, existsSync, mkdtempSync, rmSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" import { acquireLock } from "../../features/claude-tasks/storage" -import { getTeamDir, getTeamTaskDir } from "./paths" +import { getTeamDir, getTeamTaskDir, getTeamsRootDir } from "./paths" import { createTeamConfig, deleteTeamData, teamExists } from "./team-config-store" describe("agent-teams team config store", () => { @@ -69,21 +69,26 @@ describe("agent-teams team config store", () => { expect(teamExists("core")).toBe(false) }) - test("deleteTeamData removes task files before team files", () => { + test("deleteTeamData removes task files before deleting team directory", () => { //#given - const sourceUrl = new URL("./team-config-store.ts", import.meta.url) - const source = readFileSync(sourceUrl, "utf-8") - const deleteFnStart = source.indexOf("export function deleteTeamData") - const deleteFnSlice = deleteFnStart >= 0 ? source.slice(deleteFnStart, deleteFnStart + 700) : "" + const taskDir = getTeamTaskDir("core") + const teamDir = getTeamDir("core") + const teamsRootDir = getTeamsRootDir() + expect(existsSync(taskDir)).toBe(true) + expect(existsSync(teamDir)).toBe(true) //#when - const taskDeleteIndex = deleteFnSlice.indexOf("rmSync(taskDir") - const teamDeleteIndex = deleteFnSlice.indexOf("rmSync(teamDir") + chmodSync(teamsRootDir, 0o555) + try { + const deleteWithBlockedTeamParent = () => deleteTeamData("core") + expect(deleteWithBlockedTeamParent).toThrow() + } finally { + chmodSync(teamsRootDir, 0o755) + } //#then - expect(deleteFnStart).toBeGreaterThanOrEqual(0) - expect(taskDeleteIndex).toBeGreaterThanOrEqual(0) - expect(teamDeleteIndex).toBeGreaterThanOrEqual(0) - expect(taskDeleteIndex).toBeLessThan(teamDeleteIndex) + expect(existsSync(taskDir)).toBe(false) + expect(existsSync(teamDir)).toBe(true) }) + }) diff --git a/src/tools/agent-teams/teammate-parent-context.test.ts b/src/tools/agent-teams/teammate-parent-context.test.ts index 8ad11e28..6654d491 100644 --- a/src/tools/agent-teams/teammate-parent-context.test.ts +++ b/src/tools/agent-teams/teammate-parent-context.test.ts @@ -1,14 +1,24 @@ /// import { describe, expect, test } from "bun:test" -import { readFileSync } from "node:fs" +import { buildTeamParentToolContext } from "./teammate-parent-context" describe("agent-teams teammate parent context", () => { test("forwards incoming abort signal to parent context resolver", () => { //#given - const sourceUrl = new URL("./teammate-parent-context.ts", import.meta.url) - const source = readFileSync(sourceUrl, "utf-8") + const abortSignal = new AbortController().signal + + //#when + const parentToolContext = buildTeamParentToolContext({ + sessionID: "ses-main", + messageID: "msg-main", + agent: "sisyphus", + abort: abortSignal, + }) //#then - expect(source.includes("abort: context.abort ?? new AbortController().signal")).toBe(true) + expect(parentToolContext.abort).toBe(abortSignal) + expect(parentToolContext.sessionID).toBe("ses-main") + expect(parentToolContext.messageID).toBe("msg-main") + expect(parentToolContext.agent).toBe("sisyphus") }) }) diff --git a/src/tools/agent-teams/teammate-parent-context.ts b/src/tools/agent-teams/teammate-parent-context.ts index 59e5f63e..129e9ba1 100644 --- a/src/tools/agent-teams/teammate-parent-context.ts +++ b/src/tools/agent-teams/teammate-parent-context.ts @@ -3,11 +3,15 @@ import { resolveParentContext } from "../delegate-task/executor" import type { ToolContextWithMetadata } from "../delegate-task/types" import type { TeamToolContext } from "./types" -export function resolveTeamParentContext(context: TeamToolContext): ParentContext { - return resolveParentContext({ +export function buildTeamParentToolContext(context: TeamToolContext): ToolContextWithMetadata { + return { sessionID: context.sessionID, messageID: context.messageID, agent: context.agent ?? "sisyphus", abort: context.abort ?? new AbortController().signal, - } as ToolContextWithMetadata) + } +} + +export function resolveTeamParentContext(context: TeamToolContext): ParentContext { + return resolveParentContext(buildTeamParentToolContext(context)) }