You've already forked agentic-coding-workflow
test(web): useAcpStream composable (connect/state/reconnect/prompt/close)
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
vi.mock('@/stores/auth', () => ({
|
||||
useAuthStore: () => ({ token: 'jwt-token' }),
|
||||
}))
|
||||
|
||||
import { useAcpStream } from './useAcpStream'
|
||||
|
||||
class MockWebSocket {
|
||||
static CONNECTING = 0
|
||||
static OPEN = 1
|
||||
static CLOSING = 2
|
||||
static CLOSED = 3
|
||||
static instances: MockWebSocket[] = []
|
||||
static last(): MockWebSocket {
|
||||
return this.instances[this.instances.length - 1]
|
||||
}
|
||||
|
||||
url: string
|
||||
readyState = 0
|
||||
onopen: ((ev: unknown) => void) | null = null
|
||||
onmessage: ((ev: { data: string }) => void) | null = null
|
||||
onerror: ((ev: unknown) => void) | null = null
|
||||
onclose: ((ev: unknown) => void) | null = null
|
||||
sent: string[] = []
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url
|
||||
MockWebSocket.instances.push(this)
|
||||
queueMicrotask(() => {
|
||||
this.readyState = 1
|
||||
this.onopen?.({})
|
||||
})
|
||||
}
|
||||
|
||||
send(data: string) {
|
||||
this.sent.push(data)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = 3
|
||||
this.onclose?.({})
|
||||
}
|
||||
|
||||
emit(data: unknown) {
|
||||
this.onmessage?.({ data: JSON.stringify(data) })
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
MockWebSocket.instances = []
|
||||
vi.stubGlobal('WebSocket', MockWebSocket)
|
||||
vi.stubGlobal('crypto', { randomUUID: () => 'fake-uuid' })
|
||||
vi.stubGlobal('window', { location: { protocol: 'http:', host: 'test.local' } })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('useAcpStream', () => {
|
||||
it('connects with ?since=0 on mount', async () => {
|
||||
const stream = useAcpStream('sess-1')
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
const ws = MockWebSocket.last()
|
||||
expect(ws.url).toContain('/sessions/sess-1/ws')
|
||||
expect(ws.url).toContain('since=0')
|
||||
expect(ws.url).toContain('token=jwt-token')
|
||||
expect(stream.status.value).toBe('streaming')
|
||||
stream.close()
|
||||
})
|
||||
|
||||
it('updates sessionStatus from state event', async () => {
|
||||
const stream = useAcpStream('s1')
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
MockWebSocket.last().emit({ kind: 'state', status: 'running' })
|
||||
expect(stream.sessionStatus.value).toBe('running')
|
||||
stream.close()
|
||||
})
|
||||
|
||||
it('accumulates events and tracks lastEventID', async () => {
|
||||
const stream = useAcpStream('s2')
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
MockWebSocket.last().emit({
|
||||
id: 1,
|
||||
direction: 'out',
|
||||
kind: 'notification',
|
||||
method: 'session/update',
|
||||
payload: {},
|
||||
})
|
||||
MockWebSocket.last().emit({
|
||||
id: 2,
|
||||
direction: 'out',
|
||||
kind: 'notification',
|
||||
method: 'session/update',
|
||||
payload: {},
|
||||
})
|
||||
expect(stream.events.value).toHaveLength(2)
|
||||
stream.close()
|
||||
})
|
||||
|
||||
it('reconnects with ?since=<last> after close', async () => {
|
||||
const stream = useAcpStream('s3')
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
MockWebSocket.last().emit({ id: 5, direction: 'out', kind: 'notification' })
|
||||
|
||||
MockWebSocket.last().close()
|
||||
await new Promise((r) => setTimeout(r, 1100))
|
||||
const ws2 = MockWebSocket.last()
|
||||
expect(ws2.url).toContain('since=5')
|
||||
stream.close()
|
||||
})
|
||||
|
||||
it('sends session/prompt with client-<uuid> ID', async () => {
|
||||
const stream = useAcpStream('s4')
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
stream.prompt('hello', 'agent-sid')
|
||||
const sent = MockWebSocket.last().sent
|
||||
expect(sent).toHaveLength(1)
|
||||
const parsed = JSON.parse(sent[0]) as {
|
||||
method: string
|
||||
id: string
|
||||
params: { sessionId: string; prompt: { text: string }[] }
|
||||
}
|
||||
expect(parsed.method).toBe('session/prompt')
|
||||
expect(parsed.id).toMatch(/^client-/)
|
||||
expect(parsed.params.sessionId).toBe('agent-sid')
|
||||
expect(parsed.params.prompt[0].text).toBe('hello')
|
||||
stream.close()
|
||||
})
|
||||
|
||||
it('manual close prevents reconnect', async () => {
|
||||
const stream = useAcpStream('s5')
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
stream.close()
|
||||
expect(stream.status.value).toBe('closed')
|
||||
await new Promise((r) => setTimeout(r, 1500))
|
||||
expect(MockWebSocket.instances).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user