cc-haha/src/server/__tests__/websocket-handler.test.ts
程序员阿江(Relakkes) 8b819bff29 Expose slash command usage metadata
Goal commands already had local CLI metadata, but desktop and SDK init consumers only received a name list. Keep the legacy slash_commands string array intact and add explicit metadata so clients can render descriptions and usage hints without breaking older readers.

Constraint: Existing SDK/system init consumers may depend on slash_commands staying string-only.

Rejected: Replace slash_commands with objects | would risk breaking older SDK and desktop consumers.

Confidence: high

Scope-risk: moderate

Directive: Keep slash_commands as the compatibility name list unless all SDK consumers have migrated.

Tested: bun test src/server/__tests__/websocket-handler.test.ts src/utils/messages/systemInit.test.ts src/commands/headless.test.ts src/goals/goalState.test.ts src/goals/goalEvaluator.test.ts

Tested: cd desktop && bun test src/components/chat/composerUtils.test.ts

Tested: bun run check:server

Tested: bun run check:desktop
2026-05-13 21:36:01 +08:00

97 lines
3.3 KiB
TypeScript

import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test'
import type { ServerWebSocket } from 'bun'
import {
__resetWebSocketHandlerStateForTests,
closeSessionConnection,
getActiveSessionIds,
handleWebSocket,
normalizeSlashCommandMetadata,
type WebSocketData,
} from '../ws/handler.js'
import { conversationService } from '../services/conversationService.js'
import { computerUseApprovalService } from '../services/computerUseApprovalService.js'
function makeClientSocket(sessionId: string) {
const sent: string[] = []
return {
data: {
sessionId,
connectedAt: Date.now(),
channel: 'client',
sdkToken: null,
serverPort: 0,
serverHost: '127.0.0.1',
},
send: mock((payload: string) => {
sent.push(payload)
}),
close: mock(() => {}),
sent,
} as unknown as ServerWebSocket<WebSocketData> & { sent: string[] }
}
describe('WebSocket handler session isolation', () => {
afterEach(() => {
__resetWebSocketHandlerStateForTests()
mock.restore()
})
it('ignores stale disconnects from an older socket for the same session', () => {
const sessionId = `duplicate-${crypto.randomUUID()}`
const first = makeClientSocket(sessionId)
const second = makeClientSocket(sessionId)
const clearCallbacks = spyOn(conversationService, 'clearOutputCallbacks')
const cancelComputerUse = spyOn(computerUseApprovalService, 'cancelSession')
handleWebSocket.open(first)
handleWebSocket.open(second)
clearCallbacks.mockClear()
cancelComputerUse.mockClear()
handleWebSocket.close(first, 1000, 'stale tab closed')
expect(getActiveSessionIds()).toContain(sessionId)
expect(clearCallbacks).not.toHaveBeenCalled()
expect(cancelComputerUse).not.toHaveBeenCalled()
})
it('closes and removes an active client socket when a session is deleted', () => {
const sessionId = `delete-${crypto.randomUUID()}`
const ws = makeClientSocket(sessionId)
const clearCallbacks = spyOn(conversationService, 'clearOutputCallbacks')
const cancelComputerUse = spyOn(computerUseApprovalService, 'cancelSession')
handleWebSocket.open(ws)
expect(closeSessionConnection(sessionId, 'session deleted')).toBe(true)
expect(getActiveSessionIds()).not.toContain(sessionId)
expect(ws.close).toHaveBeenCalledWith(1000, 'session deleted')
expect(clearCallbacks).toHaveBeenCalledWith(sessionId)
expect(cancelComputerUse).toHaveBeenCalledWith(sessionId)
})
it('normalizes slash command metadata while preserving legacy string payloads', () => {
expect(normalizeSlashCommandMetadata([
{
name: 'goal',
description: 'Create or manage an autonomous completion goal',
argumentHint: '<objective>|status|pause|resume|clear|complete',
whenToUse: 'Use when the session should keep iterating until done.',
},
'clear',
{ command: 'compact', description: 'Compact context' },
{ name: ' ' },
])).toEqual([
{
name: 'goal',
description: 'Create or manage an autonomous completion goal',
argumentHint: '<objective>|status|pause|resume|clear|complete',
whenToUse: 'Use when the session should keep iterating until done.',
},
{ name: 'clear', description: '' },
{ name: 'compact', description: 'Compact context' },
])
})
})