| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- /* eslint-disable @typescript-eslint/no-unsafe-function-type */
- /* eslint-disable @typescript-eslint/no-explicit-any */
- import type { Socket } from "socket.io-client"
- import {
- type TaskLike,
- type ClineMessage,
- RooCodeEventName,
- TaskBridgeEventName,
- TaskBridgeCommandName,
- TaskSocketEvents,
- TaskStatus,
- } from "@roo-code/types"
- import { TaskChannel } from "../TaskChannel.js"
- describe("TaskChannel", () => {
- let mockSocket: Socket
- let taskChannel: TaskChannel
- let mockTask: TaskLike
- const instanceId = "test-instance-123"
- const taskId = "test-task-456"
- beforeEach(() => {
- // Create mock socket
- mockSocket = {
- emit: vi.fn(),
- on: vi.fn(),
- off: vi.fn(),
- disconnect: vi.fn(),
- } as unknown as Socket
- // Create mock task with event emitter functionality
- const listeners = new Map<string, Set<(...args: unknown[]) => unknown>>()
- mockTask = {
- taskId,
- taskStatus: TaskStatus.Running,
- taskAsk: undefined,
- metadata: {},
- on: vi.fn((event: string, listener: (...args: unknown[]) => unknown) => {
- if (!listeners.has(event)) {
- listeners.set(event, new Set())
- }
- listeners.get(event)!.add(listener)
- return mockTask
- }),
- off: vi.fn((event: string, listener: (...args: unknown[]) => unknown) => {
- const eventListeners = listeners.get(event)
- if (eventListeners) {
- eventListeners.delete(listener)
- if (eventListeners.size === 0) {
- listeners.delete(event)
- }
- }
- return mockTask
- }),
- approveAsk: vi.fn(),
- denyAsk: vi.fn(),
- submitUserMessage: vi.fn(),
- abortTask: vi.fn(),
- // Helper to trigger events in tests
- _triggerEvent: (event: string, ...args: any[]) => {
- const eventListeners = listeners.get(event)
- if (eventListeners) {
- eventListeners.forEach((listener) => listener(...args))
- }
- },
- _getListenerCount: (event: string) => {
- return listeners.get(event)?.size || 0
- },
- } as unknown as TaskLike & {
- _triggerEvent: (event: string, ...args: any[]) => void
- _getListenerCount: (event: string) => number
- }
- // Create task channel instance
- taskChannel = new TaskChannel(instanceId)
- })
- afterEach(() => {
- vi.clearAllMocks()
- })
- describe("Event Mapping Refactoring", () => {
- it("should use the unified event mapping approach", () => {
- // Access the private eventMapping through type assertion
- const channel = taskChannel as any
- // Verify eventMapping exists and has the correct structure
- expect(channel.eventMapping).toBeDefined()
- expect(Array.isArray(channel.eventMapping)).toBe(true)
- expect(channel.eventMapping.length).toBe(3)
- // Verify each mapping has the required properties
- channel.eventMapping.forEach((mapping: any) => {
- expect(mapping).toHaveProperty("from")
- expect(mapping).toHaveProperty("to")
- expect(mapping).toHaveProperty("createPayload")
- expect(typeof mapping.createPayload).toBe("function")
- })
- // Verify specific mappings
- expect(channel.eventMapping[0].from).toBe(RooCodeEventName.Message)
- expect(channel.eventMapping[0].to).toBe(TaskBridgeEventName.Message)
- expect(channel.eventMapping[1].from).toBe(RooCodeEventName.TaskModeSwitched)
- expect(channel.eventMapping[1].to).toBe(TaskBridgeEventName.TaskModeSwitched)
- expect(channel.eventMapping[2].from).toBe(RooCodeEventName.TaskInteractive)
- expect(channel.eventMapping[2].to).toBe(TaskBridgeEventName.TaskInteractive)
- })
- it("should setup listeners using the event mapping", async () => {
- // Mock the publish method to simulate successful subscription
- const channel = taskChannel as any
- channel.publish = vi.fn((event: string, data: any, callback?: Function) => {
- if (event === TaskSocketEvents.JOIN && callback) {
- // Simulate successful join response
- callback({ success: true })
- }
- return true
- })
- // Connect and subscribe to task
- await taskChannel.onConnect(mockSocket)
- await channel.subscribeToTask(mockTask, mockSocket)
- // Wait for async operations
- await new Promise((resolve) => setTimeout(resolve, 0))
- // Verify listeners were registered for all mapped events
- const task = mockTask as any
- expect(task._getListenerCount(RooCodeEventName.Message)).toBe(1)
- expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(1)
- expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(1)
- })
- it("should correctly transform Message event payloads", async () => {
- // Setup channel with task
- const channel = taskChannel as any
- let publishCalls: any[] = []
- channel.publish = vi.fn((event: string, data: any, callback?: Function) => {
- publishCalls.push({ event, data })
- if (event === TaskSocketEvents.JOIN && callback) {
- callback({ success: true })
- }
- return true
- })
- await taskChannel.onConnect(mockSocket)
- await channel.subscribeToTask(mockTask, mockSocket)
- await new Promise((resolve) => setTimeout(resolve, 0))
- // Clear previous calls
- publishCalls = []
- // Trigger Message event
- const messageData = {
- action: "test-action",
- message: { type: "say", text: "Hello" } as ClineMessage,
- }
- ;(mockTask as any)._triggerEvent(RooCodeEventName.Message, messageData)
- // Verify the event was published with correct payload
- expect(publishCalls.length).toBe(1)
- expect(publishCalls[0]).toEqual({
- event: TaskSocketEvents.EVENT,
- data: {
- type: TaskBridgeEventName.Message,
- taskId: taskId,
- action: messageData.action,
- message: messageData.message,
- },
- })
- })
- it("should correctly transform TaskModeSwitched event payloads", async () => {
- // Setup channel with task
- const channel = taskChannel as any
- let publishCalls: any[] = []
- channel.publish = vi.fn((event: string, data: any, callback?: Function) => {
- publishCalls.push({ event, data })
- if (event === TaskSocketEvents.JOIN && callback) {
- callback({ success: true })
- }
- return true
- })
- await taskChannel.onConnect(mockSocket)
- await channel.subscribeToTask(mockTask, mockSocket)
- await new Promise((resolve) => setTimeout(resolve, 0))
- // Clear previous calls
- publishCalls = []
- // Trigger TaskModeSwitched event
- const mode = "architect"
- ;(mockTask as any)._triggerEvent(RooCodeEventName.TaskModeSwitched, mode)
- // Verify the event was published with correct payload
- expect(publishCalls.length).toBe(1)
- expect(publishCalls[0]).toEqual({
- event: TaskSocketEvents.EVENT,
- data: {
- type: TaskBridgeEventName.TaskModeSwitched,
- taskId: taskId,
- mode: mode,
- },
- })
- })
- it("should correctly transform TaskInteractive event payloads", async () => {
- // Setup channel with task
- const channel = taskChannel as any
- let publishCalls: any[] = []
- channel.publish = vi.fn((event: string, data: any, callback?: Function) => {
- publishCalls.push({ event, data })
- if (event === TaskSocketEvents.JOIN && callback) {
- callback({ success: true })
- }
- return true
- })
- await taskChannel.onConnect(mockSocket)
- await channel.subscribeToTask(mockTask, mockSocket)
- await new Promise((resolve) => setTimeout(resolve, 0))
- // Clear previous calls
- publishCalls = []
- // Trigger TaskInteractive event
- ;(mockTask as any)._triggerEvent(RooCodeEventName.TaskInteractive, taskId)
- // Verify the event was published with correct payload
- expect(publishCalls.length).toBe(1)
- expect(publishCalls[0]).toEqual({
- event: TaskSocketEvents.EVENT,
- data: {
- type: TaskBridgeEventName.TaskInteractive,
- taskId: taskId,
- },
- })
- })
- it("should properly clean up listeners using event mapping", async () => {
- // Setup channel with task
- const channel = taskChannel as any
- channel.publish = vi.fn((event: string, data: any, callback?: Function) => {
- if (event === TaskSocketEvents.JOIN && callback) {
- callback({ success: true })
- }
- if (event === TaskSocketEvents.LEAVE && callback) {
- callback({ success: true })
- }
- return true
- })
- await taskChannel.onConnect(mockSocket)
- await channel.subscribeToTask(mockTask, mockSocket)
- await new Promise((resolve) => setTimeout(resolve, 0))
- // Verify listeners are registered
- const task = mockTask as any
- expect(task._getListenerCount(RooCodeEventName.Message)).toBe(1)
- expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(1)
- expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(1)
- // Clean up
- await taskChannel.cleanup(mockSocket)
- // Verify all listeners were removed
- expect(task._getListenerCount(RooCodeEventName.Message)).toBe(0)
- expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(0)
- expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(0)
- })
- it("should handle duplicate listener prevention", async () => {
- // Setup channel with task
- await taskChannel.onConnect(mockSocket)
- // Subscribe to the same task twice
- const channel = taskChannel as any
- channel.subscribedTasks.set(taskId, mockTask)
- channel.setupTaskListeners(mockTask)
- // Try to setup listeners again (should remove old ones first)
- const warnSpy = vi.spyOn(console, "warn")
- channel.setupTaskListeners(mockTask)
- // Verify warning was logged
- expect(warnSpy).toHaveBeenCalledWith(
- `[TaskChannel] Listeners already exist for task, removing old listeners for ${taskId}`,
- )
- // Verify only one set of listeners exists
- const task = mockTask as any
- expect(task._getListenerCount(RooCodeEventName.Message)).toBe(1)
- expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(1)
- expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(1)
- warnSpy.mockRestore()
- })
- })
- describe("Command Handling", () => {
- beforeEach(async () => {
- // Setup channel with a subscribed task
- await taskChannel.onConnect(mockSocket)
- const channel = taskChannel as any
- channel.subscribedTasks.set(taskId, mockTask)
- })
- it("should handle Message command", () => {
- const command = {
- type: TaskBridgeCommandName.Message,
- taskId,
- timestamp: Date.now(),
- payload: {
- text: "Hello, world!",
- images: ["image1.png"],
- },
- }
- taskChannel.handleCommand(command)
- expect(mockTask.submitUserMessage).toHaveBeenCalledWith(command.payload.text, command.payload.images)
- })
- it("should handle ApproveAsk command", () => {
- const command = {
- type: TaskBridgeCommandName.ApproveAsk,
- taskId,
- timestamp: Date.now(),
- payload: {
- text: "Approved",
- },
- }
- taskChannel.handleCommand(command)
- expect(mockTask.approveAsk).toHaveBeenCalledWith(command.payload)
- })
- it("should handle DenyAsk command", () => {
- const command = {
- type: TaskBridgeCommandName.DenyAsk,
- taskId,
- timestamp: Date.now(),
- payload: {
- text: "Denied",
- },
- }
- taskChannel.handleCommand(command)
- expect(mockTask.denyAsk).toHaveBeenCalledWith(command.payload)
- })
- it("should log error for unknown task", () => {
- const errorSpy = vi.spyOn(console, "error")
- const command = {
- type: TaskBridgeCommandName.Message,
- taskId: "unknown-task",
- timestamp: Date.now(),
- payload: {
- text: "Hello",
- },
- }
- taskChannel.handleCommand(command)
- expect(errorSpy).toHaveBeenCalledWith(`[TaskChannel] Unable to find task unknown-task`)
- errorSpy.mockRestore()
- })
- })
- })
|