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