mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 18:15:40 +00:00
Merge upstream/dev into feat/browser-mcp-hover-phase11e
This commit is contained in:
commit
96db93ee6c
98 files changed
+6334
-1745
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
resolveBrowserExposure,
|
||||
resolveBrowserLaunchFlagResolution,
|
||||
} from '../../../../src/utils/browser/browser-policy';
|
||||
|
||||
describe('browser policy', () => {
|
||||
it('strips browser launch flags and records the override', () => {
|
||||
expect(resolveBrowserLaunchFlagResolution(['glm', '--browser', 'check app'])).toEqual({
|
||||
override: 'force-enable',
|
||||
argsWithoutFlags: ['glm', 'check app'],
|
||||
});
|
||||
expect(resolveBrowserLaunchFlagResolution(['glm', '--no-browser', 'check app'])).toEqual({
|
||||
override: 'force-disable',
|
||||
argsWithoutFlags: ['glm', 'check app'],
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects conflicting browser launch flags', () => {
|
||||
expect(() => resolveBrowserLaunchFlagResolution(['--browser', '--no-browser'])).toThrow(
|
||||
'Use either `--browser` or `--no-browser`, not both.'
|
||||
);
|
||||
});
|
||||
|
||||
it('stops parsing browser launch flags after the option terminator', () => {
|
||||
expect(resolveBrowserLaunchFlagResolution(['glm', '--', '--browser', 'literal'])).toEqual({
|
||||
override: undefined,
|
||||
argsWithoutFlags: ['glm', '--', '--browser', 'literal'],
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves browser exposure from saved policy and one-run overrides', () => {
|
||||
expect(resolveBrowserExposure({ enabled: true, policy: 'auto' })).toMatchObject({
|
||||
exposeByDefault: true,
|
||||
exposeForLaunch: true,
|
||||
});
|
||||
expect(resolveBrowserExposure({ enabled: true, policy: 'manual' })).toMatchObject({
|
||||
exposeByDefault: false,
|
||||
exposeForLaunch: false,
|
||||
});
|
||||
expect(
|
||||
resolveBrowserExposure({ enabled: true, policy: 'manual' }, 'force-enable')
|
||||
).toMatchObject({
|
||||
exposeByDefault: false,
|
||||
exposeForLaunch: true,
|
||||
});
|
||||
expect(
|
||||
resolveBrowserExposure({ enabled: true, policy: 'auto' }, 'force-disable')
|
||||
).toMatchObject({
|
||||
exposeByDefault: true,
|
||||
exposeForLaunch: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import type { UnifiedConfig } from '../../../../src/config/unified-config-types';
|
||||
import { runBrowserSetup, type BrowserSetupDeps } from '../../../../src/utils/browser/browser-setup';
|
||||
|
||||
function createUnifiedConfig(userDataDir: string): UnifiedConfig {
|
||||
return {
|
||||
version: 12,
|
||||
default: undefined,
|
||||
profiles: {},
|
||||
profile_targets: {},
|
||||
copilot: {
|
||||
enabled: false,
|
||||
prompt: '',
|
||||
command: '',
|
||||
args: [],
|
||||
env: {},
|
||||
auto_install: false,
|
||||
},
|
||||
cursor: {
|
||||
enabled: false,
|
||||
model: '',
|
||||
port: 3891,
|
||||
daemon_mode: false,
|
||||
auth: {
|
||||
token: '',
|
||||
},
|
||||
},
|
||||
websearch: {
|
||||
enabled: false,
|
||||
providers: {},
|
||||
},
|
||||
browser: {
|
||||
claude: {
|
||||
enabled: false,
|
||||
user_data_dir: userDataDir,
|
||||
devtools_port: 9222,
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
image_analysis: {
|
||||
enabled: false,
|
||||
providers: {},
|
||||
},
|
||||
global_env: {},
|
||||
cliproxy_server: {
|
||||
mode: 'local',
|
||||
remote: {
|
||||
enabled: false,
|
||||
host: '',
|
||||
port: 0,
|
||||
protocol: 'http',
|
||||
auth_token: '',
|
||||
management_key: '',
|
||||
},
|
||||
local: {
|
||||
port: 8085,
|
||||
auto_start: true,
|
||||
},
|
||||
},
|
||||
cliproxy_safety: {
|
||||
concurrent_limit: 1,
|
||||
cooldown_seconds: 0,
|
||||
shared_responsibility: false,
|
||||
},
|
||||
quota_management: {
|
||||
enabled: false,
|
||||
},
|
||||
thinking: {
|
||||
mode: 'auto',
|
||||
show_warnings: true,
|
||||
},
|
||||
official_channels: {
|
||||
enabled: false,
|
||||
selected: [],
|
||||
unattended: false,
|
||||
},
|
||||
dashboard_auth: {
|
||||
enabled: false,
|
||||
users: [],
|
||||
session_secret: '',
|
||||
},
|
||||
logging: {
|
||||
enabled: true,
|
||||
profile_starts: true,
|
||||
delegation_calls: true,
|
||||
proxy_requests: false,
|
||||
quota_polls: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('browser setup', () => {
|
||||
let tempDir = '';
|
||||
|
||||
afterEach(() => {
|
||||
if (tempDir) {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
tempDir = '';
|
||||
}
|
||||
});
|
||||
|
||||
it('enables Claude browser attach and prepares the managed default path', async () => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-setup-'));
|
||||
const config = createUnifiedConfig(join(tempDir, 'browser-profile'));
|
||||
config.browser.claude.user_data_dir = '';
|
||||
|
||||
const deps: BrowserSetupDeps = {
|
||||
getBrowserConfig: () => config.browser,
|
||||
mutateUnifiedConfig: (mutator) => {
|
||||
mutator(config);
|
||||
return config;
|
||||
},
|
||||
ensureBrowserMcp: () => true,
|
||||
getBrowserStatus: async () =>
|
||||
({
|
||||
claude: {
|
||||
enabled: true,
|
||||
source: 'config',
|
||||
overrideActive: false,
|
||||
state: 'ready',
|
||||
title: 'Claude Browser Attach is ready.',
|
||||
detail: 'ready',
|
||||
nextStep: 'Launch Claude.',
|
||||
effectiveUserDataDir: config.browser.claude.user_data_dir,
|
||||
recommendedUserDataDir: config.browser.claude.user_data_dir,
|
||||
devtoolsPort: 9222,
|
||||
managedMcpServerName: 'ccs-browser',
|
||||
managedMcpServerPath: '/tmp/ccs-browser-server.cjs',
|
||||
launchCommands: {
|
||||
darwin: 'open -na "Google Chrome" --args',
|
||||
linux: 'google-chrome --remote-debugging-port=9222',
|
||||
win32: 'chrome.exe --remote-debugging-port=9222',
|
||||
},
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
state: 'enabled',
|
||||
title: 'Codex Browser Tools are enabled.',
|
||||
detail: 'ready',
|
||||
nextStep: 'Use Codex.',
|
||||
serverName: 'ccs_browser',
|
||||
supportsConfigOverrides: true,
|
||||
binaryPath: '/usr/local/bin/codex',
|
||||
},
|
||||
}) as Awaited<ReturnType<BrowserSetupDeps['getBrowserStatus']>>,
|
||||
};
|
||||
|
||||
const result = await runBrowserSetup(deps);
|
||||
|
||||
expect(result.configUpdated).toBe(true);
|
||||
expect(result.createdUserDataDir).toBe(true);
|
||||
expect(result.ready).toBe(true);
|
||||
expect(config.browser.claude.enabled).toBe(true);
|
||||
expect(config.browser.claude.user_data_dir).not.toBe('');
|
||||
});
|
||||
|
||||
it('does not create a custom override path during setup', async () => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-setup-'));
|
||||
const config = createUnifiedConfig(join(tempDir, 'browser-profile'));
|
||||
config.browser.claude.enabled = true;
|
||||
|
||||
const deps: BrowserSetupDeps = {
|
||||
getBrowserConfig: () => config.browser,
|
||||
mutateUnifiedConfig: (mutator) => {
|
||||
mutator(config);
|
||||
return config;
|
||||
},
|
||||
ensureBrowserMcp: () => true,
|
||||
getBrowserStatus: async () =>
|
||||
({
|
||||
claude: {
|
||||
enabled: true,
|
||||
source: 'CCS_BROWSER_USER_DATA_DIR',
|
||||
overrideActive: true,
|
||||
state: 'browser_not_running',
|
||||
title: 'Claude Browser Attach could not find a running browser session.',
|
||||
detail: 'override active',
|
||||
nextStep: 'Run `ccs browser setup` to configure and start the managed browser session.',
|
||||
effectiveUserDataDir: config.browser.claude.user_data_dir,
|
||||
recommendedUserDataDir: config.browser.claude.user_data_dir,
|
||||
devtoolsPort: 9222,
|
||||
managedMcpServerName: 'ccs-browser',
|
||||
managedMcpServerPath: '/tmp/ccs-browser-server.cjs',
|
||||
launchCommands: {
|
||||
darwin: 'open -na "Google Chrome" --args',
|
||||
linux: 'google-chrome --remote-debugging-port=9222',
|
||||
win32: 'chrome.exe --remote-debugging-port=9222',
|
||||
},
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
state: 'enabled',
|
||||
title: 'Codex Browser Tools are enabled.',
|
||||
detail: 'ready',
|
||||
nextStep: 'Use Codex.',
|
||||
serverName: 'ccs_browser',
|
||||
supportsConfigOverrides: true,
|
||||
binaryPath: '/usr/local/bin/codex',
|
||||
},
|
||||
}) as Awaited<ReturnType<BrowserSetupDeps['getBrowserStatus']>>,
|
||||
};
|
||||
|
||||
const result = await runBrowserSetup(deps);
|
||||
|
||||
expect(result.createdUserDataDir).toBe(false);
|
||||
expect(result.overrideActive).toBe(false);
|
||||
expect(result.ready).toBe(false);
|
||||
expect(result.notes[0]).toContain('did not create the current browser user-data dir');
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,19 @@
|
||||
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 { join } from 'node:path';
|
||||
import { mutateUnifiedConfig } from '../../../../src/config/unified-config-loader';
|
||||
import {
|
||||
getBrowserConfig,
|
||||
mutateUnifiedConfig,
|
||||
saveUnifiedConfig,
|
||||
} from '../../../../src/config/unified-config-loader';
|
||||
import { createEmptyUnifiedConfig } from '../../../../src/config/unified-config-types';
|
||||
import * as chromeReuse from '../../../../src/utils/browser/chrome-reuse';
|
||||
import { getBrowserStatus } from '../../../../src/utils/browser/browser-status';
|
||||
import {
|
||||
getEffectiveClaudeBrowserAttachConfig,
|
||||
resolveOptionalBrowserAttachRuntime,
|
||||
} from '../../../../src/utils/browser/browser-settings';
|
||||
import * as codexDetector from '../../../../src/targets/codex-detector';
|
||||
|
||||
describe('browser status', () => {
|
||||
@@ -55,7 +64,7 @@ describe('browser status', () => {
|
||||
rmSync(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns a disabled Claude lane with the recommended managed user-data dir by default', async () => {
|
||||
it('returns disabled/manual browser lanes with the recommended managed user-data dir by default', async () => {
|
||||
const codexSpy = spyOn(codexDetector, 'getCodexBinaryInfo').mockReturnValue({
|
||||
path: '/usr/local/bin/codex',
|
||||
needsShell: false,
|
||||
@@ -69,21 +78,110 @@ describe('browser status', () => {
|
||||
expect(status.claude).toMatchObject({
|
||||
enabled: false,
|
||||
state: 'disabled',
|
||||
policy: 'manual',
|
||||
source: 'config',
|
||||
effectiveUserDataDir: join(tempHome, '.ccs', 'browser', 'chrome-user-data'),
|
||||
devtoolsPort: 9222,
|
||||
evalMode: 'readonly',
|
||||
managedMcpServerName: 'ccs-browser',
|
||||
});
|
||||
expect(status.claude.launchCommands.linux).toContain('--remote-debugging-port=9222');
|
||||
expect(status.claude.detail).toContain('off by default');
|
||||
expect(status.codex).toMatchObject({
|
||||
enabled: false,
|
||||
policy: 'manual',
|
||||
state: 'disabled',
|
||||
serverName: 'ccs_browser',
|
||||
supportsConfigOverrides: false,
|
||||
});
|
||||
expect(status.codex.detail).toContain('off by default');
|
||||
expect(existsSync(join(tempHome, '.ccs', 'browser', 'chrome-user-data'))).toBe(false);
|
||||
} finally {
|
||||
codexSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves missing saved browser policies to manual while preserving explicit enabled values', async () => {
|
||||
const config = createEmptyUnifiedConfig();
|
||||
config.browser = {
|
||||
claude: {
|
||||
enabled: true,
|
||||
user_data_dir: '/tmp/explicit-claude',
|
||||
devtools_port: 9333,
|
||||
} as typeof config.browser.claude,
|
||||
codex: {
|
||||
enabled: true,
|
||||
} as typeof config.browser.codex,
|
||||
};
|
||||
saveUnifiedConfig(config);
|
||||
|
||||
const runtimeSpy = spyOn(chromeReuse, 'resolveBrowserRuntimeEnv').mockRejectedValue(
|
||||
new Error('Chrome reuse metadata not found: /tmp/explicit-claude/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).toMatchObject({
|
||||
enabled: true,
|
||||
policy: 'manual',
|
||||
effectiveUserDataDir: '/tmp/explicit-claude',
|
||||
devtoolsPort: 9333,
|
||||
});
|
||||
expect(status.codex).toMatchObject({
|
||||
enabled: true,
|
||||
policy: 'manual',
|
||||
state: 'enabled',
|
||||
serverName: 'ccs_browser',
|
||||
evalMode: 'readonly',
|
||||
supportsConfigOverrides: true,
|
||||
});
|
||||
} finally {
|
||||
runtimeSpy.mockRestore();
|
||||
codexSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('bootstraps the managed default browser profile dir before reporting attach readiness', async () => {
|
||||
mutateUnifiedConfig((config) => {
|
||||
config.browser = {
|
||||
claude: {
|
||||
enabled: true,
|
||||
policy: 'auto',
|
||||
user_data_dir: '',
|
||||
devtools_port: 9222,
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
policy: 'auto',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
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 not ready yet.');
|
||||
expect(status.claude.detail).toContain('created the managed browser profile directory');
|
||||
expect(status.claude.nextStep).toContain('ccs browser setup');
|
||||
expect(existsSync(join(tempHome, '.ccs', 'browser', 'chrome-user-data'))).toBe(true);
|
||||
} finally {
|
||||
runtimeSpy.mockRestore();
|
||||
codexSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
@@ -93,13 +191,13 @@ describe('browser status', () => {
|
||||
config.browser = {
|
||||
claude: {
|
||||
enabled: true,
|
||||
policy: 'auto',
|
||||
user_data_dir: '/config-browser',
|
||||
devtools_port: 9333,
|
||||
eval_mode: 'readwrite',
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
eval_mode: 'disabled',
|
||||
policy: 'auto',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -112,7 +210,6 @@ describe('browser status', () => {
|
||||
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',
|
||||
CCS_BROWSER_EVAL_MODE: 'readwrite',
|
||||
});
|
||||
const codexSpy = spyOn(codexDetector, 'getCodexBinaryInfo').mockReturnValue({
|
||||
path: '/usr/local/bin/codex',
|
||||
@@ -130,29 +227,70 @@ describe('browser status', () => {
|
||||
source: 'CCS_BROWSER_USER_DATA_DIR',
|
||||
effectiveUserDataDir: '/env-browser',
|
||||
devtoolsPort: 9444,
|
||||
evalMode: 'readwrite',
|
||||
});
|
||||
expect(status.claude.runtimeEnv?.CCS_BROWSER_DEVTOOLS_PORT).toBe('9444');
|
||||
expect(status.claude.runtimeEnv?.CCS_BROWSER_EVAL_MODE).toBe('readwrite');
|
||||
expect(status.codex.evalMode).toBe('disabled');
|
||||
} finally {
|
||||
runtimeSpy.mockRestore();
|
||||
codexSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('reports browser_not_running when attach metadata is missing', async () => {
|
||||
it('returns a short managed attach warning when the managed browser dir is missing', async () => {
|
||||
mutateUnifiedConfig((config) => {
|
||||
config.browser = {
|
||||
claude: {
|
||||
enabled: true,
|
||||
user_data_dir: '/tmp/browser-profile',
|
||||
policy: 'auto',
|
||||
user_data_dir: '',
|
||||
devtools_port: 9222,
|
||||
eval_mode: 'readonly',
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
eval_mode: 'readonly',
|
||||
policy: 'auto',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const resolution = await resolveOptionalBrowserAttachRuntime(
|
||||
getEffectiveClaudeBrowserAttachConfig(getBrowserConfig())
|
||||
);
|
||||
|
||||
expect(resolution.runtimeEnv).toBeUndefined();
|
||||
expect(resolution.warning).toContain('Claude Browser Attach is not ready yet.');
|
||||
expect(resolution.warning).toContain('ccs browser setup');
|
||||
expect(resolution.warning).toContain('ccs browser doctor');
|
||||
});
|
||||
|
||||
it('returns the same 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 resolution = await resolveOptionalBrowserAttachRuntime({
|
||||
enabled: true,
|
||||
source: 'config',
|
||||
overrideActive: false,
|
||||
userDataDir: managedDir,
|
||||
devtoolsPort: 43123,
|
||||
hasExplicitDevtoolsPort: true,
|
||||
});
|
||||
|
||||
expect(resolution.runtimeEnv).toBeUndefined();
|
||||
expect(resolution.warning).toContain('Claude Browser Attach is not ready yet.');
|
||||
expect(resolution.warning).toContain('ccs browser setup');
|
||||
});
|
||||
|
||||
it('reports browser_not_running when attach metadata is missing for a custom path', async () => {
|
||||
mutateUnifiedConfig((config) => {
|
||||
config.browser = {
|
||||
claude: {
|
||||
enabled: true,
|
||||
policy: 'auto',
|
||||
user_data_dir: '/tmp/browser-profile',
|
||||
devtools_port: 9222,
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
policy: 'auto',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -188,7 +326,6 @@ describe('browser status', () => {
|
||||
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',
|
||||
CCS_BROWSER_EVAL_MODE: 'readonly',
|
||||
});
|
||||
const codexSpy = spyOn(codexDetector, 'getCodexBinaryInfo').mockReturnValue({
|
||||
path: '/usr/local/bin/codex',
|
||||
@@ -203,10 +340,8 @@ describe('browser status', () => {
|
||||
expect(runtimeSpy.mock.calls[0]?.[0]).toEqual({
|
||||
profileDir: '/legacy-browser',
|
||||
devtoolsPort: undefined,
|
||||
evalMode: 'readonly',
|
||||
});
|
||||
expect(status.claude.runtimeEnv?.CCS_BROWSER_DEVTOOLS_PORT).toBe('50123');
|
||||
expect(status.claude.runtimeEnv?.CCS_BROWSER_EVAL_MODE).toBe('readonly');
|
||||
} finally {
|
||||
runtimeSpy.mockRestore();
|
||||
codexSpy.mockRestore();
|
||||
@@ -218,13 +353,13 @@ describe('browser status', () => {
|
||||
config.browser = {
|
||||
claude: {
|
||||
enabled: true,
|
||||
policy: 'auto',
|
||||
user_data_dir: '/tmp/config-browser',
|
||||
devtools_port: 9222,
|
||||
eval_mode: 'disabled',
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
eval_mode: 'readwrite',
|
||||
policy: 'auto',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -235,7 +370,6 @@ describe('browser status', () => {
|
||||
CCS_BROWSER_DEVTOOLS_PORT: '9222',
|
||||
CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:9222',
|
||||
CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/config',
|
||||
CCS_BROWSER_EVAL_MODE: 'disabled',
|
||||
});
|
||||
const codexSpy = spyOn(codexDetector, 'getCodexBinaryInfo').mockReturnValue({
|
||||
path: '/usr/local/bin/codex',
|
||||
@@ -250,7 +384,6 @@ describe('browser status', () => {
|
||||
expect(runtimeSpy.mock.calls[0]?.[0]).toEqual({
|
||||
profileDir: '/tmp/config-browser',
|
||||
devtoolsPort: '9222',
|
||||
evalMode: 'disabled',
|
||||
});
|
||||
} finally {
|
||||
runtimeSpy.mockRestore();
|
||||
|
||||
@@ -266,6 +266,7 @@ describe('CLAUDECODE environment stripping', () => {
|
||||
expect(spawnCalls.length).toBeGreaterThan(0);
|
||||
const env = spawnCalls[0].options?.env as NodeJS.ProcessEnv;
|
||||
expect(Object.keys(env).map((k) => k.toUpperCase())).not.toContain('CLAUDECODE');
|
||||
expect(spawnCalls[0].options?.shell).toBe('cmd.exe');
|
||||
});
|
||||
|
||||
it('execClaude sets DISABLE_AUTOUPDATER=1 when preferences.auto_update is false', () => {
|
||||
|
||||
@@ -81,6 +81,56 @@ describe('escapeShellArg', () => {
|
||||
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
|
||||
expect(escapeShellArg('hello!')).toBe('"hello^^!"');
|
||||
});
|
||||
|
||||
it('prefers ComSpec when resolving the escaped command shell', async () => {
|
||||
const originalComSpec = process.env.ComSpec;
|
||||
const originalCOMSPEC = process.env.COMSPEC;
|
||||
|
||||
try {
|
||||
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe';
|
||||
delete process.env.COMSPEC;
|
||||
const { getWindowsEscapedCommandShell } = await import(
|
||||
'../../../src/utils/shell-executor'
|
||||
);
|
||||
expect(getWindowsEscapedCommandShell()).toBe('C:\\Windows\\System32\\cmd.exe');
|
||||
} finally {
|
||||
if (originalComSpec === undefined) delete process.env.ComSpec;
|
||||
else process.env.ComSpec = originalComSpec;
|
||||
if (originalCOMSPEC === undefined) delete process.env.COMSPEC;
|
||||
else process.env.COMSPEC = originalCOMSPEC;
|
||||
}
|
||||
});
|
||||
|
||||
it('falls back to cmd.exe when ComSpec is unavailable', async () => {
|
||||
const originalComSpec = process.env.ComSpec;
|
||||
const originalCOMSPEC = process.env.COMSPEC;
|
||||
|
||||
try {
|
||||
delete process.env.ComSpec;
|
||||
delete process.env.COMSPEC;
|
||||
const { getWindowsEscapedCommandShell } = await import(
|
||||
'../../../src/utils/shell-executor'
|
||||
);
|
||||
expect(getWindowsEscapedCommandShell()).toBe('cmd.exe');
|
||||
} finally {
|
||||
if (originalComSpec === undefined) delete process.env.ComSpec;
|
||||
else process.env.ComSpec = originalComSpec;
|
||||
if (originalCOMSPEC === undefined) delete process.env.COMSPEC;
|
||||
else process.env.COMSPEC = originalCOMSPEC;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWindowsEscapedCommandShell', () => {
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
});
|
||||
|
||||
it('returns shell=true outside Windows if called defensively', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
const { getWindowsEscapedCommandShell } = await import('../../../src/utils/shell-executor');
|
||||
expect(getWindowsEscapedCommandShell()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user