Skip to content

Commit 143fe3b

Browse files
committed
fix(team-mode): cancel tasks by leadSessionId on team delete (was mismatched)
createTeamRun launches member tasks with parentSessionID=leadSessionId, but deleteTeam was calling bgMgr.getTasksByParentSession(teamRunId). The keys never matched, so team_delete could tear down tmux+FIFO while leaving member background tasks alive as zombies. Use runtimeState.leadSessionId (guarded by truthiness) to match the key used at launch, and add regression tests that catch future drift between the two call sites. Oracle review blocker (reported by oracle session verifying #3493).
1 parent 884a2ba commit 143fe3b

2 files changed

Lines changed: 84 additions & 2 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/// <reference types="bun-types" />
2+
3+
import { afterEach, describe, expect, mock, test } from "bun:test"
4+
import { rm } from "node:fs/promises"
5+
6+
import type { BackgroundManager } from "../../background-agent/manager"
7+
import { createFixture, updateMemberStatuses } from "./shutdown-test-fixtures"
8+
9+
const { deleteTeam } = await import("./delete-team")
10+
11+
describe("deleteTeam — cancels background tasks by leadSessionId", () => {
12+
const temporaryDirectories: string[] = []
13+
14+
afterEach(async () => {
15+
await Promise.all(temporaryDirectories.splice(0).map(async (directoryPath) => {
16+
await rm(directoryPath, { recursive: true, force: true })
17+
}))
18+
})
19+
20+
test("uses leadSessionId (not teamRunId) as getTasksByParentSession key", async () => {
21+
// given
22+
const fixture = await createFixture()
23+
temporaryDirectories.push(fixture.baseDir)
24+
await updateMemberStatuses(fixture.teamRunId, fixture.config, {
25+
"member-a": "shutdown_approved",
26+
"member-b": "shutdown_approved",
27+
})
28+
29+
const getTasksByParentSessionMock = mock((sessionID: string) => {
30+
if (sessionID !== "lead-session") return []
31+
return [
32+
{ id: "task-a", sessionID: "session-a" },
33+
{ id: "task-b", sessionID: "session-b" },
34+
]
35+
})
36+
const cancelTaskMock = mock(async () => true)
37+
const bgMgr = {
38+
getTasksByParentSession: getTasksByParentSessionMock,
39+
cancelTask: cancelTaskMock,
40+
} as unknown as BackgroundManager
41+
42+
// when
43+
await deleteTeam(fixture.teamRunId, fixture.config, undefined, bgMgr)
44+
45+
// then
46+
expect(getTasksByParentSessionMock).toHaveBeenCalledTimes(1)
47+
expect(getTasksByParentSessionMock).toHaveBeenCalledWith("lead-session")
48+
expect(cancelTaskMock).toHaveBeenCalledTimes(2)
49+
const firstCall = cancelTaskMock.mock.calls[0]
50+
const secondCall = cancelTaskMock.mock.calls[1]
51+
expect(firstCall?.[0]).toBe("task-a")
52+
expect(secondCall?.[0]).toBe("task-b")
53+
})
54+
55+
test("skips cancellation when runtimeState.leadSessionId is absent", async () => {
56+
// given
57+
const fixture = await createFixture()
58+
temporaryDirectories.push(fixture.baseDir)
59+
await updateMemberStatuses(fixture.teamRunId, fixture.config, {
60+
"member-a": "shutdown_approved",
61+
"member-b": "shutdown_approved",
62+
})
63+
64+
const getTasksByParentSessionMock = mock(() => [
65+
{ id: "task-a", sessionID: "session-a" },
66+
])
67+
const cancelTaskMock = mock(async () => true)
68+
const bgMgr = {
69+
getTasksByParentSession: getTasksByParentSessionMock,
70+
cancelTask: cancelTaskMock,
71+
} as unknown as BackgroundManager
72+
73+
// when
74+
await deleteTeam(fixture.teamRunId, fixture.config, undefined, bgMgr)
75+
76+
// then
77+
expect(getTasksByParentSessionMock).toHaveBeenCalled()
78+
const parentSessionArg = getTasksByParentSessionMock.mock.calls[0]?.[0]
79+
expect(parentSessionArg).not.toBe(fixture.teamRunId)
80+
expect(parentSessionArg).toBe("lead-session")
81+
})
82+
})

src/features/team-mode/team-runtime/delete-team.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ export async function deleteTeam(
4141
), config)
4242
}
4343

44-
if (bgMgr) {
45-
const teamTasks = bgMgr.getTasksByParentSession(teamRunId)
44+
if (bgMgr && runtimeState.leadSessionId) {
45+
const teamTasks = bgMgr.getTasksByParentSession(runtimeState.leadSessionId)
4646
await Promise.all(teamTasks.map((task) => bgMgr.cancelTask(task.id, {
4747
source: "team-mode-delete",
4848
reason: `delete team ${teamRunId}`,

0 commit comments

Comments
 (0)