jsonRpcPeer.test.ts 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { PassThrough } from 'node:stream';
  2. import { describe, expect, it, vi } from 'vitest';
  3. import { JsonRpcPeer, JsonRpcRequestError } from './jsonRpcPeer';
  4. /** 移植自 Noobi.ai src/main/jsonRpcPeer.test.ts:纯内存流,不依赖 codex 二进制与网络 */
  5. function makePeer() {
  6. const input = new PassThrough();
  7. const output = new PassThrough();
  8. output.setEncoding('utf8');
  9. const peer = new JsonRpcPeer(input, output);
  10. peer.start();
  11. return { input, output, peer };
  12. }
  13. describe('JsonRpcPeer', () => {
  14. it('把响应匹配回对应的 pending 请求', async () => {
  15. const { input, output, peer } = makePeer();
  16. const written = new Promise<string>((resolve) => output.once('data', resolve));
  17. const pending = peer.request<{ models: unknown[] }>('model/list');
  18. const request = JSON.parse(await written) as { id: number; method: string };
  19. expect(request.method).toBe('model/list');
  20. input.write(`${JSON.stringify({ id: request.id, result: { models: [] } })}\n`);
  21. await expect(pending).resolves.toEqual({ models: [] });
  22. peer.close();
  23. });
  24. it('区分通知与服务端请求,且能交错处理', async () => {
  25. const { input, peer } = makePeer();
  26. const notification = vi.fn();
  27. const serverRequest = vi.fn();
  28. peer.on('notification', notification);
  29. peer.on('serverRequest', serverRequest);
  30. input.write('{"method":"turn/started","params":{"turnId":"turn-1"}}\n');
  31. input.write('{"id":"approval-1","method":"item/commandExecution/requestApproval","params":{}}\n');
  32. await new Promise((resolve) => setImmediate(resolve));
  33. expect(notification).toHaveBeenCalledWith({
  34. method: 'turn/started',
  35. params: { turnId: 'turn-1' },
  36. });
  37. expect(serverRequest).toHaveBeenCalledWith({
  38. id: 'approval-1',
  39. method: 'item/commandExecution/requestApproval',
  40. params: {},
  41. });
  42. peer.close();
  43. });
  44. it('把 App Server 返回的协议错误抛给调用方', async () => {
  45. const { input, output, peer } = makePeer();
  46. const written = new Promise<string>((resolve) => output.once('data', resolve));
  47. const pending = peer.request('thread/start');
  48. const request = JSON.parse(await written) as { id: number };
  49. input.write(`${JSON.stringify({
  50. id: request.id,
  51. error: { code: -32602, message: 'invalid params' },
  52. })}\n`);
  53. await expect(pending).rejects.toBeInstanceOf(JsonRpcRequestError);
  54. peer.close();
  55. });
  56. it('遇到非法 JSON 只报错、不中断流', async () => {
  57. const { input, peer } = makePeer();
  58. const protocolError = vi.fn();
  59. const notification = vi.fn();
  60. peer.on('protocolError', protocolError);
  61. peer.on('notification', notification);
  62. input.write('{not-json}\n');
  63. input.write('{"method":"warning","params":{"message":"still alive"}}\n');
  64. await new Promise((resolve) => setImmediate(resolve));
  65. expect(protocolError).toHaveBeenCalledOnce();
  66. expect(notification).toHaveBeenCalledOnce();
  67. peer.close();
  68. });
  69. it('关闭后拒绝全部 pending 请求并广播 closed', async () => {
  70. const { peer } = makePeer();
  71. const closed = vi.fn();
  72. peer.on('closed', closed);
  73. const pending = peer.request('model/list');
  74. peer.close(new Error('子进程退出'));
  75. await expect(pending).rejects.toThrow('子进程退出');
  76. expect(closed).toHaveBeenCalledOnce();
  77. await expect(peer.request('model/list')).rejects.toThrow('已关闭');
  78. });
  79. });