daemon-client-liveness.test.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. /**
  2. * Unit coverage for the daemon-side client-liveness primitives (#692, Layer 2).
  3. *
  4. * These back the daemon's defense against a phantom client — one whose process
  5. * died without the socket ever signalling close (a Windows named-pipe hazard).
  6. * The wire parsing and the liveness decision are pure, so they're tested here;
  7. * the full handshake + sweep is exercised end-to-end in `mcp-daemon.test.ts`.
  8. */
  9. import { describe, it, expect } from 'vitest';
  10. import { parseClientHelloLine, peerIsDead } from '../src/mcp/daemon';
  11. describe('parseClientHelloLine', () => {
  12. it('parses a well-formed client-hello', () => {
  13. expect(parseClientHelloLine('{"codegraph_client":1,"pid":1234,"hostPid":56}'))
  14. .toEqual({ pid: 1234, hostPid: 56 });
  15. });
  16. it('accepts a null host pid and a missing host pid', () => {
  17. expect(parseClientHelloLine('{"codegraph_client":1,"pid":1234,"hostPid":null}'))
  18. .toEqual({ pid: 1234, hostPid: null });
  19. expect(parseClientHelloLine('{"codegraph_client":1,"pid":1234}'))
  20. .toEqual({ pid: 1234, hostPid: null });
  21. });
  22. it('returns null for a JSON-RPC message (no marker) so it is treated as data', () => {
  23. expect(parseClientHelloLine('{"jsonrpc":"2.0","id":1,"method":"initialize"}')).toBeNull();
  24. });
  25. it('rejects a wrong-typed marker, a non-numeric pid, and a non-integer marker', () => {
  26. expect(parseClientHelloLine('{"codegraph_client":true,"pid":1}')).toBeNull();
  27. expect(parseClientHelloLine('{"codegraph_client":2,"pid":1}')).toBeNull();
  28. expect(parseClientHelloLine('{"codegraph_client":1,"pid":"1"}')).toBeNull();
  29. });
  30. it('returns null for invalid / empty / non-object JSON', () => {
  31. expect(parseClientHelloLine('not json')).toBeNull();
  32. expect(parseClientHelloLine('')).toBeNull();
  33. expect(parseClientHelloLine('42')).toBeNull();
  34. expect(parseClientHelloLine('null')).toBeNull();
  35. });
  36. });
  37. describe('peerIsDead', () => {
  38. const aliveAll = () => true;
  39. const deadAll = () => false;
  40. const deadOnly = (...pids: number[]) => (pid: number) => !pids.includes(pid);
  41. it('never reaps a client with an unknown pid (no client-hello)', () => {
  42. expect(peerIsDead({ pid: null, hostPid: null }, deadAll)).toBe(false);
  43. expect(peerIsDead({ pid: null, hostPid: 99 }, deadAll)).toBe(false);
  44. });
  45. it('keeps a client whose proxy is alive', () => {
  46. expect(peerIsDead({ pid: 100, hostPid: null }, aliveAll)).toBe(false);
  47. });
  48. it('reaps a client whose proxy process is gone', () => {
  49. expect(peerIsDead({ pid: 100, hostPid: null }, deadOnly(100))).toBe(true);
  50. });
  51. it('reaps when the proxy is alive but its host is gone', () => {
  52. // proxy 100 alive, host 42 dead
  53. expect(peerIsDead({ pid: 100, hostPid: 42 }, deadOnly(42))).toBe(true);
  54. });
  55. it('keeps a client when both proxy and host are alive', () => {
  56. expect(peerIsDead({ pid: 100, hostPid: 42 }, aliveAll)).toBe(false);
  57. });
  58. });