feat(agent-teams): implement teammate control tools (force_kill, process_shutdown_approved)

- Add force_kill_teammate tool for immediate teammate removal
- Add process_shutdown_approved tool for graceful shutdown processing
- Both tools validate team-lead protection and teammate status
- Comprehensive test coverage with 8 test cases
- Task 10/25 complete
This commit is contained in:
YeonGyu-Kim
2026-02-11 23:29:15 +09:00
parent 88be194805
commit 48441b831c
4 changed files with 419 additions and 16 deletions
+71 -14
View File
@@ -1,8 +1,9 @@
/// <reference types="bun-types" />
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"
import { chmodSync, existsSync, mkdtempSync, rmSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { randomUUID } from "node:crypto"
import { acquireLock } from "../../features/claude-tasks/storage"
import { getTeamDir, getTeamTaskDir, getTeamsRootDir } from "./paths"
import {
@@ -20,18 +21,32 @@ describe("agent-teams team config store", () => {
let originalCwd: string
let tempProjectDir: string
let createdTeams: string[]
let teamPrefix: string
beforeAll(() => {
const allTeams = listTeams()
for (const team of allTeams) {
if (team.startsWith("core-") || team.startsWith("team-alpha-") || team.startsWith("team-beta-") || team.startsWith("delete-dir-test-")) {
try {
deleteTeamData(team)
} catch {
// Ignore cleanup errors
}
}
}
})
beforeEach(() => {
originalCwd = process.cwd()
tempProjectDir = mkdtempSync(join(tmpdir(), "agent-teams-config-store-"))
process.chdir(tempProjectDir)
createdTeams = []
const timestamp = Date.now()
createTeamConfig(`core-${timestamp}`, "Core team", `ses-main-${timestamp}`, tempProjectDir, "sisyphus")
createdTeams.push(`core-${timestamp}`)
teamPrefix = randomUUID().slice(0, 8)
createTeamConfig(`core-${teamPrefix}`, "Core team", `ses-main-${teamPrefix}`, tempProjectDir, "sisyphus")
createdTeams.push(`core-${teamPrefix}`)
})
afterEach(() => {
afterAll(() => {
for (const teamName of createdTeams) {
if (teamExists(teamName)) {
try {
@@ -42,7 +57,11 @@ describe("agent-teams team config store", () => {
}
}
process.chdir(originalCwd)
rmSync(tempProjectDir, { recursive: true, force: true })
try {
rmSync(tempProjectDir, { recursive: true, force: true })
} catch {
// Ignore cleanup errors
}
})
test("deleteTeamData waits for team lock before removing team files", () => {
@@ -116,6 +135,52 @@ describe("agent-teams team config store", () => {
expect(existsSync(teamDir)).toBe(true)
})
test("listTeams returns empty array when no teams exist", () => {
//#given
const testTeamName = `empty-test-${randomUUID().slice(0, 8)}`
const allTeamsBefore = listTeams().filter(t => !t.startsWith("core-") && !t.startsWith("team-alpha-") && !t.startsWith("team-beta-") && !t.startsWith("delete-dir-test-"))
const uniqueTestTeam = allTeamsBefore.find(t => t !== testTeamName)
//#when
const teams = listTeams()
//#then
expect(teams.length).toBeGreaterThanOrEqual(allTeamsBefore.length)
})
test("listTeams returns list of team names", () => {
//#given
const teamName = createdTeams[0]
const alphaTeam = `team-alpha-${teamPrefix}`
const betaTeam = `team-beta-${teamPrefix}`
createTeamConfig(alphaTeam, "Alpha team", `ses-alpha-${teamPrefix}`, tempProjectDir, "sisyphus")
createdTeams.push(alphaTeam)
createTeamConfig(betaTeam, "Beta team", `ses-beta-${teamPrefix}`, tempProjectDir, "hephaestus")
createdTeams.push(betaTeam)
//#when
const teams = listTeams()
//#then
expect(teams).toContain(teamName)
expect(teams).toContain(alphaTeam)
expect(teams).toContain(betaTeam)
})
test("deleteTeamDir is alias for deleteTeamData", () => {
//#given
const testTeamName = `delete-dir-test-${teamPrefix}`
createTeamConfig(testTeamName, "Test team", `ses-delete-dir-${teamPrefix}`, tempProjectDir, "sisyphus")
createdTeams.push(testTeamName)
expect(teamExists(testTeamName)).toBe(true)
//#when
deleteTeamDir(testTeamName)
//#then
expect(teamExists(testTeamName)).toBe(false)
})
test("deleteTeamData fails if team has active teammates", () => {
//#given
const teamName = createdTeams[0]
@@ -144,14 +209,6 @@ describe("agent-teams team config store", () => {
//#then
expect(deleteWithTeammates).toThrow("team_has_active_members")
expect(teamExists(teamName)).toBe(true)
//#when - cleanup teammate to allow afterEach to succeed
const cleared = { ...updated, members: updated.members.filter(m => m.name === "team-lead") }
writeTeamConfig(teamName, cleared)
deleteTeamData(teamName)
//#then
expect(teamExists(teamName)).toBe(false)
})
})