Files
archon/archon-ui-main/test/services/socketIOService.test.ts
sean-eskerium 1b5196d70f - Fix the threading service to properly handle rate limiting.
- Fix the clipboard functionality to work on non local hosts and https
- Improvements in sockets on front-end and backend. Storing session in local browser storage for reconnect. Logic to prevent socket echos coausing rerender and performance issues.
- Fixes and udpates to re-ordering logic in adding a new task, reordering items on the task table.
- Allowing assignee to not be hardcoded enum.
- Fix to Document Version Control (Improvements still needed in the Milkdown editor conversion to store in the docs.
- Adding types to remove [any] typescript issues.
2025-08-20 02:28:02 -04:00

195 lines
6.9 KiB
TypeScript

import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
import { io, Socket } from 'socket.io-client'
// Mock socket.io-client
vi.mock('socket.io-client', () => ({
io: vi.fn(() => ({
on: vi.fn(),
off: vi.fn(),
emit: vi.fn(),
disconnect: vi.fn(),
connect: vi.fn(),
connected: true,
id: 'test-socket-id'
}))
}))
describe('socketIOService - Shared Instance Pattern', () => {
let socketIOService: any
let knowledgeSocketIO: any
let taskUpdateSocketIO: any
let projectListSocketIO: any
beforeEach(async () => {
// Reset all mocks
vi.resetAllMocks()
vi.resetModules()
// Import fresh instances
const module = await import('../../src/services/socketIOService')
socketIOService = module
knowledgeSocketIO = module.knowledgeSocketIO
taskUpdateSocketIO = module.taskUpdateSocketIO
projectListSocketIO = module.projectListSocketIO
})
afterEach(() => {
vi.clearAllMocks()
})
test('creates only a single shared socket instance', () => {
// All exported instances should be the same object
expect(knowledgeSocketIO).toBe(taskUpdateSocketIO)
expect(taskUpdateSocketIO).toBe(projectListSocketIO)
expect(knowledgeSocketIO).toBe(projectListSocketIO)
})
test('socket.io is called only once despite multiple exports', () => {
// The io function should only be called once to create the shared instance
expect(io).toHaveBeenCalledTimes(1)
})
test('all services share the same socket connection', () => {
// Get the internal socket from each service
const knowledgeSocket = knowledgeSocketIO.socket
const taskSocket = taskUpdateSocketIO.socket
const projectSocket = projectListSocketIO.socket
// All should reference the same socket instance
expect(knowledgeSocket).toBe(taskSocket)
expect(taskSocket).toBe(projectSocket)
})
test('operations from different services use the same socket', () => {
const mockCallback = vi.fn()
// Subscribe to events from different service exports
knowledgeSocketIO.on('knowledge_update', mockCallback)
taskUpdateSocketIO.on('task_update', mockCallback)
projectListSocketIO.on('project_update', mockCallback)
// All operations should use the same underlying socket
const socket = knowledgeSocketIO.socket
expect(socket.on).toHaveBeenCalledWith('knowledge_update', expect.any(Function))
expect(socket.on).toHaveBeenCalledWith('task_update', expect.any(Function))
expect(socket.on).toHaveBeenCalledWith('project_update', expect.any(Function))
})
test('disconnecting one service disconnects all', () => {
// Disconnect using one service
knowledgeSocketIO.disconnect()
// Check that the shared socket was disconnected
const socket = knowledgeSocketIO.socket
expect(socket.disconnect).toHaveBeenCalledTimes(1)
// Verify all services report as disconnected
expect(knowledgeSocketIO.isConnected()).toBe(false)
expect(taskUpdateSocketIO.isConnected()).toBe(false)
expect(projectListSocketIO.isConnected()).toBe(false)
})
test('operation tracking is shared across all service exports', () => {
// Add operation from one service
const operationId = 'test-op-123'
knowledgeSocketIO.addOperation(operationId)
// Check if operation is tracked in all services
expect(knowledgeSocketIO.isOwnOperation(operationId)).toBe(true)
expect(taskUpdateSocketIO.isOwnOperation(operationId)).toBe(true)
expect(projectListSocketIO.isOwnOperation(operationId)).toBe(true)
})
test('removing operation from one service removes from all', () => {
const operationId = 'test-op-456'
// Add operation
taskUpdateSocketIO.addOperation(operationId)
expect(knowledgeSocketIO.isOwnOperation(operationId)).toBe(true)
// Remove operation using different service
projectListSocketIO.removeOperation(operationId)
// Verify removed from all
expect(knowledgeSocketIO.isOwnOperation(operationId)).toBe(false)
expect(taskUpdateSocketIO.isOwnOperation(operationId)).toBe(false)
expect(projectListSocketIO.isOwnOperation(operationId)).toBe(false)
})
test('echo suppression works across all service exports', () => {
const operationId = 'echo-test-789'
const callback = vi.fn()
// Subscribe to event
knowledgeSocketIO.on('test_event', callback, true) // skipOwnOperations = true
// Add operation from different service export
taskUpdateSocketIO.addOperation(operationId)
// Simulate event with operation ID
const eventData = { operationId, data: 'test' }
const handler = knowledgeSocketIO.socket.on.mock.calls[0][1]
handler(eventData)
// Callback should not be called due to echo suppression
expect(callback).not.toHaveBeenCalled()
// Simulate event without operation ID
const externalEvent = { data: 'external' }
handler(externalEvent)
// Callback should be called for external events
expect(callback).toHaveBeenCalledWith(externalEvent)
})
test('connection state is synchronized across all exports', () => {
const mockSocket = knowledgeSocketIO.socket
// Simulate connected state
mockSocket.connected = true
expect(knowledgeSocketIO.isConnected()).toBe(true)
expect(taskUpdateSocketIO.isConnected()).toBe(true)
expect(projectListSocketIO.isConnected()).toBe(true)
// Simulate disconnected state
mockSocket.connected = false
expect(knowledgeSocketIO.isConnected()).toBe(false)
expect(taskUpdateSocketIO.isConnected()).toBe(false)
expect(projectListSocketIO.isConnected()).toBe(false)
})
test('emitting from any service uses the shared socket', () => {
const mockSocket = knowledgeSocketIO.socket
// Emit from different services
knowledgeSocketIO.emit('event1', { data: 1 })
taskUpdateSocketIO.emit('event2', { data: 2 })
projectListSocketIO.emit('event3', { data: 3 })
// All should use the same socket
expect(mockSocket.emit).toHaveBeenCalledTimes(3)
expect(mockSocket.emit).toHaveBeenCalledWith('event1', { data: 1 }, undefined)
expect(mockSocket.emit).toHaveBeenCalledWith('event2', { data: 2 }, undefined)
expect(mockSocket.emit).toHaveBeenCalledWith('event3', { data: 3 }, undefined)
})
test('prevents multiple socket connections when switching tabs', () => {
// Simulate tab switching by importing the module multiple times
// In a real scenario, this would happen when components unmount/remount
// First "tab"
const socket1 = knowledgeSocketIO.socket
// Simulate switching tabs (in reality, components would remount)
// But the shared instance pattern prevents new connections
const socket2 = taskUpdateSocketIO.socket
const socket3 = projectListSocketIO.socket
// All should be the same instance
expect(socket1).toBe(socket2)
expect(socket2).toBe(socket3)
// io should still only be called once
expect(io).toHaveBeenCalledTimes(1)
})
})