workspaceGateway.test.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. // @vitest-environment node
  2. import {expect,it,vi} from 'vitest'
  3. import {WebSocketServer,type WebSocket} from 'ws'
  4. import {WorkspaceGateway,type GatewayFrame,type GatewayTarget} from '../../src/server/workspaceGateway'
  5. it('adapts Hermes server requests, scopes answers, restores pending cards and withdraws cancelled requests',async()=>{
  6. const server=new WebSocketServer({port:0,host:'127.0.0.1'})
  7. await new Promise<void>(done=>server.once('listening',done))
  8. const received:any[]=[],events:GatewayFrame[]=[]
  9. let socket:WebSocket
  10. server.on('connection',connection=>{
  11. socket=connection
  12. connection.send(JSON.stringify({method:'event',params:{type:'gateway.ready'}}))
  13. connection.on('message',raw=>{
  14. const value=JSON.parse(String(raw));received.push(value)
  15. if(value.method==='session.resume')connection.send(JSON.stringify({id:value.id,result:{session_id:'session',open_requests:[
  16. {id:'srq-replay',method:'clarify',params:{session_id:'session',question:'下一步做什么?'}},
  17. ]}}))
  18. })
  19. })
  20. const target={url:new URL(`http://127.0.0.1:${(server.address() as {port:number}).port}`),
  21. client:{directAgent:undefined},session:{webSocketCredential:async()=>({name:'ticket',value:'fixture'})}} as unknown as GatewayTarget
  22. const gateway=new WorkspaceGateway(target);gateway.onEvent=frame=>events.push(frame)
  23. const send=(value:unknown)=>socket.send(JSON.stringify(value))
  24. try{
  25. await gateway.connect()
  26. send({id:'srq-approve',method:'approval',params:{session_id:'session',request_id:'queue-id',description:'需要读取授权文件',choices:['once','deny']}})
  27. await expect.poll(()=>events.some(e=>e.type==='approval.requested')).toBe(true)
  28. expect(events.at(-1)).toMatchObject({session_id:'session',payload:{request_id:'srq-approve',message:'需要读取授权文件'}})
  29. await expect(gateway.rpc('approval.respond',{session_id:'another',request_id:'srq-approve',choice:'once'})).rejects.toMatchObject({code:'gateway_rejected'})
  30. await gateway.rpc('approval.respond',{session_id:'session',request_id:'srq-approve',choice:'deny'})
  31. await expect.poll(()=>received.some(r=>r.id==='srq-approve')).toBe(true)
  32. expect(received.find(r=>r.id==='srq-approve')).toEqual({jsonrpc:'2.0',id:'srq-approve',result:{choice:'deny'}})
  33. await expect(gateway.rpc('approval.respond',{session_id:'session',request_id:'srq-approve',choice:'once'})).rejects.toMatchObject({code:'gateway_rejected'})
  34. await gateway.rpc('session.resume',{session_id:'session'})
  35. await expect.poll(()=>events.some(e=>e.type==='clarify.requested')).toBe(true)
  36. send({method:'event',params:{type:'request.cancel',session_id:'session',payload:{id:'srq-replay',method:'clarify',reason:'interrupted'}}})
  37. await expect.poll(()=>events.some(e=>e.type==='interaction.cancelled')).toBe(true)
  38. await expect(gateway.rpc('clarify.respond',{session_id:'session',request_id:'srq-replay',answer:'late'})).rejects.toMatchObject({code:'gateway_rejected'})
  39. send({id:'srq-unknown',method:'unsupported.prompt',params:{session_id:'session'}})
  40. await expect.poll(()=>received.some(r=>r.id==='srq-unknown')).toBe(true)
  41. expect(received.find(r=>r.id==='srq-unknown').error.code).toBe(-32601)
  42. }finally{
  43. gateway.close();for(const client of server.clients)client.terminate()
  44. await new Promise<void>(done=>server.close(()=>done()))
  45. }
  46. })
  47. it('ignores frames and closes from the previous socket after a reconnect', async () => {
  48. const server = new WebSocketServer({port: 0, host: '127.0.0.1'})
  49. await new Promise<void>(resolve => server.once('listening', resolve))
  50. let current!: WebSocket, pending: any
  51. server.on('connection', socket => {
  52. current = socket
  53. socket.send(JSON.stringify({method: 'event', params: {type: 'gateway.ready'}}))
  54. socket.on('message', raw => { pending = JSON.parse(String(raw)) })
  55. })
  56. const target = {url: new URL(`http://127.0.0.1:${(server.address() as {port:number}).port}`), client: {},
  57. session: {webSocketCredential: async () => ({name: 'ticket', value: 'fixture'})}} as unknown as GatewayTarget
  58. const gateway = new WorkspaceGateway(target), events = vi.fn(), disconnect = vi.fn()
  59. gateway.onEvent = events; gateway.onDisconnect = disconnect
  60. try {
  61. await gateway.connect()
  62. const previous = (gateway as any).socket as WebSocket
  63. current.terminate()
  64. await expect.poll(() => disconnect.mock.calls.length).toBe(1)
  65. await gateway.connect()
  66. const reply = gateway.rpc('session.resume', {session_id: 'stored'})
  67. await expect.poll(() => pending?.method).toBe('session.resume')
  68. previous.emit('message', Buffer.from(JSON.stringify({method: 'event', params: {type: 'message.complete', session_id: 'stale'}})))
  69. previous.emit('close')
  70. expect(gateway.connected).toBe(true)
  71. expect(disconnect).toHaveBeenCalledOnce()
  72. expect(events.mock.calls.some(([frame]) => frame.session_id === 'stale')).toBe(false)
  73. current.send(JSON.stringify({id: pending.id, result: {session_id: 'current'}}))
  74. await expect(reply).resolves.toEqual({session_id: 'current'})
  75. } finally {
  76. gateway.close(); for (const client of server.clients) client.terminate()
  77. await new Promise<void>(resolve => server.close(() => resolve()))
  78. }
  79. })
  80. it('detects a half-open connection when Hermes stops answering pings', async () => {
  81. vi.useFakeTimers({toFake: ['setInterval', 'clearInterval']})
  82. const server = new WebSocketServer({port: 0, host: '127.0.0.1', autoPong: false})
  83. await new Promise<void>(resolve => server.once('listening', resolve))
  84. const ping = vi.fn()
  85. server.on('connection', socket => {
  86. socket.on('ping', ping)
  87. socket.send(JSON.stringify({method: 'event', params: {type: 'gateway.ready'}}))
  88. })
  89. const target = {url: new URL(`http://127.0.0.1:${(server.address() as {port:number}).port}`), client: {},
  90. session: {webSocketCredential: async () => ({name: 'ticket', value: 'fixture'})}} as unknown as GatewayTarget
  91. const gateway = new WorkspaceGateway(target), disconnect = vi.fn()
  92. gateway.onDisconnect = disconnect
  93. try {
  94. await gateway.connect()
  95. await vi.advanceTimersByTimeAsync(15_000)
  96. await expect.poll(() => ping.mock.calls.length).toBe(1)
  97. await vi.advanceTimersByTimeAsync(15_000)
  98. await expect.poll(() => disconnect.mock.calls.length).toBe(1)
  99. expect(gateway.connected).toBe(false)
  100. } finally {
  101. gateway.close(); for (const client of server.clients) client.terminate()
  102. await new Promise<void>(resolve => server.close(() => resolve()))
  103. vi.useRealTimers()
  104. }
  105. })