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 });