fix(agent-teams): close delete race and preserve parent-agent fallback

This commit is contained in:
Nguyen Khac Trung Kien
2026-02-08 14:55:26 +07:00
committed by YeonGyu-Kim
parent 1e2c10e7b0
commit accb874155
5 changed files with 79 additions and 16 deletions

View File

@@ -5,7 +5,14 @@ import { tmpdir } from "node:os"
import { join } from "node:path"
import { acquireLock } from "../../features/claude-tasks/storage"
import { getTeamDir, getTeamTaskDir, getTeamsRootDir } from "./paths"
import { createTeamConfig, deleteTeamData, teamExists } from "./team-config-store"
import {
createTeamConfig,
deleteTeamData,
readTeamConfigOrThrow,
teamExists,
upsertTeammate,
writeTeamConfig,
} from "./team-config-store"
describe("agent-teams team config store", () => {
let originalCwd: string
@@ -31,15 +38,18 @@ describe("agent-teams team config store", () => {
const lock = acquireLock(getTeamDir("core"))
expect(lock.acquired).toBe(true)
//#when
const deleteWhileLocked = () => deleteTeamData("core")
try {
//#when
const deleteWhileLocked = () => deleteTeamData("core")
//#then
expect(deleteWhileLocked).toThrow("team_lock_unavailable")
expect(teamExists("core")).toBe(true)
//#then
expect(deleteWhileLocked).toThrow("team_lock_unavailable")
expect(teamExists("core")).toBe(true)
} finally {
//#when
lock.release()
}
//#when
lock.release()
deleteTeamData("core")
//#then
@@ -91,4 +101,41 @@ describe("agent-teams team config store", () => {
expect(existsSync(teamDir)).toBe(true)
})
test("deleteTeamData fails if team has active teammates", () => {
//#given
const config = readTeamConfigOrThrow("core")
const updated = upsertTeammate(config, {
agentId: "teammate@core",
name: "teammate",
agentType: "sisyphus",
category: "test",
model: "sisyphus",
prompt: "test prompt",
color: "#000000",
planModeRequired: false,
joinedAt: Date.now(),
cwd: process.cwd(),
subscriptions: [],
backendType: "native",
isActive: true,
sessionID: "ses-sub",
})
writeTeamConfig("core", updated)
//#when
const deleteWithTeammates = () => deleteTeamData("core")
//#then
expect(deleteWithTeammates).toThrow("team_has_active_members")
expect(teamExists("core")).toBe(true)
//#when - cleanup teammate to allow afterEach to succeed
const cleared = { ...updated, members: updated.members.filter(m => m.name === "team-lead") }
writeTeamConfig("core", cleared)
deleteTeamData("core")
//#then
expect(teamExists("core")).toBe(false)
})
})

View File

@@ -1,10 +1,5 @@
import { existsSync, rmSync } from "node:fs"
import {
acquireLock,
ensureDir,
readJsonSafe,
writeJsonAtomic,
} from "../../features/claude-tasks/storage"
import { acquireLock, ensureDir, readJsonSafe, writeJsonAtomic } from "../../features/claude-tasks/storage"
import {
getTeamConfigPath,
getTeamDir,
@@ -175,6 +170,15 @@ export function assignNextColor(config: TeamConfig): string {
export function deleteTeamData(teamName: string): void {
assertValidTeamName(teamName)
withTeamLock(teamName, () => {
const config = readJsonSafe(getTeamConfigPath(teamName), TeamConfigSchema)
if (!config) {
throw new Error("team_not_found")
}
if (listTeammates(config).length > 0) {
throw new Error("team_has_active_members")
}
withTeamTaskLock(teamName, () => {
const teamDir = getTeamDir(teamName)
const taskDir = getTeamTaskDir(teamName)

View File

@@ -21,4 +21,16 @@ describe("agent-teams teammate parent context", () => {
expect(parentToolContext.messageID).toBe("msg-main")
expect(parentToolContext.agent).toBe("sisyphus")
})
test("leaves agent undefined if missing in tool context", () => {
//#when
const parentToolContext = buildTeamParentToolContext({
sessionID: "ses-main",
messageID: "msg-main",
abort: new AbortController().signal,
})
//#then
expect(parentToolContext.agent).toBeUndefined()
})
})

View File

@@ -7,7 +7,7 @@ export function buildTeamParentToolContext(context: TeamToolContext): ToolContex
return {
sessionID: context.sessionID,
messageID: context.messageID,
agent: context.agent ?? "sisyphus",
agent: context.agent,
abort: context.abort ?? new AbortController().signal,
}
}

View File

@@ -26,7 +26,7 @@ export interface DelegateTaskArgs {
export interface ToolContextWithMetadata {
sessionID: string
messageID: string
agent: string
agent?: string
abort: AbortSignal
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void | Promise<void>
/**