1
0

daemon-manager.test.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { describe, it, expect } from 'vitest';
  2. import {
  3. formatUptime,
  4. buildPickItems,
  5. runDaemonPicker,
  6. STOP_ALL,
  7. CANCEL,
  8. type PickerDeps,
  9. } from '../src/mcp/daemon-manager';
  10. import type { DaemonRecord, StopResult } from '../src/mcp/daemon-registry';
  11. const rec = (root: string, pid: number, startedAt: number): DaemonRecord => ({
  12. root, pid, version: '1.0.0', socketPath: `${root}/.codegraph/daemon.sock`, startedAt,
  13. });
  14. describe('formatUptime', () => {
  15. it('formats seconds / minutes / hours', () => {
  16. expect(formatUptime(45_000)).toBe('45s');
  17. expect(formatUptime(12 * 60_000)).toBe('12m');
  18. expect(formatUptime((3 * 60 + 5) * 60_000)).toBe('3h 5m');
  19. });
  20. });
  21. describe('buildPickItems', () => {
  22. const old = rec('/p/old', 1, 1000);
  23. const fresh = rec('/p/new', 2, 2000);
  24. const cwd = rec('/p/cwd', 3, 500);
  25. it('orders newest-first and appends Stop all + Cancel', () => {
  26. const items = buildPickItems([old, fresh], null, 3000);
  27. expect(items.map((i) => i.value)).toEqual(['/p/new', '/p/old', STOP_ALL, CANCEL]);
  28. expect(items[0].hint).toContain('pid 2');
  29. expect(items[0].hint).toContain('Running');
  30. });
  31. it('omits Stop all for a single daemon (but keeps Cancel)', () => {
  32. expect(buildPickItems([old], null, 3000).map((i) => i.value)).toEqual(['/p/old', CANCEL]);
  33. });
  34. it('floats the current project to the top, auto-selected and labelled', () => {
  35. const items = buildPickItems([old, fresh, cwd], '/p/cwd', 3000);
  36. expect(items[0].value).toBe('/p/cwd');
  37. expect(items[0].label).toContain('(current project)');
  38. expect(items.slice(1, 3).map((i) => i.value)).toEqual(['/p/new', '/p/old']); // rest newest-first
  39. });
  40. });
  41. describe('runDaemonPicker', () => {
  42. // A fake registry whose list shrinks as daemons are stopped (like the real one).
  43. function harness(initial: DaemonRecord[], choices: unknown[]) {
  44. let daemons = [...initial];
  45. const stopped: string[] = [];
  46. const notes: string[] = [];
  47. let doneMsg = '';
  48. let i = 0;
  49. const CANCEL_SYMBOL = Symbol('cancel');
  50. const deps: PickerDeps = {
  51. list: () => daemons,
  52. stop: async (root): Promise<StopResult> => {
  53. daemons = daemons.filter((d) => d.root !== root);
  54. stopped.push(root);
  55. return { root, pid: 0, outcome: 'term' };
  56. },
  57. stopAll: async (): Promise<StopResult[]> => {
  58. const all = daemons.map((d) => ({ root: d.root, pid: d.pid, outcome: 'term' as const }));
  59. daemons = [];
  60. stopped.push('ALL');
  61. return all;
  62. },
  63. cwdRoot: null,
  64. now: () => 5000,
  65. select: async () => choices[i++],
  66. isCancel: (v) => v === CANCEL_SYMBOL,
  67. note: (m) => notes.push(m),
  68. done: (m) => { doneMsg = m; },
  69. };
  70. return { deps, stopped, notes, getDone: () => doneMsg, CANCEL_SYMBOL };
  71. }
  72. it('stops the chosen daemon, then re-prompts and exits on Cancel', async () => {
  73. const h = harness([rec('/p/a', 1, 1), rec('/p/b', 2, 2)], ['/p/b', CANCEL]);
  74. await runDaemonPicker(h.deps);
  75. expect(h.stopped).toEqual(['/p/b']);
  76. expect(h.getDone()).toContain('Cancelled');
  77. });
  78. it('keeps stopping until none remain', async () => {
  79. const h = harness([rec('/p/a', 1, 1), rec('/p/b', 2, 2)], ['/p/a', '/p/b']);
  80. await runDaemonPicker(h.deps);
  81. expect(h.stopped).toEqual(['/p/a', '/p/b']);
  82. expect(h.getDone()).toContain('All daemons stopped');
  83. });
  84. it('Stop all stops everything in one shot', async () => {
  85. const h = harness([rec('/p/a', 1, 1), rec('/p/b', 2, 2)], [STOP_ALL]);
  86. await runDaemonPicker(h.deps);
  87. expect(h.stopped).toEqual(['ALL']);
  88. expect(h.getDone()).toBe('Done.');
  89. });
  90. it('Cancel (and Esc/Ctrl-C) stop nothing', async () => {
  91. const h1 = harness([rec('/p/a', 1, 1)], [CANCEL]);
  92. await runDaemonPicker(h1.deps);
  93. expect(h1.stopped).toEqual([]);
  94. expect(h1.getDone()).toContain('Cancelled');
  95. const h2 = harness([rec('/p/a', 1, 1)], [/* will use the cancel symbol */]);
  96. h2.deps.select = async () => h2.CANCEL_SYMBOL;
  97. await runDaemonPicker(h2.deps);
  98. expect(h2.stopped).toEqual([]);
  99. expect(h2.getDone()).toContain('Cancelled');
  100. });
  101. });