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))
}