Skip to content

Commit a988451

Browse files
committed
refactor: removeAllListeners on dispose
1 parent dba4454 commit a988451

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed

src/core/task/Task.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
12981298
public dispose(): void {
12991299
console.log(`[Task] disposing task ${this.taskId}.${this.instanceId}`)
13001300

1301+
// Remove all event listeners to prevent memory leaks
1302+
try {
1303+
this.removeAllListeners()
1304+
} catch (error) {
1305+
console.error("Error removing event listeners:", error)
1306+
}
1307+
13011308
// Stop waiting for child task completion.
13021309
if (this.pauseInterval) {
13031310
clearInterval(this.pauseInterval)
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { Task } from "../Task"
2+
import { ClineProvider } from "../../webview/ClineProvider"
3+
import { ProviderSettings } from "@roo-code/types"
4+
import { vi, describe, test, expect, beforeEach, afterEach } from "vitest"
5+
6+
// Mock dependencies
7+
vi.mock("../../webview/ClineProvider")
8+
vi.mock("../../../integrations/terminal/TerminalRegistry", () => ({
9+
TerminalRegistry: {
10+
releaseTerminalsForTask: vi.fn(),
11+
},
12+
}))
13+
vi.mock("../../ignore/RooIgnoreController")
14+
vi.mock("../../protect/RooProtectedController")
15+
vi.mock("../../context-tracking/FileContextTracker")
16+
vi.mock("../../../services/browser/UrlContentFetcher")
17+
vi.mock("../../../services/browser/BrowserSession")
18+
vi.mock("../../../integrations/editor/DiffViewProvider")
19+
vi.mock("../../tools/ToolRepetitionDetector")
20+
vi.mock("../../../api", () => ({
21+
buildApiHandler: vi.fn(() => ({
22+
getModel: () => ({ info: {}, id: "test-model" }),
23+
})),
24+
}))
25+
vi.mock("./AutoApprovalHandler")
26+
27+
// Mock TelemetryService
28+
vi.mock("@roo-code/telemetry", () => ({
29+
TelemetryService: {
30+
instance: {
31+
captureTaskCreated: vi.fn(),
32+
captureTaskRestarted: vi.fn(),
33+
},
34+
},
35+
}))
36+
37+
describe("Task dispose method", () => {
38+
let mockProvider: any
39+
let mockApiConfiguration: ProviderSettings
40+
let task: Task
41+
42+
beforeEach(() => {
43+
// Reset all mocks
44+
vi.clearAllMocks()
45+
46+
// Mock provider
47+
mockProvider = {
48+
context: {
49+
globalStorageUri: { fsPath: "/test/path" },
50+
},
51+
getState: vi.fn().mockResolvedValue({ mode: "code" }),
52+
log: vi.fn(),
53+
}
54+
55+
// Mock API configuration
56+
mockApiConfiguration = {
57+
apiProvider: "anthropic",
58+
apiKey: "test-key",
59+
} as ProviderSettings
60+
61+
// Create task instance without starting it
62+
task = new Task({
63+
provider: mockProvider as ClineProvider,
64+
apiConfiguration: mockApiConfiguration,
65+
startTask: false,
66+
})
67+
})
68+
69+
afterEach(() => {
70+
// Clean up
71+
if (task && !task.abort) {
72+
task.dispose()
73+
}
74+
})
75+
76+
test("should remove all event listeners when dispose is called", () => {
77+
// Add some event listeners using type assertion to bypass strict typing for testing
78+
const listener1 = vi.fn(() => {})
79+
const listener2 = vi.fn(() => {})
80+
const listener3 = vi.fn((taskId: string) => {})
81+
82+
// Use type assertion to bypass strict event typing for testing
83+
;(task as any).on("TaskStarted", listener1)
84+
;(task as any).on("TaskAborted", listener2)
85+
;(task as any).on("TaskIdle", listener3)
86+
87+
// Verify listeners are added
88+
expect(task.listenerCount("TaskStarted")).toBe(1)
89+
expect(task.listenerCount("TaskAborted")).toBe(1)
90+
expect(task.listenerCount("TaskIdle")).toBe(1)
91+
92+
// Spy on removeAllListeners method
93+
const removeAllListenersSpy = vi.spyOn(task, "removeAllListeners")
94+
95+
// Call dispose
96+
task.dispose()
97+
98+
// Verify removeAllListeners was called
99+
expect(removeAllListenersSpy).toHaveBeenCalledOnce()
100+
101+
// Verify all listeners are removed
102+
expect(task.listenerCount("TaskStarted")).toBe(0)
103+
expect(task.listenerCount("TaskAborted")).toBe(0)
104+
expect(task.listenerCount("TaskIdle")).toBe(0)
105+
})
106+
107+
test("should handle errors when removing event listeners", () => {
108+
// Mock removeAllListeners to throw an error
109+
const originalRemoveAllListeners = task.removeAllListeners
110+
task.removeAllListeners = vi.fn(() => {
111+
throw new Error("Test error")
112+
})
113+
114+
// Spy on console.error
115+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
116+
117+
// Call dispose - should not throw
118+
expect(() => task.dispose()).not.toThrow()
119+
120+
// Verify error was logged
121+
expect(consoleErrorSpy).toHaveBeenCalledWith("Error removing event listeners:", expect.any(Error))
122+
123+
// Restore
124+
task.removeAllListeners = originalRemoveAllListeners
125+
consoleErrorSpy.mockRestore()
126+
})
127+
128+
test("should clean up all resources in correct order", () => {
129+
const removeAllListenersSpy = vi.spyOn(task, "removeAllListeners")
130+
const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {})
131+
132+
// Call dispose
133+
task.dispose()
134+
135+
// Verify dispose was called and logged
136+
expect(consoleLogSpy).toHaveBeenCalledWith(
137+
expect.stringContaining(`[Task] disposing task ${task.taskId}.${task.instanceId}`),
138+
)
139+
140+
// Verify removeAllListeners was called first (before other cleanup)
141+
expect(removeAllListenersSpy).toHaveBeenCalledOnce()
142+
143+
// Clean up
144+
consoleLogSpy.mockRestore()
145+
})
146+
147+
test("should prevent memory leaks by removing listeners before other cleanup", () => {
148+
// Add multiple listeners of different types using type assertion for testing
149+
const listeners = {
150+
TaskStarted: vi.fn(() => {}),
151+
TaskAborted: vi.fn(() => {}),
152+
TaskIdle: vi.fn((taskId: string) => {}),
153+
TaskActive: vi.fn((taskId: string) => {}),
154+
TaskAskResponded: vi.fn(() => {}),
155+
Message: vi.fn((data: { action: "created" | "updated"; message: any }) => {}),
156+
TaskTokenUsageUpdated: vi.fn((taskId: string, tokenUsage: any) => {}),
157+
TaskToolFailed: vi.fn((taskId: string, tool: any, error: string) => {}),
158+
TaskUnpaused: vi.fn(() => {}),
159+
}
160+
161+
// Add all listeners using type assertion to bypass strict typing for testing
162+
const taskAny = task as any
163+
taskAny.on("TaskStarted", listeners.TaskStarted)
164+
taskAny.on("TaskAborted", listeners.TaskAborted)
165+
taskAny.on("TaskIdle", listeners.TaskIdle)
166+
taskAny.on("TaskActive", listeners.TaskActive)
167+
taskAny.on("TaskAskResponded", listeners.TaskAskResponded)
168+
taskAny.on("Message", listeners.Message)
169+
taskAny.on("TaskTokenUsageUpdated", listeners.TaskTokenUsageUpdated)
170+
taskAny.on("TaskToolFailed", listeners.TaskToolFailed)
171+
taskAny.on("TaskUnpaused", listeners.TaskUnpaused)
172+
173+
// Verify all listeners are added
174+
expect(task.listenerCount("TaskStarted")).toBe(1)
175+
expect(task.listenerCount("TaskAborted")).toBe(1)
176+
expect(task.listenerCount("TaskIdle")).toBe(1)
177+
expect(task.listenerCount("TaskActive")).toBe(1)
178+
expect(task.listenerCount("TaskAskResponded")).toBe(1)
179+
expect(task.listenerCount("Message")).toBe(1)
180+
expect(task.listenerCount("TaskTokenUsageUpdated")).toBe(1)
181+
expect(task.listenerCount("TaskToolFailed")).toBe(1)
182+
expect(task.listenerCount("TaskUnpaused")).toBe(1)
183+
184+
// Call dispose
185+
task.dispose()
186+
187+
// Verify all listeners are removed
188+
expect(task.listenerCount("TaskStarted")).toBe(0)
189+
expect(task.listenerCount("TaskAborted")).toBe(0)
190+
expect(task.listenerCount("TaskIdle")).toBe(0)
191+
expect(task.listenerCount("TaskActive")).toBe(0)
192+
expect(task.listenerCount("TaskAskResponded")).toBe(0)
193+
expect(task.listenerCount("Message")).toBe(0)
194+
expect(task.listenerCount("TaskTokenUsageUpdated")).toBe(0)
195+
expect(task.listenerCount("TaskToolFailed")).toBe(0)
196+
expect(task.listenerCount("TaskUnpaused")).toBe(0)
197+
198+
// Verify total listener count is 0
199+
expect(task.eventNames().length).toBe(0)
200+
})
201+
})

0 commit comments

Comments
 (0)