fix(agent-teams): close delete race and preserve parent-agent fallback
This commit is contained in:
committed by
YeonGyu-Kim
parent
1e2c10e7b0
commit
accb874155
@@ -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)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user