mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-10 06:20:13 +00:00
fix(browser): bootstrap managed attach profile setup
This commit is contained in:
@@ -129,6 +129,10 @@ chrome.exe --remote-debugging-port=9222 --user-data-dir="%USERPROFILE%\\.ccs\\br
|
|||||||
Using a dedicated CCS browser data dir is recommended. It avoids profile-locking issues and keeps
|
Using a dedicated CCS browser data dir is recommended. It avoids profile-locking issues and keeps
|
||||||
automation state separate from your daily browser profile.
|
automation state separate from your daily browser profile.
|
||||||
|
|
||||||
|
When Claude Browser Attach uses the recommended managed path (`~/.ccs/browser/chrome-user-data`),
|
||||||
|
CCS now creates that directory automatically the first time it needs it. After that bootstrap step,
|
||||||
|
the remaining requirement is a running Chrome session started with `--remote-debugging-port`.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Browser status says Claude Browser Attach is disabled
|
### Browser status says Claude Browser Attach is disabled
|
||||||
@@ -144,6 +148,9 @@ The configured Chrome user-data directory does not exist yet.
|
|||||||
2. Start Chrome in attach mode with `--remote-debugging-port`
|
2. Start Chrome in attach mode with `--remote-debugging-port`
|
||||||
3. Rerun `ccs browser doctor`
|
3. Rerun `ccs browser doctor`
|
||||||
|
|
||||||
|
If you are using the CCS-managed default path, this usually means the path could not be created
|
||||||
|
automatically and now needs manual attention.
|
||||||
|
|
||||||
### Browser status says no running browser session was found
|
### Browser status says no running browser session was found
|
||||||
|
|
||||||
CCS could not find usable DevTools attach metadata for the configured user-data directory.
|
CCS could not find usable DevTools attach metadata for the configured user-data directory.
|
||||||
@@ -152,6 +159,9 @@ CCS could not find usable DevTools attach metadata for the configured user-data
|
|||||||
2. Make sure it is using the same `user_data_dir` configured in CCS
|
2. Make sure it is using the same `user_data_dir` configured in CCS
|
||||||
3. Rerun `ccs browser doctor`
|
3. Rerun `ccs browser doctor`
|
||||||
|
|
||||||
|
For the CCS-managed default path, this is the normal first-run state after CCS bootstraps the
|
||||||
|
directory for you.
|
||||||
|
|
||||||
### Browser status says the DevTools endpoint is unreachable
|
### Browser status says the DevTools endpoint is unreachable
|
||||||
|
|
||||||
CCS found attach metadata, but the endpoint did not answer successfully.
|
CCS found attach metadata, but the endpoint did not answer successfully.
|
||||||
|
|||||||
+2
-2
@@ -1067,7 +1067,7 @@ async function main(): Promise<void> {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
|
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
|
||||||
if (browserAttachRuntime?.warning) {
|
if (browserAttachRuntime?.warning) {
|
||||||
console.error(warn(browserAttachRuntime.warning));
|
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
|
||||||
}
|
}
|
||||||
if (resolvedTarget === 'claude') {
|
if (resolvedTarget === 'claude') {
|
||||||
ensureWebSearchMcpOrThrow();
|
ensureWebSearchMcpOrThrow();
|
||||||
@@ -1477,7 +1477,7 @@ async function main(): Promise<void> {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
|
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
|
||||||
if (browserAttachRuntime?.warning) {
|
if (browserAttachRuntime?.warning) {
|
||||||
console.error(warn(browserAttachRuntime.warning));
|
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedTarget === 'claude') {
|
if (resolvedTarget === 'claude') {
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ export async function execClaudeWithCLIProxy(
|
|||||||
: undefined;
|
: undefined;
|
||||||
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
|
const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv;
|
||||||
if (browserAttachRuntime?.warning) {
|
if (browserAttachRuntime?.warning) {
|
||||||
console.error(warn(browserAttachRuntime.warning));
|
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
|
||||||
}
|
}
|
||||||
if (browserRuntimeEnv) {
|
if (browserRuntimeEnv) {
|
||||||
ensureBrowserMcpOrThrow();
|
ensureBrowserMcpOrThrow();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { BrowserConfig } from '../../config/unified-config-types';
|
import type { BrowserConfig } from '../../config/unified-config-types';
|
||||||
import { getCcsDir } from '../config-manager';
|
import { getCcsDir } from '../config-manager';
|
||||||
import { expandPath } from '../helpers';
|
import { expandPath } from '../helpers';
|
||||||
|
import { getNodePlatformKey } from './platform';
|
||||||
import { type BrowserRuntimeEnv, resolveBrowserRuntimeEnv } from './chrome-reuse';
|
import { type BrowserRuntimeEnv, resolveBrowserRuntimeEnv } from './chrome-reuse';
|
||||||
|
|
||||||
export type BrowserOverrideSource = 'CCS_BROWSER_USER_DATA_DIR' | 'CCS_BROWSER_PROFILE_DIR';
|
export type BrowserOverrideSource = 'CCS_BROWSER_USER_DATA_DIR' | 'CCS_BROWSER_PROFILE_DIR';
|
||||||
@@ -24,10 +26,141 @@ export interface BrowserAttachRuntimeResolution {
|
|||||||
warning?: string;
|
warning?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ManagedBrowserAttachBootstrap {
|
||||||
|
usesManagedDefaultDir: boolean;
|
||||||
|
createdProfileDir: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ManagedBrowserAttachNotReadyMessage {
|
||||||
|
state: 'path_missing' | 'browser_not_running' | 'endpoint_unreachable';
|
||||||
|
title: string;
|
||||||
|
detail: string;
|
||||||
|
nextStep: string;
|
||||||
|
warning: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isManagedDefaultBrowserAttach(config: EffectiveClaudeBrowserAttachConfig): boolean {
|
||||||
|
return (
|
||||||
|
config.source === 'config' &&
|
||||||
|
path.resolve(config.userDataDir) === path.resolve(getRecommendedBrowserUserDataDir())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCurrentPlatformLaunchCommand(userDataDir: string, devtoolsPort: number): string {
|
||||||
|
const quotedPath = JSON.stringify(userDataDir);
|
||||||
|
switch (getNodePlatformKey()) {
|
||||||
|
case 'darwin':
|
||||||
|
return `open -na "Google Chrome" --args --remote-debugging-port=${devtoolsPort} --user-data-dir=${quotedPath}`;
|
||||||
|
case 'win32':
|
||||||
|
return `chrome.exe --remote-debugging-port=${devtoolsPort} --user-data-dir=${quotedPath}`;
|
||||||
|
default:
|
||||||
|
return `google-chrome --remote-debugging-port=${devtoolsPort} --user-data-dir=${quotedPath}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveBrowserUserDataDir(value?: string): string | undefined {
|
export function resolveBrowserUserDataDir(value?: string): string | undefined {
|
||||||
return value?.trim() ? expandPath(value) : undefined;
|
return value?.trim() ? expandPath(value) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ensureManagedBrowserUserDataDir(
|
||||||
|
config: EffectiveClaudeBrowserAttachConfig
|
||||||
|
): ManagedBrowserAttachBootstrap {
|
||||||
|
if (!isManagedDefaultBrowserAttach(config)) {
|
||||||
|
return {
|
||||||
|
usesManagedDefaultDir: false,
|
||||||
|
createdProfileDir: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.statSync(config.userDataDir);
|
||||||
|
return {
|
||||||
|
usesManagedDefaultDir: true,
|
||||||
|
createdProfileDir: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const code = (error as NodeJS.ErrnoException).code;
|
||||||
|
if (code && code !== 'ENOENT') {
|
||||||
|
return {
|
||||||
|
usesManagedDefaultDir: true,
|
||||||
|
createdProfileDir: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(config.userDataDir, { recursive: true, mode: 0o700 });
|
||||||
|
return {
|
||||||
|
usesManagedDefaultDir: true,
|
||||||
|
createdProfileDir: true,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
usesManagedDefaultDir: true,
|
||||||
|
createdProfileDir: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function describeManagedBrowserAttachNotReady(
|
||||||
|
config: EffectiveClaudeBrowserAttachConfig,
|
||||||
|
errorMessage: string,
|
||||||
|
options: {
|
||||||
|
createdProfileDir?: boolean;
|
||||||
|
launchCommand?: string;
|
||||||
|
} = {}
|
||||||
|
): ManagedBrowserAttachNotReadyMessage | undefined {
|
||||||
|
if (!isManagedDefaultBrowserAttach(config)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const launchCommand =
|
||||||
|
options.launchCommand ??
|
||||||
|
buildCurrentPlatformLaunchCommand(config.userDataDir, config.devtoolsPort);
|
||||||
|
const continueWithoutTools =
|
||||||
|
'CCS will continue without browser tools until the attach session is ready.';
|
||||||
|
|
||||||
|
if (errorMessage.includes('Chrome reuse metadata')) {
|
||||||
|
const summary = options.createdProfileDir
|
||||||
|
? `CCS created the managed browser profile at ${config.userDataDir}, but no running attach-mode Chrome session is using it yet`
|
||||||
|
: `No running attach-mode Chrome session is using the managed browser profile at ${config.userDataDir}`;
|
||||||
|
const nextStep = `Start Chrome with remote debugging and the managed user-data dir. Example: ${launchCommand}`;
|
||||||
|
return {
|
||||||
|
state: 'browser_not_running',
|
||||||
|
title: 'Claude Browser Attach is waiting for a managed Chrome session.',
|
||||||
|
detail: `${summary}. Diagnostic: ${errorMessage}`,
|
||||||
|
nextStep,
|
||||||
|
warning: `${summary}. ${nextStep} ${continueWithoutTools}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage.includes('Chrome DevTools endpoint')) {
|
||||||
|
const summary = `CCS could not reach the attach-mode DevTools endpoint for the managed browser profile at ${config.userDataDir}`;
|
||||||
|
const nextStep = `Restart Chrome in attach mode and retry. Example: ${launchCommand}`;
|
||||||
|
return {
|
||||||
|
state: 'endpoint_unreachable',
|
||||||
|
title: 'Claude Browser Attach could not reach the managed Chrome session.',
|
||||||
|
detail: `${summary}. Diagnostic: ${errorMessage}`,
|
||||||
|
nextStep,
|
||||||
|
warning: `${summary}. ${nextStep} ${continueWithoutTools}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage.includes('Chrome profile directory is invalid')) {
|
||||||
|
const summary = `CCS could not initialize the managed browser profile at ${config.userDataDir}`;
|
||||||
|
const nextStep = `Confirm the path is writable or reset it to the CCS-managed default, then launch Chrome in attach mode. Example: ${launchCommand}`;
|
||||||
|
return {
|
||||||
|
state: 'path_missing',
|
||||||
|
title: 'Claude Browser Attach could not initialize the managed profile.',
|
||||||
|
detail: `${summary}. Diagnostic: ${errorMessage}`,
|
||||||
|
nextStep,
|
||||||
|
warning: `${summary}. ${nextStep} ${continueWithoutTools}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function getBrowserAttachOverride(env: NodeJS.ProcessEnv = process.env): {
|
export function getBrowserAttachOverride(env: NodeJS.ProcessEnv = process.env): {
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
devtoolsPort?: number;
|
devtoolsPort?: number;
|
||||||
@@ -94,6 +227,17 @@ export async function resolveOptionalBrowserAttachRuntime(
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bootstrap = ensureManagedBrowserUserDataDir(config);
|
||||||
|
if (bootstrap.createdProfileDir) {
|
||||||
|
return {
|
||||||
|
warning: describeManagedBrowserAttachNotReady(
|
||||||
|
config,
|
||||||
|
`Chrome reuse metadata not found: ${path.join(config.userDataDir, 'DevToolsActivePort')}`,
|
||||||
|
{ createdProfileDir: true }
|
||||||
|
)?.warning,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
runtimeEnv: await resolveBrowserRuntimeEnv({
|
runtimeEnv: await resolveBrowserRuntimeEnv({
|
||||||
@@ -103,13 +247,12 @@ export async function resolveOptionalBrowserAttachRuntime(
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
const usesManagedDefaultDir =
|
const managedDefaultMessage = describeManagedBrowserAttachNotReady(config, message, {
|
||||||
config.source === 'config' &&
|
createdProfileDir: bootstrap.createdProfileDir,
|
||||||
path.resolve(config.userDataDir) === path.resolve(getRecommendedBrowserUserDataDir());
|
});
|
||||||
|
if (managedDefaultMessage) {
|
||||||
if (usesManagedDefaultDir && message.includes('Chrome profile directory is invalid')) {
|
|
||||||
return {
|
return {
|
||||||
warning: `Claude Browser Attach is enabled, but the managed browser profile does not exist yet (${config.userDataDir}). Launching without browser tools. Run \`ccs browser doctor\` to finish setup.`,
|
warning: managedDefaultMessage.warning,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import * as path from 'path';
|
||||||
import { getBrowserConfig } from '../../config/unified-config-loader';
|
import { getBrowserConfig } from '../../config/unified-config-loader';
|
||||||
import { getCodexBinaryInfo } from '../../targets/codex-detector';
|
import { getCodexBinaryInfo } from '../../targets/codex-detector';
|
||||||
import { type BrowserRuntimeEnv, resolveBrowserRuntimeEnv } from './chrome-reuse';
|
import { type BrowserRuntimeEnv, resolveBrowserRuntimeEnv } from './chrome-reuse';
|
||||||
import { getBrowserMcpServerName, getBrowserMcpServerPath } from './mcp-installer';
|
import { getBrowserMcpServerName, getBrowserMcpServerPath } from './mcp-installer';
|
||||||
import { getNodePlatformKey } from './platform';
|
import { getNodePlatformKey } from './platform';
|
||||||
import {
|
import {
|
||||||
|
describeManagedBrowserAttachNotReady,
|
||||||
|
ensureManagedBrowserUserDataDir,
|
||||||
getEffectiveClaudeBrowserAttachConfig,
|
getEffectiveClaudeBrowserAttachConfig,
|
||||||
getRecommendedBrowserUserDataDir,
|
getRecommendedBrowserUserDataDir,
|
||||||
} from './browser-settings';
|
} from './browser-settings';
|
||||||
@@ -61,6 +64,7 @@ async function buildClaudeBrowserStatus(
|
|||||||
): Promise<ClaudeBrowserStatus> {
|
): Promise<ClaudeBrowserStatus> {
|
||||||
const effective = getEffectiveClaudeBrowserAttachConfig(browserConfig);
|
const effective = getEffectiveClaudeBrowserAttachConfig(browserConfig);
|
||||||
const launchCommands = buildLaunchCommands(effective.userDataDir, effective.devtoolsPort);
|
const launchCommands = buildLaunchCommands(effective.userDataDir, effective.devtoolsPort);
|
||||||
|
const managedBootstrap = ensureManagedBrowserUserDataDir(effective);
|
||||||
const base: Omit<ClaudeBrowserStatus, 'state' | 'title' | 'detail' | 'nextStep'> = {
|
const base: Omit<ClaudeBrowserStatus, 'state' | 'title' | 'detail' | 'nextStep'> = {
|
||||||
enabled: effective.enabled,
|
enabled: effective.enabled,
|
||||||
source: effective.source,
|
source: effective.source,
|
||||||
@@ -85,6 +89,26 @@ async function buildClaudeBrowserStatus(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (managedBootstrap.createdProfileDir) {
|
||||||
|
const managedDefaultMessage = describeManagedBrowserAttachNotReady(
|
||||||
|
effective,
|
||||||
|
`Chrome reuse metadata not found: ${path.join(effective.userDataDir, 'DevToolsActivePort')}`,
|
||||||
|
{
|
||||||
|
createdProfileDir: true,
|
||||||
|
launchCommand: launchCommands[getNodePlatformKey()],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (managedDefaultMessage) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
state: managedDefaultMessage.state,
|
||||||
|
title: managedDefaultMessage.title,
|
||||||
|
detail: managedDefaultMessage.detail,
|
||||||
|
nextStep: managedDefaultMessage.nextStep,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const runtimeEnv = await resolveBrowserRuntimeEnv({
|
const runtimeEnv = await resolveBrowserRuntimeEnv({
|
||||||
profileDir: effective.userDataDir,
|
profileDir: effective.userDataDir,
|
||||||
@@ -102,6 +126,20 @@ async function buildClaudeBrowserStatus(
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = (error as Error).message;
|
const message = (error as Error).message;
|
||||||
|
const managedDefaultMessage = describeManagedBrowserAttachNotReady(effective, message, {
|
||||||
|
createdProfileDir: managedBootstrap.createdProfileDir,
|
||||||
|
launchCommand: launchCommands[getNodePlatformKey()],
|
||||||
|
});
|
||||||
|
if (managedDefaultMessage) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
state: managedDefaultMessage.state,
|
||||||
|
title: managedDefaultMessage.title,
|
||||||
|
detail: managedDefaultMessage.detail,
|
||||||
|
nextStep: managedDefaultMessage.nextStep,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (message.includes('Chrome profile directory is invalid')) {
|
if (message.includes('Chrome profile directory is invalid')) {
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
|
|||||||
@@ -81,6 +81,18 @@ function runCcs(args: string[], env: NodeJS.ProcessEnv): RunResult {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reserveClosedPort(): number {
|
||||||
|
const server = Bun.serve({
|
||||||
|
port: 0,
|
||||||
|
fetch() {
|
||||||
|
return new Response('ok');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { port } = server;
|
||||||
|
server.stop(true);
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
describe('default profile browser launch', () => {
|
describe('default profile browser launch', () => {
|
||||||
let tmpHome = '';
|
let tmpHome = '';
|
||||||
let fakeClaudePath = '';
|
let fakeClaudePath = '';
|
||||||
@@ -259,7 +271,7 @@ server.listen(0, '127.0.0.1', () => {
|
|||||||
claude: {
|
claude: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
user_data_dir: '',
|
user_data_dir: '',
|
||||||
devtools_port: 9222,
|
devtools_port: 43123,
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -272,8 +284,10 @@ server.listen(0, '127.0.0.1', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.status).toBe(0);
|
expect(result.status).toBe(0);
|
||||||
expect(result.stderr).toContain('Launching without browser tools');
|
expect(result.stderr).toContain('CCS created the managed browser profile');
|
||||||
expect(result.stderr).toContain('ccs browser doctor');
|
expect(result.stderr).toContain('Start Chrome with remote debugging');
|
||||||
|
expect(result.stderr).toContain('continue without browser tools');
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.ccs', 'browser', 'chrome-user-data'))).toBe(true);
|
||||||
|
|
||||||
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
|
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
|
||||||
expect(launchedArgs).not.toContain(BROWSER_PROMPT_SNIPPET);
|
expect(launchedArgs).not.toContain(BROWSER_PROMPT_SNIPPET);
|
||||||
@@ -291,6 +305,47 @@ server.listen(0, '127.0.0.1', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('skips managed browser attach when the managed profile exists but no browser session is running', () => {
|
||||||
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
|
const originalCcsHome = process.env.CCS_HOME;
|
||||||
|
process.env.CCS_HOME = tmpHome;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const unreachablePort = reserveClosedPort();
|
||||||
|
const managedProfileDir = path.join(tmpHome, '.ccs', 'browser', 'chrome-user-data');
|
||||||
|
fs.mkdirSync(managedProfileDir, { recursive: true });
|
||||||
|
|
||||||
|
mutateUnifiedConfig((config) => {
|
||||||
|
config.browser = {
|
||||||
|
claude: {
|
||||||
|
enabled: true,
|
||||||
|
user_data_dir: '',
|
||||||
|
devtools_port: unreachablePort,
|
||||||
|
},
|
||||||
|
codex: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = runCcs(['default', 'smoke'], {
|
||||||
|
...baseEnv,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.status).toBe(0);
|
||||||
|
|
||||||
|
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
|
||||||
|
expect(launchedArgs).not.toContain(BROWSER_PROMPT_SNIPPET);
|
||||||
|
} finally {
|
||||||
|
if (originalCcsHome !== undefined) {
|
||||||
|
process.env.CCS_HOME = originalCcsHome;
|
||||||
|
} else {
|
||||||
|
delete process.env.CCS_HOME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('uses config-backed browser attach settings when env overrides are absent', async () => {
|
it('uses config-backed browser attach settings when env overrides are absent', async () => {
|
||||||
if (process.platform === 'win32') return;
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ function runCcs(args: string[], env: NodeJS.ProcessEnv): RunResult {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reserveClosedPort(): number {
|
||||||
|
const server = Bun.serve({
|
||||||
|
port: 0,
|
||||||
|
fetch() {
|
||||||
|
return new Response('ok');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { port } = server;
|
||||||
|
server.stop(true);
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
describe('settings profile browser launch', () => {
|
describe('settings profile browser launch', () => {
|
||||||
let tmpHome = '';
|
let tmpHome = '';
|
||||||
let ccsDir = '';
|
let ccsDir = '';
|
||||||
@@ -210,7 +222,7 @@ server.listen(0, '127.0.0.1', () => {
|
|||||||
claude: {
|
claude: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
user_data_dir: '',
|
user_data_dir: '',
|
||||||
devtools_port: 9222,
|
devtools_port: 43123,
|
||||||
},
|
},
|
||||||
codex: {
|
codex: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -223,8 +235,10 @@ server.listen(0, '127.0.0.1', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.status).toBe(0);
|
expect(result.status).toBe(0);
|
||||||
expect(result.stderr).toContain('Launching without browser tools');
|
expect(result.stderr).toContain('CCS created the managed browser profile');
|
||||||
expect(result.stderr).toContain('ccs browser doctor');
|
expect(result.stderr).toContain('Start Chrome with remote debugging');
|
||||||
|
expect(result.stderr).toContain('continue without browser tools');
|
||||||
|
expect(fs.existsSync(path.join(tmpHome, '.ccs', 'browser', 'chrome-user-data'))).toBe(true);
|
||||||
|
|
||||||
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
|
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
|
||||||
expect(launchedArgs).not.toContain(BROWSER_PROMPT_SNIPPET);
|
expect(launchedArgs).not.toContain(BROWSER_PROMPT_SNIPPET);
|
||||||
@@ -242,6 +256,48 @@ server.listen(0, '127.0.0.1', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('skips managed browser attach for settings-profile launches when no managed browser session is running', () => {
|
||||||
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
|
const originalCcsHome = process.env.CCS_HOME;
|
||||||
|
process.env.CCS_HOME = tmpHome;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const unreachablePort = reserveClosedPort();
|
||||||
|
fs.mkdirSync(path.join(tmpHome, '.ccs', 'browser', 'chrome-user-data'), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
mutateUnifiedConfig((config) => {
|
||||||
|
config.browser = {
|
||||||
|
claude: {
|
||||||
|
enabled: true,
|
||||||
|
user_data_dir: '',
|
||||||
|
devtools_port: unreachablePort,
|
||||||
|
},
|
||||||
|
codex: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = runCcs(['glm', 'smoke'], {
|
||||||
|
...baseEnv,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.status).toBe(0);
|
||||||
|
|
||||||
|
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
|
||||||
|
expect(launchedArgs).not.toContain(BROWSER_PROMPT_SNIPPET);
|
||||||
|
} finally {
|
||||||
|
if (originalCcsHome !== undefined) {
|
||||||
|
process.env.CCS_HOME = originalCcsHome;
|
||||||
|
} else {
|
||||||
|
delete process.env.CCS_HOME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('uses config-backed browser attach settings for settings-profile launches', async () => {
|
it('uses config-backed browser attach settings for settings-profile launches', async () => {
|
||||||
if (process.platform === 'win32') return;
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test';
|
import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test';
|
||||||
import { mkdtempSync, rmSync } from 'node:fs';
|
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { mutateUnifiedConfig } from '../../../../src/config/unified-config-loader';
|
import { mutateUnifiedConfig } from '../../../../src/config/unified-config-loader';
|
||||||
import * as chromeReuse from '../../../../src/utils/browser/chrome-reuse';
|
import * as chromeReuse from '../../../../src/utils/browser/chrome-reuse';
|
||||||
import { getBrowserStatus } from '../../../../src/utils/browser/browser-status';
|
import {
|
||||||
|
getBrowserStatus,
|
||||||
|
} from '../../../../src/utils/browser/browser-status';
|
||||||
|
import { resolveOptionalBrowserAttachRuntime } from '../../../../src/utils/browser/browser-settings';
|
||||||
import * as codexDetector from '../../../../src/targets/codex-detector';
|
import * as codexDetector from '../../../../src/targets/codex-detector';
|
||||||
|
|
||||||
describe('browser status', () => {
|
describe('browser status', () => {
|
||||||
@@ -86,6 +89,48 @@ describe('browser status', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('bootstraps the managed default browser profile dir before reporting attach readiness', async () => {
|
||||||
|
mutateUnifiedConfig((config) => {
|
||||||
|
config.browser = {
|
||||||
|
claude: {
|
||||||
|
enabled: true,
|
||||||
|
user_data_dir: '',
|
||||||
|
devtools_port: 9222,
|
||||||
|
},
|
||||||
|
codex: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtimeSpy = spyOn(chromeReuse, 'resolveBrowserRuntimeEnv').mockRejectedValue(
|
||||||
|
new Error(
|
||||||
|
`Chrome reuse metadata not found: ${join(tempHome, '.ccs', 'browser', 'chrome-user-data', '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.title).toBe(
|
||||||
|
'Claude Browser Attach is waiting for a managed Chrome session.'
|
||||||
|
);
|
||||||
|
expect(status.claude.detail).toContain('CCS created the managed browser profile');
|
||||||
|
expect(status.claude.nextStep).toContain('--remote-debugging-port=9222');
|
||||||
|
expect(existsSync(join(tempHome, '.ccs', 'browser', 'chrome-user-data'))).toBe(true);
|
||||||
|
} finally {
|
||||||
|
runtimeSpy.mockRestore();
|
||||||
|
codexSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('prefers CCS_BROWSER_USER_DATA_DIR over config when an env override is present', async () => {
|
it('prefers CCS_BROWSER_USER_DATA_DIR over config when an env override is present', async () => {
|
||||||
mutateUnifiedConfig((config) => {
|
mutateUnifiedConfig((config) => {
|
||||||
config.browser = {
|
config.browser = {
|
||||||
@@ -169,6 +214,26 @@ describe('browser status', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns a managed attach warning when the configured DevTools port is unreachable', async () => {
|
||||||
|
const managedDir = join(tempHome, '.ccs', 'browser', 'chrome-user-data');
|
||||||
|
mkdirSync(managedDir, { recursive: true });
|
||||||
|
|
||||||
|
const runtime = await resolveOptionalBrowserAttachRuntime({
|
||||||
|
enabled: true,
|
||||||
|
source: 'config',
|
||||||
|
overrideActive: false,
|
||||||
|
userDataDir: managedDir,
|
||||||
|
devtoolsPort: 43123,
|
||||||
|
hasExplicitDevtoolsPort: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(runtime.runtimeEnv).toBeUndefined();
|
||||||
|
expect(runtime.warning).toContain(
|
||||||
|
'could not reach the attach-mode DevTools endpoint for the managed browser profile'
|
||||||
|
);
|
||||||
|
expect(runtime.warning).toContain('continue without browser tools');
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves legacy metadata-based port discovery when only CCS_BROWSER_PROFILE_DIR is set', async () => {
|
it('preserves legacy metadata-based port discovery when only CCS_BROWSER_PROFILE_DIR is set', async () => {
|
||||||
process.env.CCS_BROWSER_PROFILE_DIR = '/legacy-browser';
|
process.env.CCS_BROWSER_PROFILE_DIR = '/legacy-browser';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { mkdtempSync, rmSync } from 'node:fs';
|
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
||||||
import type { Server } from 'node:http';
|
import type { Server } from 'node:http';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
@@ -184,6 +184,9 @@ describe('browser routes', () => {
|
|||||||
userDataDir: join(tempHome, '.ccs', 'browser', 'chrome-user-data'),
|
userDataDir: join(tempHome, '.ccs', 'browser', 'chrome-user-data'),
|
||||||
devtoolsPort: 9333,
|
devtoolsPort: 9333,
|
||||||
});
|
});
|
||||||
|
expect(payload.browser.status.claude.state).toBe('browser_not_running');
|
||||||
|
expect(payload.browser.status.claude.detail).toContain('CCS created the managed browser profile');
|
||||||
|
expect(existsSync(join(tempHome, '.ccs', 'browser', 'chrome-user-data'))).toBe(true);
|
||||||
|
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const config = loadOrCreateUnifiedConfig();
|
||||||
expect(config.browser).toMatchObject({
|
expect(config.browser).toMatchObject({
|
||||||
|
|||||||
Reference in New Issue
Block a user