import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { mutateUnifiedConfig } from '../../../../src/config/unified-config-loader'; import * as chromeReuse from '../../../../src/utils/browser/chrome-reuse'; import { getBrowserStatus } from '../../../../src/utils/browser/browser-status'; import * as codexDetector from '../../../../src/targets/codex-detector'; describe('browser status', () => { let tempHome = ''; let originalCcsHome: string | undefined; let originalBrowserUserDataDir: string | undefined; let originalBrowserProfileDir: string | undefined; let originalBrowserDevtoolsPort: string | undefined; beforeEach(() => { tempHome = mkdtempSync(join(tmpdir(), 'ccs-browser-status-')); originalCcsHome = process.env.CCS_HOME; originalBrowserUserDataDir = process.env.CCS_BROWSER_USER_DATA_DIR; originalBrowserProfileDir = process.env.CCS_BROWSER_PROFILE_DIR; originalBrowserDevtoolsPort = process.env.CCS_BROWSER_DEVTOOLS_PORT; process.env.CCS_HOME = tempHome; delete process.env.CCS_BROWSER_USER_DATA_DIR; delete process.env.CCS_BROWSER_PROFILE_DIR; delete process.env.CCS_BROWSER_DEVTOOLS_PORT; }); afterEach(() => { if (originalCcsHome !== undefined) { process.env.CCS_HOME = originalCcsHome; } else { delete process.env.CCS_HOME; } if (originalBrowserUserDataDir !== undefined) { process.env.CCS_BROWSER_USER_DATA_DIR = originalBrowserUserDataDir; } else { delete process.env.CCS_BROWSER_USER_DATA_DIR; } if (originalBrowserProfileDir !== undefined) { process.env.CCS_BROWSER_PROFILE_DIR = originalBrowserProfileDir; } else { delete process.env.CCS_BROWSER_PROFILE_DIR; } if (originalBrowserDevtoolsPort !== undefined) { process.env.CCS_BROWSER_DEVTOOLS_PORT = originalBrowserDevtoolsPort; } else { delete process.env.CCS_BROWSER_DEVTOOLS_PORT; } rmSync(tempHome, { recursive: true, force: true }); }); it('returns a disabled Claude lane with the recommended managed user-data dir by default', async () => { const codexSpy = spyOn(codexDetector, 'getCodexBinaryInfo').mockReturnValue({ path: '/usr/local/bin/codex', needsShell: false, version: 'codex-cli 0.120.0', features: ['config-overrides'], }); try { const status = await getBrowserStatus(); expect(status.claude).toMatchObject({ enabled: false, state: 'disabled', source: 'config', effectiveUserDataDir: join(tempHome, '.ccs', 'browser', 'chrome-user-data'), devtoolsPort: 9222, managedMcpServerName: 'ccs-browser', }); expect(status.claude.launchCommands.linux).toContain('--remote-debugging-port=9222'); expect(status.codex).toMatchObject({ enabled: true, state: 'enabled', serverName: 'ccs_browser', supportsConfigOverrides: true, }); } finally { codexSpy.mockRestore(); } }); it('prefers CCS_BROWSER_USER_DATA_DIR over config when an env override is present', async () => { mutateUnifiedConfig((config) => { config.browser = { claude: { enabled: true, user_data_dir: '/config-browser', devtools_port: 9333, }, codex: { enabled: true, }, }; }); process.env.CCS_BROWSER_USER_DATA_DIR = '/env-browser'; process.env.CCS_BROWSER_DEVTOOLS_PORT = '9444'; const runtimeSpy = spyOn(chromeReuse, 'resolveBrowserRuntimeEnv').mockResolvedValue({ CCS_BROWSER_USER_DATA_DIR: '/env-browser', CCS_BROWSER_DEVTOOLS_HOST: '127.0.0.1', CCS_BROWSER_DEVTOOLS_PORT: '9444', CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:9444', CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/test', }); const codexSpy = spyOn(codexDetector, 'getCodexBinaryInfo').mockReturnValue({ path: '/usr/local/bin/codex', needsShell: false, version: 'codex-cli 0.120.0', features: ['config-overrides'], }); try { const status = await getBrowserStatus(); expect(status.claude).toMatchObject({ enabled: true, state: 'ready', source: 'CCS_BROWSER_USER_DATA_DIR', effectiveUserDataDir: '/env-browser', devtoolsPort: 9444, }); expect(status.claude.runtimeEnv?.CCS_BROWSER_DEVTOOLS_PORT).toBe('9444'); } finally { runtimeSpy.mockRestore(); codexSpy.mockRestore(); } }); it('reports browser_not_running when attach metadata is missing', async () => { mutateUnifiedConfig((config) => { config.browser = { claude: { enabled: true, user_data_dir: '/tmp/browser-profile', devtools_port: 9222, }, codex: { enabled: true, }, }; }); const runtimeSpy = spyOn(chromeReuse, 'resolveBrowserRuntimeEnv').mockRejectedValue( new Error('Chrome reuse metadata not found: /tmp/browser-profile/DevToolsActivePort') ); const codexSpy = spyOn(codexDetector, 'getCodexBinaryInfo').mockReturnValue({ path: '/usr/local/bin/codex', needsShell: false, version: 'codex-cli 0.120.0', features: ['config-overrides'], }); try { const status = await getBrowserStatus(); expect(status.claude.state).toBe('browser_not_running'); expect(status.claude.detail).toContain('DevToolsActivePort'); expect(status.claude.nextStep).toContain('--remote-debugging-port=9222'); } finally { runtimeSpy.mockRestore(); codexSpy.mockRestore(); } }); it('preserves legacy metadata-based port discovery when only CCS_BROWSER_PROFILE_DIR is set', async () => { process.env.CCS_BROWSER_PROFILE_DIR = '/legacy-browser'; const runtimeSpy = spyOn(chromeReuse, 'resolveBrowserRuntimeEnv').mockResolvedValue({ CCS_BROWSER_USER_DATA_DIR: '/legacy-browser', CCS_BROWSER_DEVTOOLS_HOST: '127.0.0.1', CCS_BROWSER_DEVTOOLS_PORT: '50123', CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:50123', CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/legacy', }); const codexSpy = spyOn(codexDetector, 'getCodexBinaryInfo').mockReturnValue({ path: '/usr/local/bin/codex', needsShell: false, version: 'codex-cli 0.120.0', features: ['config-overrides'], }); try { const status = await getBrowserStatus(); expect(runtimeSpy.mock.calls[0]?.[0]).toEqual({ profileDir: '/legacy-browser', devtoolsPort: undefined, }); expect(status.claude.runtimeEnv?.CCS_BROWSER_DEVTOOLS_PORT).toBe('50123'); } finally { runtimeSpy.mockRestore(); codexSpy.mockRestore(); } }); });