mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
fix(browser): harden runtime policy edge cases
This commit is contained in:
@@ -126,6 +126,9 @@ CCS still supports environment-variable overrides for backward compatibility.
|
||||
If an override is active, Browser status surfaces should report that the current session is being
|
||||
managed externally by environment variables.
|
||||
|
||||
The saved browser policy still controls default exposure. Env overrides change the effective attach
|
||||
path/port for the current shell; they do not bypass `policy: manual`.
|
||||
|
||||
Override precedence is:
|
||||
|
||||
1. `CCS_BROWSER_USER_DATA_DIR`
|
||||
|
||||
+11
-12
@@ -463,6 +463,16 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
args = normalizeLegacyCursorArgs(args);
|
||||
let browserLaunchOverride: BrowserLaunchOverride | undefined;
|
||||
try {
|
||||
const browserLaunchFlags = resolveBrowserLaunchFlagResolution(args);
|
||||
browserLaunchOverride = browserLaunchFlags.override;
|
||||
args = browserLaunchFlags.argsWithoutFlags;
|
||||
} catch (error) {
|
||||
console.error(fail((error as Error).message));
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
cliLogger.info('command.start', 'CLI invocation started', {
|
||||
command: args[0] || 'default',
|
||||
@@ -605,17 +615,6 @@ async function main(): Promise<void> {
|
||||
const detector = new ProfileDetector();
|
||||
|
||||
try {
|
||||
let browserLaunchOverride: BrowserLaunchOverride | undefined;
|
||||
try {
|
||||
const browserLaunchFlags = resolveBrowserLaunchFlagResolution(args);
|
||||
browserLaunchOverride = browserLaunchFlags.override;
|
||||
args = browserLaunchFlags.argsWithoutFlags;
|
||||
} catch (error) {
|
||||
console.error(fail((error as Error).message));
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect profile (strip --target flags before profile detection)
|
||||
const cleanArgs = stripTargetFlag(args);
|
||||
const { profile, remainingArgs } = detectProfile(cleanArgs);
|
||||
@@ -736,7 +735,7 @@ async function main(): Promise<void> {
|
||||
? resolveBrowserExposure(
|
||||
{
|
||||
enabled: claudeAttachConfig?.enabled ?? browserConfig.claude.enabled,
|
||||
policy: claudeAttachConfig?.overrideActive ? 'auto' : browserConfig.claude.policy,
|
||||
policy: browserConfig.claude.policy,
|
||||
},
|
||||
browserLaunchOverride
|
||||
)
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
} from '../../utils/image-analysis';
|
||||
import {
|
||||
appendBrowserToolArgs,
|
||||
type BrowserLaunchOverride,
|
||||
ensureBrowserMcpOrThrow,
|
||||
getBlockedBrowserOverrideWarning,
|
||||
getEffectiveClaudeBrowserAttachConfig,
|
||||
@@ -248,15 +249,23 @@ export async function execClaudeWithCLIProxy(
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
const browserLaunchFlags = resolveBrowserLaunchFlagResolution(argsWithoutProxy);
|
||||
const browserLaunchOverride = browserLaunchFlags.override;
|
||||
const argsWithoutBrowserFlags = browserLaunchFlags.argsWithoutFlags;
|
||||
let browserLaunchOverride: BrowserLaunchOverride | undefined;
|
||||
let argsWithoutBrowserFlags = argsWithoutProxy;
|
||||
try {
|
||||
const browserLaunchFlags = resolveBrowserLaunchFlagResolution(argsWithoutProxy);
|
||||
browserLaunchOverride = browserLaunchFlags.override;
|
||||
argsWithoutBrowserFlags = browserLaunchFlags.argsWithoutFlags;
|
||||
} catch (error) {
|
||||
console.error(fail((error as Error).message));
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
const browserConfig = getBrowserConfig();
|
||||
const browserAttachConfig = getEffectiveClaudeBrowserAttachConfig(browserConfig);
|
||||
const claudeBrowserExposure = resolveBrowserExposure(
|
||||
{
|
||||
enabled: browserAttachConfig.enabled,
|
||||
policy: browserAttachConfig.overrideActive ? 'auto' : browserConfig.claude.policy,
|
||||
policy: browserConfig.claude.policy,
|
||||
},
|
||||
browserLaunchOverride
|
||||
);
|
||||
|
||||
@@ -305,6 +305,8 @@ export const PROVIDER_FLAGS = [
|
||||
'--effort',
|
||||
'--1m',
|
||||
'--no-1m',
|
||||
'--browser',
|
||||
'--no-browser',
|
||||
'--logout',
|
||||
'--headless',
|
||||
'--port-forward',
|
||||
|
||||
@@ -84,6 +84,15 @@ function completeSubcommands(
|
||||
return uniqueStrings([...values, ...flags]).map((value) => suggestion(value));
|
||||
}
|
||||
|
||||
function completeBrowserPolicyArgs(tokensBeforeCurrent: string[]): CompletionSuggestion[] {
|
||||
const lastToken = tokensBeforeCurrent[tokensBeforeCurrent.length - 1];
|
||||
if (lastToken === '--all' || lastToken === '--claude' || lastToken === '--codex') {
|
||||
return completeSubcommands(['auto', 'manual'], ['--help', '-h']);
|
||||
}
|
||||
|
||||
return completeSubcommands(['--all', '--claude', '--codex'], ['--help', '-h']);
|
||||
}
|
||||
|
||||
function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSuggestion[] {
|
||||
const [command, subcommand] = tokensBeforeCurrent;
|
||||
const lastToken = tokensBeforeCurrent[tokensBeforeCurrent.length - 1];
|
||||
@@ -195,6 +204,12 @@ function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSugg
|
||||
['--help', '-h']
|
||||
);
|
||||
}
|
||||
if (subcommand === 'enable' || subcommand === 'disable') {
|
||||
return completeSubcommands(['claude', 'codex', 'all'], ['--help', '-h']);
|
||||
}
|
||||
if (subcommand === 'policy') {
|
||||
return completeBrowserPolicyArgs(tokensBeforeCurrent);
|
||||
}
|
||||
if (subcommand === 'setup' || subcommand === 'doctor') {
|
||||
return completeSubcommands([], ['--help', '-h']);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,14 @@ export function resolveBrowserLaunchFlagResolution(args: string[]): BrowserLaunc
|
||||
let override: BrowserLaunchOverride | undefined;
|
||||
const argsWithoutFlags: string[] = [];
|
||||
|
||||
for (const arg of args) {
|
||||
for (let index = 0; index < args.length; index++) {
|
||||
const arg = args[index];
|
||||
|
||||
if (arg === '--') {
|
||||
argsWithoutFlags.push(...args.slice(index));
|
||||
break;
|
||||
}
|
||||
|
||||
if (arg === ENABLE_BROWSER_FLAG) {
|
||||
if (override === 'force-disable') {
|
||||
throw new Error('Use either `--browser` or `--no-browser`, not both.');
|
||||
|
||||
@@ -65,7 +65,6 @@ async function buildClaudeBrowserStatus(
|
||||
): Promise<ClaudeBrowserStatus> {
|
||||
const effective = getEffectiveClaudeBrowserAttachConfig(browserConfig);
|
||||
const launchCommands = buildBrowserLaunchCommands(effective.userDataDir, effective.devtoolsPort);
|
||||
const managedBootstrap = ensureManagedBrowserUserDataDir(effective);
|
||||
const base: Omit<ClaudeBrowserStatus, 'state' | 'title' | 'detail' | 'nextStep'> = {
|
||||
enabled: effective.enabled,
|
||||
policy: browserConfig.claude.policy,
|
||||
@@ -90,6 +89,8 @@ async function buildClaudeBrowserStatus(
|
||||
};
|
||||
}
|
||||
|
||||
const managedBootstrap = ensureManagedBrowserUserDataDir(effective);
|
||||
|
||||
if (managedBootstrap.createdProfileDir) {
|
||||
const managedMessage = describeManagedBrowserAttachNotReady(
|
||||
effective,
|
||||
|
||||
@@ -20,6 +20,10 @@ interface BrowserRouteBody {
|
||||
};
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isValidBrowserPolicy(value: string): value is 'auto' | 'manual' {
|
||||
return value === 'auto' || value === 'manual';
|
||||
}
|
||||
@@ -56,25 +60,49 @@ router.get('/status', async (_req: Request, res: Response): Promise<void> => {
|
||||
});
|
||||
|
||||
router.put('/', async (req: Request, res: Response): Promise<void> => {
|
||||
if (
|
||||
req.body === null ||
|
||||
req.body === undefined ||
|
||||
typeof req.body !== 'object' ||
|
||||
Array.isArray(req.body)
|
||||
) {
|
||||
if (!isPlainObject(req.body)) {
|
||||
res.status(400).json({ error: 'Invalid request body. Must be an object.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { claude, codex } = req.body as BrowserRouteBody;
|
||||
if (claude && (typeof claude !== 'object' || Array.isArray(claude))) {
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const rootKeys = Object.keys(body);
|
||||
const unknownRootKeys = rootKeys.filter((key) => key !== 'claude' && key !== 'codex');
|
||||
if (unknownRootKeys.length > 0) {
|
||||
res.status(400).json({
|
||||
error: `Unknown browser config field(s): ${unknownRootKeys.join(', ')}.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { claude, codex } = body as BrowserRouteBody;
|
||||
if (Object.prototype.hasOwnProperty.call(body, 'claude') && !isPlainObject(claude)) {
|
||||
res.status(400).json({ error: 'Invalid value for claude. Must be an object.' });
|
||||
return;
|
||||
}
|
||||
if (codex && (typeof codex !== 'object' || Array.isArray(codex))) {
|
||||
if (Object.prototype.hasOwnProperty.call(body, 'codex') && !isPlainObject(codex)) {
|
||||
res.status(400).json({ error: 'Invalid value for codex. Must be an object.' });
|
||||
return;
|
||||
}
|
||||
const unknownClaudeKeys = Object.keys(claude ?? {}).filter(
|
||||
(key) =>
|
||||
key !== 'enabled' && key !== 'policy' && key !== 'userDataDir' && key !== 'devtoolsPort'
|
||||
);
|
||||
if (unknownClaudeKeys.length > 0) {
|
||||
res.status(400).json({
|
||||
error: `Unknown claude browser field(s): ${unknownClaudeKeys.join(', ')}.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const unknownCodexKeys = Object.keys(codex ?? {}).filter(
|
||||
(key) => key !== 'enabled' && key !== 'policy'
|
||||
);
|
||||
if (unknownCodexKeys.length > 0) {
|
||||
res.status(400).json({
|
||||
error: `Unknown codex browser field(s): ${unknownCodexKeys.join(', ')}.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (claude?.enabled !== undefined && typeof claude.enabled !== 'boolean') {
|
||||
res.status(400).json({ error: 'Invalid value for claude.enabled. Must be a boolean.' });
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { afterEach, beforeEach, describe, expect, it, jest } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
execClaudeWithCLIProxy,
|
||||
hasGitLabTokenLoginFlag,
|
||||
readOptionValue,
|
||||
} from '../../../src/cliproxy/executor/index';
|
||||
@@ -40,3 +44,44 @@ describe('readOptionValue', () => {
|
||||
expect(hasGitLabTokenLoginFlag(['--gitlab-url', 'https://gitlab.example.com'])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execClaudeWithCLIProxy browser flag validation', () => {
|
||||
let tmpHome = '';
|
||||
let fakeClaudePath = '';
|
||||
let originalCcsHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cliproxy-executor-'));
|
||||
fakeClaudePath = path.join(tmpHome, 'fake-claude.sh');
|
||||
fs.writeFileSync(fakeClaudePath, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
||||
fs.chmodSync(fakeClaudePath, 0o755);
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.CCS_HOME = tmpHome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
}
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('exits cleanly when conflicting browser launch flags are provided', async () => {
|
||||
const exitSpy = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((() => undefined as never) as typeof process.exit);
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
await execClaudeWithCLIProxy(fakeClaudePath, 'gemini', ['--browser', '--no-browser'], {});
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
expect(errorSpy).toHaveBeenCalledWith('[X] Use either `--browser` or `--no-browser`, not both.');
|
||||
} finally {
|
||||
exitSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,6 +148,18 @@ describe('completion backend', () => {
|
||||
expect(suggestionValues(['browser', 'doctor'])).toEqual(
|
||||
expect.arrayContaining(['--help', '-h'])
|
||||
);
|
||||
expect(suggestionValues(['browser', 'enable'])).toEqual(
|
||||
expect.arrayContaining(['claude', 'codex', 'all'])
|
||||
);
|
||||
expect(suggestionValues(['browser', 'policy'])).toEqual(
|
||||
expect.arrayContaining(['--all', '--claude', '--codex'])
|
||||
);
|
||||
expect(suggestionValues(['browser', 'policy', '--all'])).toEqual(
|
||||
expect.arrayContaining(['auto', 'manual'])
|
||||
);
|
||||
expect(suggestionValues(['codex'])).toEqual(
|
||||
expect.arrayContaining(['--browser', '--no-browser'])
|
||||
);
|
||||
});
|
||||
|
||||
test('treats cursor as a provider shortcut in completion', () => {
|
||||
|
||||
@@ -396,6 +396,24 @@ process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
it('strips browser launch flags before native codex passthrough diagnostics', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const result = runCodexAlias(['--version', '--browser'], {
|
||||
...process.env,
|
||||
CI: '1',
|
||||
NO_COLOR: '1',
|
||||
CCS_HOME: tmpHome,
|
||||
CCS_CODEX_PATH: fakeCodexPath,
|
||||
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
||||
CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test',
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain('codex-cli 9.9.9-test');
|
||||
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]);
|
||||
});
|
||||
|
||||
for (const helpFlag of ['--help', '-h']) {
|
||||
it(`passes ccsx ${helpFlag} straight through to the native Codex binary`, () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
@@ -431,4 +431,86 @@ server.listen(0, '127.0.0.1', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps Claude browser attach hidden under manual policy until --browser is passed', async () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const mockServerScriptPath = path.join(tmpHome, 'mock-devtools-server-manual-default.js');
|
||||
const mockServerPortPath = path.join(tmpHome, 'mock-devtools-port-manual-default.txt');
|
||||
fs.writeFileSync(
|
||||
mockServerScriptPath,
|
||||
`const { createServer } = require('http');
|
||||
const fs = require('fs');
|
||||
const server = createServer((req, res) => {
|
||||
if (req.url === '/json/version') {
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ Browser: 'Chrome/136.0.0.0', webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/browser/manual-default-target' }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end('not found');
|
||||
});
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const address = server.address();
|
||||
fs.writeFileSync(${JSON.stringify(mockServerPortPath)}, String(address.port), 'utf8');
|
||||
});
|
||||
`,
|
||||
'utf8'
|
||||
);
|
||||
|
||||
devtoolsServer = spawn(process.execPath, [mockServerScriptPath], {
|
||||
stdio: 'ignore',
|
||||
env: baseEnv,
|
||||
});
|
||||
|
||||
const port = await waitForMockDevtoolsPort(mockServerPortPath);
|
||||
await waitForDevtoolsVersionEndpoint(port);
|
||||
|
||||
fs.mkdirSync(browserProfileDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(browserProfileDir, 'DevToolsActivePort'),
|
||||
`${port}\n/devtools/browser/manual-default-target`,
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.CCS_HOME = tmpHome;
|
||||
|
||||
try {
|
||||
mutateUnifiedConfig((config) => {
|
||||
config.browser = {
|
||||
claude: {
|
||||
enabled: true,
|
||||
policy: 'manual',
|
||||
user_data_dir: browserProfileDir,
|
||||
devtools_port: Number.parseInt(port, 10),
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
policy: 'auto',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const hiddenResult = runCcs(['default', 'smoke'], {
|
||||
...baseEnv,
|
||||
});
|
||||
expect(hiddenResult.status).toBe(0);
|
||||
expect(fs.readFileSync(claudeArgsLogPath, 'utf8')).not.toContain(BROWSER_PROMPT_SNIPPET);
|
||||
expect(fs.readFileSync(claudeEnvLogPath, 'utf8')).not.toContain(browserProfileDir);
|
||||
|
||||
const forcedResult = runCcs(['default', '--browser', 'smoke'], {
|
||||
...baseEnv,
|
||||
});
|
||||
expect(forcedResult.status).toBe(0);
|
||||
expect(fs.readFileSync(claudeArgsLogPath, 'utf8')).toContain(BROWSER_PROMPT_SNIPPET);
|
||||
expect(fs.readFileSync(claudeEnvLogPath, 'utf8')).toContain(browserProfileDir);
|
||||
} finally {
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -389,4 +389,92 @@ server.listen(0, '127.0.0.1', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps settings-profile Claude attach hidden under manual policy until --browser is passed', async () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const mockServerScriptPath = path.join(tmpHome, 'mock-devtools-server-manual-settings.js');
|
||||
const mockServerPortPath = path.join(tmpHome, 'mock-devtools-port-manual-settings.txt');
|
||||
fs.writeFileSync(
|
||||
mockServerScriptPath,
|
||||
`const { createServer } = require('http');
|
||||
const fs = require('fs');
|
||||
const server = createServer((req, res) => {
|
||||
if (req.url === '/json/version') {
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ Browser: 'Chrome/136.0.0.0', webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/browser/manual-settings-target' }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end('not found');
|
||||
});
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const address = server.address();
|
||||
fs.writeFileSync(${JSON.stringify(mockServerPortPath)}, String(address.port), 'utf8');
|
||||
});
|
||||
`,
|
||||
'utf8'
|
||||
);
|
||||
|
||||
devtoolsServer = spawn(process.execPath, [mockServerScriptPath], {
|
||||
stdio: 'ignore',
|
||||
env: baseEnv,
|
||||
});
|
||||
|
||||
const startDeadline = Date.now() + 5000;
|
||||
while (!fs.existsSync(mockServerPortPath)) {
|
||||
if (Date.now() > startDeadline) {
|
||||
throw new Error('Timed out waiting for mock DevTools server to start');
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
const port = fs.readFileSync(mockServerPortPath, 'utf8').trim();
|
||||
|
||||
fs.mkdirSync(browserProfileDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(browserProfileDir, 'DevToolsActivePort'),
|
||||
`${port}\n/devtools/browser/manual-settings-target`,
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.CCS_HOME = tmpHome;
|
||||
|
||||
try {
|
||||
mutateUnifiedConfig((config) => {
|
||||
config.browser = {
|
||||
claude: {
|
||||
enabled: true,
|
||||
policy: 'manual',
|
||||
user_data_dir: browserProfileDir,
|
||||
devtools_port: Number.parseInt(port, 10),
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
policy: 'auto',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const hiddenResult = runCcs(['glm', 'smoke'], {
|
||||
...baseEnv,
|
||||
});
|
||||
expect(hiddenResult.status).toBe(0);
|
||||
expect(fs.readFileSync(claudeArgsLogPath, 'utf8')).not.toContain(BROWSER_PROMPT_SNIPPET);
|
||||
expect(fs.readFileSync(claudeEnvLogPath, 'utf8')).not.toContain(browserProfileDir);
|
||||
|
||||
const forcedResult = runCcs(['glm', '--browser', 'smoke'], {
|
||||
...baseEnv,
|
||||
});
|
||||
expect(forcedResult.status).toBe(0);
|
||||
expect(fs.readFileSync(claudeArgsLogPath, 'utf8')).toContain(BROWSER_PROMPT_SNIPPET);
|
||||
expect(fs.readFileSync(claudeEnvLogPath, 'utf8')).toContain(browserProfileDir);
|
||||
} finally {
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,13 @@ describe('browser policy', () => {
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
|
||||
@@ -88,6 +88,7 @@ describe('browser status', () => {
|
||||
serverName: 'ccs_browser',
|
||||
supportsConfigOverrides: true,
|
||||
});
|
||||
expect(existsSync(join(tempHome, '.ccs', 'browser', 'chrome-user-data'))).toBe(false);
|
||||
} finally {
|
||||
codexSpy.mockRestore();
|
||||
}
|
||||
|
||||
@@ -226,6 +226,38 @@ describe('browser routes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects null browser lane payloads instead of treating them as no-ops', async () => {
|
||||
const response = await fetch(`${baseUrl}/api/browser`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
claude: null,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.json()).toEqual({
|
||||
error: 'Invalid value for claude. Must be an object.',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects unknown browser config fields instead of silently ignoring them', async () => {
|
||||
const response = await fetch(`${baseUrl}/api/browser`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
codxe: {
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.json()).toEqual({
|
||||
error: 'Unknown browser config field(s): codxe.',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects invalid browser policy values at the route boundary', async () => {
|
||||
const response = await fetch(`${baseUrl}/api/browser`, {
|
||||
method: 'PUT',
|
||||
@@ -242,4 +274,21 @@ describe('browser routes', () => {
|
||||
error: 'Invalid value for codex.policy. Must be auto or manual.',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects unknown nested browser lane fields instead of silently ignoring them', async () => {
|
||||
const response = await fetch(`${baseUrl}/api/browser`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
claude: {
|
||||
userDatDir: '/tmp/typo',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.json()).toEqual({
|
||||
error: 'Unknown claude browser field(s): userDatDir.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user