TaskChannel.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. /* eslint-disable @typescript-eslint/no-unsafe-function-type */
  2. /* eslint-disable @typescript-eslint/no-explicit-any */
  3. import type { Socket } from "socket.io-client"
  4. import {
  5. type TaskLike,
  6. type ClineMessage,
  7. RooCodeEventName,
  8. TaskBridgeEventName,
  9. TaskBridgeCommandName,
  10. TaskSocketEvents,
  11. TaskStatus,
  12. } from "@roo-code/types"
  13. import { TaskChannel } from "../TaskChannel.js"
  14. describe("TaskChannel", () => {
  15. let mockSocket: Socket
  16. let taskChannel: TaskChannel
  17. let mockTask: TaskLike
  18. const instanceId = "test-instance-123"
  19. const taskId = "test-task-456"
  20. beforeEach(() => {
  21. // Create mock socket
  22. mockSocket = {
  23. emit: vi.fn(),
  24. on: vi.fn(),
  25. off: vi.fn(),
  26. disconnect: vi.fn(),
  27. } as unknown as Socket
  28. // Create mock task with event emitter functionality
  29. const listeners = new Map<string, Set<(...args: unknown[]) => unknown>>()
  30. mockTask = {
  31. taskId,
  32. taskStatus: TaskStatus.Running,
  33. taskAsk: undefined,
  34. metadata: {},
  35. on: vi.fn((event: string, listener: (...args: unknown[]) => unknown) => {
  36. if (!listeners.has(event)) {
  37. listeners.set(event, new Set())
  38. }
  39. listeners.get(event)!.add(listener)
  40. return mockTask
  41. }),
  42. off: vi.fn((event: string, listener: (...args: unknown[]) => unknown) => {
  43. const eventListeners = listeners.get(event)
  44. if (eventListeners) {
  45. eventListeners.delete(listener)
  46. if (eventListeners.size === 0) {
  47. listeners.delete(event)
  48. }
  49. }
  50. return mockTask
  51. }),
  52. approveAsk: vi.fn(),
  53. denyAsk: vi.fn(),
  54. submitUserMessage: vi.fn(),
  55. abortTask: vi.fn(),
  56. // Helper to trigger events in tests
  57. _triggerEvent: (event: string, ...args: any[]) => {
  58. const eventListeners = listeners.get(event)
  59. if (eventListeners) {
  60. eventListeners.forEach((listener) => listener(...args))
  61. }
  62. },
  63. _getListenerCount: (event: string) => {
  64. return listeners.get(event)?.size || 0
  65. },
  66. } as unknown as TaskLike & {
  67. _triggerEvent: (event: string, ...args: any[]) => void
  68. _getListenerCount: (event: string) => number
  69. }
  70. // Create task channel instance
  71. taskChannel = new TaskChannel(instanceId)
  72. })
  73. afterEach(() => {
  74. vi.clearAllMocks()
  75. })
  76. describe("Event Mapping Refactoring", () => {
  77. it("should use the unified event mapping approach", () => {
  78. // Access the private eventMapping through type assertion
  79. const channel = taskChannel as any
  80. // Verify eventMapping exists and has the correct structure
  81. expect(channel.eventMapping).toBeDefined()
  82. expect(Array.isArray(channel.eventMapping)).toBe(true)
  83. expect(channel.eventMapping.length).toBe(3)
  84. // Verify each mapping has the required properties
  85. channel.eventMapping.forEach((mapping: any) => {
  86. expect(mapping).toHaveProperty("from")
  87. expect(mapping).toHaveProperty("to")
  88. expect(mapping).toHaveProperty("createPayload")
  89. expect(typeof mapping.createPayload).toBe("function")
  90. })
  91. // Verify specific mappings
  92. expect(channel.eventMapping[0].from).toBe(RooCodeEventName.Message)
  93. expect(channel.eventMapping[0].to).toBe(TaskBridgeEventName.Message)
  94. expect(channel.eventMapping[1].from).toBe(RooCodeEventName.TaskModeSwitched)
  95. expect(channel.eventMapping[1].to).toBe(TaskBridgeEventName.TaskModeSwitched)
  96. expect(channel.eventMapping[2].from).toBe(RooCodeEventName.TaskInteractive)
  97. expect(channel.eventMapping[2].to).toBe(TaskBridgeEventName.TaskInteractive)
  98. })
  99. it("should setup listeners using the event mapping", async () => {
  100. // Mock the publish method to simulate successful subscription
  101. const channel = taskChannel as any
  102. channel.publish = vi.fn((event: string, data: any, callback?: Function) => {
  103. if (event === TaskSocketEvents.JOIN && callback) {
  104. // Simulate successful join response
  105. callback({ success: true })
  106. }
  107. return true
  108. })
  109. // Connect and subscribe to task
  110. await taskChannel.onConnect(mockSocket)
  111. await channel.subscribeToTask(mockTask, mockSocket)
  112. // Wait for async operations
  113. await new Promise((resolve) => setTimeout(resolve, 0))
  114. // Verify listeners were registered for all mapped events
  115. const task = mockTask as any
  116. expect(task._getListenerCount(RooCodeEventName.Message)).toBe(1)
  117. expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(1)
  118. expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(1)
  119. })
  120. it("should correctly transform Message event payloads", async () => {
  121. // Setup channel with task
  122. const channel = taskChannel as any
  123. let publishCalls: any[] = []
  124. channel.publish = vi.fn((event: string, data: any, callback?: Function) => {
  125. publishCalls.push({ event, data })
  126. if (event === TaskSocketEvents.JOIN && callback) {
  127. callback({ success: true })
  128. }
  129. return true
  130. })
  131. await taskChannel.onConnect(mockSocket)
  132. await channel.subscribeToTask(mockTask, mockSocket)
  133. await new Promise((resolve) => setTimeout(resolve, 0))
  134. // Clear previous calls
  135. publishCalls = []
  136. // Trigger Message event
  137. const messageData = {
  138. action: "test-action",
  139. message: { type: "say", text: "Hello" } as ClineMessage,
  140. }
  141. ;(mockTask as any)._triggerEvent(RooCodeEventName.Message, messageData)
  142. // Verify the event was published with correct payload
  143. expect(publishCalls.length).toBe(1)
  144. expect(publishCalls[0]).toEqual({
  145. event: TaskSocketEvents.EVENT,
  146. data: {
  147. type: TaskBridgeEventName.Message,
  148. taskId: taskId,
  149. action: messageData.action,
  150. message: messageData.message,
  151. },
  152. })
  153. })
  154. it("should correctly transform TaskModeSwitched event payloads", async () => {
  155. // Setup channel with task
  156. const channel = taskChannel as any
  157. let publishCalls: any[] = []
  158. channel.publish = vi.fn((event: string, data: any, callback?: Function) => {
  159. publishCalls.push({ event, data })
  160. if (event === TaskSocketEvents.JOIN && callback) {
  161. callback({ success: true })
  162. }
  163. return true
  164. })
  165. await taskChannel.onConnect(mockSocket)
  166. await channel.subscribeToTask(mockTask, mockSocket)
  167. await new Promise((resolve) => setTimeout(resolve, 0))
  168. // Clear previous calls
  169. publishCalls = []
  170. // Trigger TaskModeSwitched event
  171. const mode = "architect"
  172. ;(mockTask as any)._triggerEvent(RooCodeEventName.TaskModeSwitched, mode)
  173. // Verify the event was published with correct payload
  174. expect(publishCalls.length).toBe(1)
  175. expect(publishCalls[0]).toEqual({
  176. event: TaskSocketEvents.EVENT,
  177. data: {
  178. type: TaskBridgeEventName.TaskModeSwitched,
  179. taskId: taskId,
  180. mode: mode,
  181. },
  182. })
  183. })
  184. it("should correctly transform TaskInteractive event payloads", async () => {
  185. // Setup channel with task
  186. const channel = taskChannel as any
  187. let publishCalls: any[] = []
  188. channel.publish = vi.fn((event: string, data: any, callback?: Function) => {
  189. publishCalls.push({ event, data })
  190. if (event === TaskSocketEvents.JOIN && callback) {
  191. callback({ success: true })
  192. }
  193. return true
  194. })
  195. await taskChannel.onConnect(mockSocket)
  196. await channel.subscribeToTask(mockTask, mockSocket)
  197. await new Promise((resolve) => setTimeout(resolve, 0))
  198. // Clear previous calls
  199. publishCalls = []
  200. // Trigger TaskInteractive event
  201. ;(mockTask as any)._triggerEvent(RooCodeEventName.TaskInteractive, taskId)
  202. // Verify the event was published with correct payload
  203. expect(publishCalls.length).toBe(1)
  204. expect(publishCalls[0]).toEqual({
  205. event: TaskSocketEvents.EVENT,
  206. data: {
  207. type: TaskBridgeEventName.TaskInteractive,
  208. taskId: taskId,
  209. },
  210. })
  211. })
  212. it("should properly clean up listeners using event mapping", async () => {
  213. // Setup channel with task
  214. const channel = taskChannel as any
  215. channel.publish = vi.fn((event: string, data: any, callback?: Function) => {
  216. if (event === TaskSocketEvents.JOIN && callback) {
  217. callback({ success: true })
  218. }
  219. if (event === TaskSocketEvents.LEAVE && callback) {
  220. callback({ success: true })
  221. }
  222. return true
  223. })
  224. await taskChannel.onConnect(mockSocket)
  225. await channel.subscribeToTask(mockTask, mockSocket)
  226. await new Promise((resolve) => setTimeout(resolve, 0))
  227. // Verify listeners are registered
  228. const task = mockTask as any
  229. expect(task._getListenerCount(RooCodeEventName.Message)).toBe(1)
  230. expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(1)
  231. expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(1)
  232. // Clean up
  233. await taskChannel.cleanup(mockSocket)
  234. // Verify all listeners were removed
  235. expect(task._getListenerCount(RooCodeEventName.Message)).toBe(0)
  236. expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(0)
  237. expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(0)
  238. })
  239. it("should handle duplicate listener prevention", async () => {
  240. // Setup channel with task
  241. await taskChannel.onConnect(mockSocket)
  242. // Subscribe to the same task twice
  243. const channel = taskChannel as any
  244. channel.subscribedTasks.set(taskId, mockTask)
  245. channel.setupTaskListeners(mockTask)
  246. // Try to setup listeners again (should remove old ones first)
  247. const warnSpy = vi.spyOn(console, "warn")
  248. channel.setupTaskListeners(mockTask)
  249. // Verify warning was logged
  250. expect(warnSpy).toHaveBeenCalledWith(
  251. `[TaskChannel] Listeners already exist for task, removing old listeners for ${taskId}`,
  252. )
  253. // Verify only one set of listeners exists
  254. const task = mockTask as any
  255. expect(task._getListenerCount(RooCodeEventName.Message)).toBe(1)
  256. expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(1)
  257. expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(1)
  258. warnSpy.mockRestore()
  259. })
  260. })
  261. describe("Command Handling", () => {
  262. beforeEach(async () => {
  263. // Setup channel with a subscribed task
  264. await taskChannel.onConnect(mockSocket)
  265. const channel = taskChannel as any
  266. channel.subscribedTasks.set(taskId, mockTask)
  267. })
  268. it("should handle Message command", () => {
  269. const command = {
  270. type: TaskBridgeCommandName.Message,
  271. taskId,
  272. timestamp: Date.now(),
  273. payload: {
  274. text: "Hello, world!",
  275. images: ["image1.png"],
  276. },
  277. }
  278. taskChannel.handleCommand(command)
  279. expect(mockTask.submitUserMessage).toHaveBeenCalledWith(command.payload.text, command.payload.images)
  280. })
  281. it("should handle ApproveAsk command", () => {
  282. const command = {
  283. type: TaskBridgeCommandName.ApproveAsk,
  284. taskId,
  285. timestamp: Date.now(),
  286. payload: {
  287. text: "Approved",
  288. },
  289. }
  290. taskChannel.handleCommand(command)
  291. expect(mockTask.approveAsk).toHaveBeenCalledWith(command.payload)
  292. })
  293. it("should handle DenyAsk command", () => {
  294. const command = {
  295. type: TaskBridgeCommandName.DenyAsk,
  296. taskId,
  297. timestamp: Date.now(),
  298. payload: {
  299. text: "Denied",
  300. },
  301. }
  302. taskChannel.handleCommand(command)
  303. expect(mockTask.denyAsk).toHaveBeenCalledWith(command.payload)
  304. })
  305. it("should log error for unknown task", () => {
  306. const errorSpy = vi.spyOn(console, "error")
  307. const command = {
  308. type: TaskBridgeCommandName.Message,
  309. taskId: "unknown-task",
  310. timestamp: Date.now(),
  311. payload: {
  312. text: "Hello",
  313. },
  314. }
  315. taskChannel.handleCommand(command)
  316. expect(errorSpy).toHaveBeenCalledWith(`[TaskChannel] Unable to find task unknown-task`)
  317. errorSpy.mockRestore()
  318. })
  319. })
  320. })