/ src / doctor.test.ts
doctor.test.ts
  1  import { beforeEach, describe, expect, it, vi } from 'vitest';
  2  
  3  const { mockGetDaemonHealth, mockListSessions, mockConnect, mockClose } = vi.hoisted(() => ({
  4    mockGetDaemonHealth: vi.fn(),
  5    mockListSessions: vi.fn(),
  6    mockConnect: vi.fn(),
  7    mockClose: vi.fn(),
  8  }));
  9  
 10  vi.mock('./browser/daemon-client.js', () => ({
 11    getDaemonHealth: mockGetDaemonHealth,
 12    listSessions: mockListSessions,
 13  }));
 14  
 15  vi.mock('./browser/index.js', () => ({
 16    BrowserBridge: class {
 17      connect = mockConnect;
 18      close = mockClose;
 19    },
 20  }));
 21  
 22  import { renderBrowserDoctorReport, runBrowserDoctor } from './doctor.js';
 23  
 24  describe('doctor report rendering', () => {
 25    const strip = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, '');
 26  
 27    beforeEach(() => {
 28      vi.clearAllMocks();
 29    });
 30  
 31    it('renders OK-style report when daemon and extension connected', () => {
 32      const text = strip(renderBrowserDoctorReport({
 33        daemonRunning: true,
 34        extensionConnected: true,
 35        extensionVersion: '1.6.8',
 36        issues: [],
 37      }));
 38  
 39      expect(text).toContain('[OK] Daemon: running on port 19825');
 40      expect(text).toContain('[OK] Extension: connected (v1.6.8)');
 41      expect(text).toContain('Everything looks good!');
 42    });
 43  
 44    it('renders MISSING when daemon not running', () => {
 45      const text = strip(renderBrowserDoctorReport({
 46        daemonRunning: false,
 47        extensionConnected: false,
 48        issues: ['Daemon is not running.'],
 49      }));
 50  
 51      expect(text).toContain('[MISSING] Daemon: not running');
 52      expect(text).toContain('[MISSING] Extension: not connected');
 53      expect(text).toContain('Daemon is not running.');
 54    });
 55  
 56    it('renders extension not connected when daemon is running', () => {
 57      const text = strip(renderBrowserDoctorReport({
 58        daemonRunning: true,
 59        extensionConnected: false,
 60        issues: ['Daemon is running but the Chrome extension is not connected.'],
 61      }));
 62  
 63      expect(text).toContain('[OK] Daemon: running on port 19825');
 64      expect(text).toContain('[MISSING] Extension: not connected');
 65    });
 66  
 67    it('renders a warning when the extension version is unknown', () => {
 68      const text = strip(renderBrowserDoctorReport({
 69        daemonRunning: true,
 70        extensionConnected: true,
 71        issues: ['Extension is connected but did not report a version.'],
 72      }));
 73  
 74      expect(text).toContain('[WARN] Extension: connected (version unknown)');
 75      expect(text).toContain('Extension is connected but did not report a version.');
 76      expect(text).not.toContain('Everything looks good!');
 77    });
 78  
 79    it('renders connectivity OK when live test succeeds', () => {
 80      const text = strip(renderBrowserDoctorReport({
 81        daemonRunning: true,
 82        extensionConnected: true,
 83        connectivity: { ok: true, durationMs: 1234 },
 84        issues: [],
 85      }));
 86  
 87      expect(text).toContain('[OK] Connectivity: connected in 1.2s');
 88    });
 89  
 90    it('renders connectivity SKIP when not tested', () => {
 91      const text = strip(renderBrowserDoctorReport({
 92        daemonRunning: true,
 93        extensionConnected: true,
 94        issues: [],
 95      }));
 96  
 97      expect(text).toContain('[SKIP] Connectivity: skipped (--no-live)');
 98    });
 99  
100    it('renders unstable extension state when live connectivity and status disagree', () => {
101      const text = strip(renderBrowserDoctorReport({
102        daemonRunning: true,
103        extensionConnected: true,
104        extensionFlaky: true,
105        connectivity: { ok: true, durationMs: 1234 },
106        issues: ['Extension connection is unstable.'],
107      }));
108  
109      expect(text).toContain('[WARN] Extension: unstable');
110      expect(text).toContain('Extension connection is unstable.');
111    });
112  
113    it('renders unstable daemon state when live connectivity and status disagree', () => {
114      const text = strip(renderBrowserDoctorReport({
115        daemonRunning: false,
116        daemonFlaky: true,
117        extensionConnected: false,
118        connectivity: { ok: true, durationMs: 1234 },
119        issues: ['Daemon connectivity is unstable.'],
120      }));
121  
122      expect(text).toContain('[WARN] Daemon: unstable');
123      expect(text).toContain('Daemon connectivity is unstable.');
124    });
125  
126    it('reports daemon not running when no-live and auto-start fails', async () => {
127      mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
128      mockConnect.mockRejectedValueOnce(new Error('Could not start daemon'));
129      mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
130  
131      const report = await runBrowserDoctor({ live: false });
132  
133      expect(report.daemonRunning).toBe(false);
134      expect(report.extensionConnected).toBe(false);
135      expect(mockGetDaemonHealth).toHaveBeenCalledTimes(2);
136      expect(report.issues).toEqual(expect.arrayContaining([
137        expect.stringContaining('Daemon is not running'),
138      ]));
139    });
140  
141    it('reports flapping when live check succeeds but final status shows extension disconnected', async () => {
142      mockConnect.mockResolvedValueOnce({
143        evaluate: vi.fn().mockResolvedValue(2),
144      });
145      mockClose.mockResolvedValueOnce(undefined);
146      mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
147  
148      const report = await runBrowserDoctor({ live: true });
149  
150      expect(report.daemonRunning).toBe(true);
151      expect(report.extensionConnected).toBe(false);
152      expect(report.extensionFlaky).toBe(true);
153      expect(report.issues).toEqual(expect.arrayContaining([
154        expect.stringContaining('Extension connection is unstable'),
155      ]));
156    });
157  
158    it('reports daemon flapping when live check succeeds but daemon disappears afterward', async () => {
159      mockConnect.mockResolvedValueOnce({
160        evaluate: vi.fn().mockResolvedValue(2),
161      });
162      mockClose.mockResolvedValueOnce(undefined);
163      mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
164  
165      const report = await runBrowserDoctor({ live: true });
166  
167      expect(report.daemonRunning).toBe(false);
168      expect(report.daemonFlaky).toBe(true);
169      expect(report.extensionConnected).toBe(false);
170      expect(report.issues).toEqual(expect.arrayContaining([
171        expect.stringContaining('Daemon connectivity is unstable'),
172      ]));
173    });
174  
175    it('uses the fast default timeout for live connectivity checks', async () => {
176      let timeoutSeen: number | undefined;
177      mockConnect.mockImplementationOnce(async (opts?: { timeout?: number }) => {
178        timeoutSeen = opts?.timeout;
179        return {
180          evaluate: vi.fn().mockResolvedValue(2),
181        };
182      });
183      mockClose.mockResolvedValueOnce(undefined);
184      mockGetDaemonHealth.mockResolvedValueOnce({ state: 'ready', status: { extensionConnected: true } });
185  
186      await runBrowserDoctor({ live: true });
187  
188      expect(timeoutSeen).toBe(8);
189    });
190  
191    it('skips auto-start in no-live mode when daemon is already running', async () => {
192      mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
193      mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
194  
195      const report = await runBrowserDoctor({ live: false });
196  
197      expect(mockConnect).not.toHaveBeenCalled();
198      expect(report.daemonRunning).toBe(true);
199      expect(report.extensionConnected).toBe(false);
200    });
201  
202    it('reports an issue when the extension is connected but does not report a version', async () => {
203      const status = {
204        state: 'ready' as const,
205        status: {
206          extensionConnected: true,
207          extensionVersion: undefined,
208        },
209      };
210      mockGetDaemonHealth
211        .mockResolvedValueOnce(status)
212        .mockResolvedValueOnce(status);
213  
214      const report = await runBrowserDoctor({ live: false });
215  
216      expect(report.issues).toEqual(expect.arrayContaining([
217        expect.stringContaining('did not report a version'),
218      ]));
219    });
220  });