Skip to content

Commit 6e233f5

Browse files
committed
Avoid opening pages during healthy doctor checks
1 parent 0591fc2 commit 6e233f5

2 files changed

Lines changed: 48 additions & 11 deletions

File tree

src/doctor.test.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('doctor report rendering', () => {
4949
beforeEach(() => {
5050
vi.clearAllMocks();
5151
mockFindShadowedUserAdapters.mockReturnValue([]);
52-
// Doctor always runs live connectivity. Tests that want connect to fail override.
52+
// Doctor falls back to a live browser command only when status is not ready.
5353
mockConnect.mockResolvedValue({
5454
evaluate: vi.fn().mockResolvedValue(2),
5555
closeWindow: vi.fn().mockResolvedValue(undefined),
@@ -181,7 +181,9 @@ describe('doctor report rendering', () => {
181181

182182
it('reports daemon not running when connectivity fails and daemon stays stopped', async () => {
183183
mockConnect.mockRejectedValueOnce(new Error('Could not start daemon'));
184-
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
184+
mockGetDaemonHealth
185+
.mockResolvedValueOnce({ state: 'stopped', status: null })
186+
.mockResolvedValueOnce({ state: 'stopped', status: null });
185187

186188
const report = await runBrowserDoctor();
187189

@@ -194,7 +196,9 @@ describe('doctor report rendering', () => {
194196
});
195197

196198
it('reports flapping when live check succeeds but final status shows extension disconnected', async () => {
197-
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
199+
mockGetDaemonHealth
200+
.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } })
201+
.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
198202

199203
const report = await runBrowserDoctor();
200204

@@ -207,7 +211,9 @@ describe('doctor report rendering', () => {
207211
});
208212

209213
it('reports daemon flapping when live check succeeds but daemon disappears afterward', async () => {
210-
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
214+
mockGetDaemonHealth
215+
.mockResolvedValueOnce({ state: 'stopped', status: null })
216+
.mockResolvedValueOnce({ state: 'stopped', status: null });
211217

212218
const report = await runBrowserDoctor();
213219

@@ -231,7 +237,9 @@ describe('doctor report rendering', () => {
231237
closeWindow,
232238
};
233239
});
234-
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'ready', status: { extensionConnected: true } });
240+
mockGetDaemonHealth
241+
.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } })
242+
.mockResolvedValueOnce({ state: 'ready', status: { extensionConnected: true } });
235243

236244
await runBrowserDoctor();
237245

@@ -328,7 +336,9 @@ describe('doctor report rendering', () => {
328336

329337
it('reads daemon health for the resolved default profile after live connectivity succeeds', async () => {
330338
mockResolveProfileContextId.mockReturnValue('work');
331-
mockGetDaemonHealth.mockResolvedValueOnce({
339+
mockGetDaemonHealth
340+
.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } })
341+
.mockResolvedValueOnce({
332342
state: 'ready',
333343
status: {
334344
contextId: 'work',
@@ -350,4 +360,23 @@ describe('doctor report rendering', () => {
350360
expect.stringContaining('Extension connection is unstable'),
351361
]));
352362
});
363+
364+
it('does not open a live browser page when selected profile status is already ready', async () => {
365+
mockResolveProfileContextId.mockReturnValue('work');
366+
mockGetDaemonHealth.mockResolvedValueOnce({
367+
state: 'ready',
368+
status: {
369+
contextId: 'work',
370+
extensionConnected: true,
371+
extensionVersion: '1.2.3',
372+
},
373+
});
374+
375+
const report = await runBrowserDoctor();
376+
377+
expect(mockGetDaemonHealth).toHaveBeenCalledWith({ contextId: 'work' });
378+
expect(mockConnect).not.toHaveBeenCalled();
379+
expect(report.connectivity?.ok).toBe(true);
380+
expect(report.extensionConnected).toBe(true);
381+
});
353382
});

src/doctor.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,23 @@ export async function checkConnectivity(opts?: { timeout?: number }): Promise<Co
100100
}
101101

102102
export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<DoctorReport> {
103-
// Live connectivity check is the core of doctor — it doubles as auto-start
104-
// (bridge.connect spawns daemon) and validates end-to-end browser bridge health.
105-
const connectivity = await checkConnectivity();
106-
107103
// Single status read *after* connectivity side-effects settle. Use the same
108104
// default/env profile selection as BrowserBridge.connect so multi-profile
109105
// setups do not report profile-required after the live check succeeds.
110106
const contextId = resolveProfileContextId();
111-
const health = await getDaemonHealth(contextId ? { contextId } : undefined);
107+
const healthOpts = contextId ? { contextId } : undefined;
108+
const statusStart = Date.now();
109+
let health = await getDaemonHealth(healthOpts);
110+
111+
// If the selected profile is already ready, do not open a temporary page just
112+
// to prove connectivity. This keeps doctor read-only for healthy profiles and
113+
// avoids creating visible Chrome windows, groups, or blank tabs.
114+
const connectivity = health.state === 'ready'
115+
? { ok: true, durationMs: Date.now() - statusStart }
116+
: await checkConnectivity();
117+
if (health.state !== 'ready') {
118+
health = await getDaemonHealth(healthOpts);
119+
}
112120
const daemonRunning = health.state !== 'stopped';
113121
const extensionConnected = health.state === 'ready';
114122
const daemonFlaky = connectivity.ok && !daemonRunning;

0 commit comments

Comments
 (0)