fix: degrade WebSearch launch provisioning failures (#1571)

This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-06-18 22:56:20 -04:00
1 parent 813a7a043a
commit 1a0398579a
13 files changed
+225 -33

No files matched your search

+2 -2
View File
@@ -53,7 +53,7 @@
"src/cliproxy/executor/auth-coordinator.ts:176", "src/cliproxy/executor/auth-coordinator.ts:176",
"src/cliproxy/executor/auth-coordinator.ts:317", "src/cliproxy/executor/auth-coordinator.ts:317",
"src/cliproxy/executor/browser-launch-setup.ts:120", "src/cliproxy/executor/browser-launch-setup.ts:120",
"src/cliproxy/executor/index.ts:378", "src/cliproxy/executor/index.ts:391",
"src/cliproxy/executor/lifecycle-manager.ts:129", "src/cliproxy/executor/lifecycle-manager.ts:129",
"src/cliproxy/executor/lifecycle-manager.ts:162", "src/cliproxy/executor/lifecycle-manager.ts:162",
"src/cliproxy/executor/lifecycle-manager.ts:59", "src/cliproxy/executor/lifecycle-manager.ts:59",
@@ -174,7 +174,7 @@
"src/dispatcher/cli-argument-parser.ts:324", "src/dispatcher/cli-argument-parser.ts:324",
"src/dispatcher/cli-argument-parser.ts:328", "src/dispatcher/cli-argument-parser.ts:328",
"src/dispatcher/flows/default-flow.ts:76", "src/dispatcher/flows/default-flow.ts:76",
"src/dispatcher/flows/settings-flow.ts:132", "src/dispatcher/flows/settings-flow.ts:135",
"src/docker/docker-assets.ts:35", "src/docker/docker-assets.ts:35",
"src/docker/docker-executor.ts:154", "src/docker/docker-executor.ts:154",
"src/docker/docker-executor.ts:435", "src/docker/docker-executor.ts:435",
@@ -71,6 +71,17 @@ describe('execClaudeWithCLIProxy browser flag validation', () => {
return fs.existsSync(filePath); return fs.existsSync(filePath);
} }
function makeWebSearchProvisioningFail(): void {
const ccsDir = path.join(tmpHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
'version: 13\nwebsearch:\n enabled: true\n providers:\n duckduckgo:\n enabled: true\n',
'utf8'
);
fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8');
}
afterEach(() => { afterEach(() => {
if (originalCcsHome !== undefined) { if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome; process.env.CCS_HOME = originalCcsHome;
@@ -81,6 +92,120 @@ describe('execClaudeWithCLIProxy browser flag validation', () => {
fs.rmSync(tmpHome, { recursive: true, force: true }); fs.rmSync(tmpHome, { recursive: true, force: true });
}); });
it('keeps WebSearch provisioning strict for --config settings writes', async () => {
makeWebSearchProvisioningFail();
let requestCount = 0;
const server = http.createServer((_req, res) => {
requestCount += 1;
res.writeHead(200, { 'content-type': 'application/json' });
res.end('{"ok":true}');
});
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
if (!address || typeof address === 'string') {
server.close();
throw new Error('Test server did not bind to a TCP port');
}
try {
await expect(
execClaudeWithCLIProxy(
fakeClaudePath,
'gemini',
[
'--proxy-host',
'127.0.0.1',
'--proxy-port',
String(address.port),
'--proxy-auth-token',
'SECRET_TOKEN_FOR_VALIDATION',
'--remote-only',
'--config',
],
{}
)
).rejects.toThrow(
'WebSearch is enabled, but CCS could not prepare the local WebSearch tool.'
);
expect(requestCount).toBeGreaterThan(0);
} finally {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
it('degrades WebSearch provisioning failures for CLIProxy launches', async () => {
makeWebSearchProvisioningFail();
const markerPath = path.join(tmpHome, 'fake-claude-launched');
fs.writeFileSync(
fakeClaudePath,
`#!/bin/sh\nprintf launched > ${JSON.stringify(markerPath)}\nexit 0\n`,
{ mode: 0o755 }
);
fs.chmodSync(fakeClaudePath, 0o755);
let requestCount = 0;
const server = http.createServer((_req, res) => {
requestCount += 1;
res.writeHead(200, { 'content-type': 'application/json' });
res.end('{"ok":true}');
});
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
if (!address || typeof address === 'string') {
server.close();
throw new Error('Test server did not bind to a TCP port');
}
const exitSpy = jest
.spyOn(process, 'exit')
.mockImplementation((() => undefined as never) as typeof process.exit);
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
try {
await execClaudeWithCLIProxy(
fakeClaudePath,
'gemini',
[
'--proxy-host',
'127.0.0.1',
'--proxy-port',
String(address.port),
'--proxy-auth-token',
'SECRET_TOKEN_FOR_VALIDATION',
'--remote-only',
'--print',
'hello',
],
{}
);
expect(await waitForFile(markerPath)).toBe(true);
expect(requestCount).toBeGreaterThan(0);
expect(exitSpy).toHaveBeenCalledWith(0);
} finally {
exitSpy.mockRestore();
logSpy.mockRestore();
errorSpy.mockRestore();
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
it('validates conflicting browser launch flags before remote proxy checks', async () => { it('validates conflicting browser launch flags before remote proxy checks', async () => {
let requestCount = 0; let requestCount = 0;
const server = http.createServer((_req, res) => { const server = http.createServer((_req, res) => {
+17 -4
View File
@@ -26,7 +26,11 @@ import { supportsModelConfig } from '../model-catalog';
import { CLIProxyProvider, ExecutorConfig } from '../types'; import { CLIProxyProvider, ExecutorConfig } from '../types';
import { CodexReasoningProxy } from '../ai-providers/codex-reasoning-proxy'; import { CodexReasoningProxy } from '../ai-providers/codex-reasoning-proxy';
import { ToolSanitizationProxy } from '../proxy/tool-sanitization-proxy'; import { ToolSanitizationProxy } from '../proxy/tool-sanitization-proxy';
import { ensureWebSearchMcpOrThrow, displayWebSearchStatus } from '../../utils/websearch-manager'; import {
ensureWebSearchMcpForLaunch,
ensureWebSearchMcpOrThrow,
displayWebSearchStatus,
} from '../../utils/websearch-manager';
import { import {
ensureImageAnalysisMcpOrThrow, ensureImageAnalysisMcpOrThrow,
syncImageAnalysisMcpToConfigDir, syncImageAnalysisMcpToConfigDir,
@@ -157,10 +161,7 @@ export async function execClaudeWithCLIProxy(
log, log,
}); });
// Setup first-class CCS WebSearch runtime
ensureWebSearchMcpOrThrow();
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow(); const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
displayWebSearchStatus();
const providerConfig = getProviderConfig(provider); const providerConfig = getProviderConfig(provider);
log(`Provider: ${providerConfig.displayName}`); log(`Provider: ${providerConfig.displayName}`);
@@ -191,6 +192,7 @@ export async function execClaudeWithCLIProxy(
const { const {
forceConfig, forceConfig,
forceImport,
addAccount, addAccount,
showAccounts, showAccounts,
useAccount, useAccount,
@@ -224,6 +226,8 @@ export async function execClaudeWithCLIProxy(
// Handle --config // Handle --config
if (forceConfig && supportsModelConfig(provider)) { if (forceConfig && supportsModelConfig(provider)) {
ensureWebSearchMcpOrThrow();
// Block --config for composite variants (per-tier models in config.yaml) // Block --config for composite variants (per-tier models in config.yaml)
if (cfg.isComposite) { if (cfg.isComposite) {
const variantName = cfg.profileName || provider; const variantName = cfg.profileName || provider;
@@ -266,8 +270,17 @@ export async function execClaudeWithCLIProxy(
await handleLogout(authCtx); await handleLogout(authCtx);
// Handle --import (early exit, Kiro only) // Handle --import (early exit, Kiro only)
if (forceImport) {
ensureWebSearchMcpOrThrow();
}
await handleImport(authCtx); await handleImport(authCtx);
// Setup first-class CCS WebSearch runtime for non-strict user launches.
const shouldDisplayWebSearchStatus = ensureWebSearchMcpForLaunch();
if (shouldDisplayWebSearchStatus) {
displayWebSearchStatus();
}
// 3. Ensure OAuth completed (if provider requires it) // 3. Ensure OAuth completed (if provider requires it)
const remoteAuthToken = proxyConfig.authToken?.trim(); const remoteAuthToken = proxyConfig.authToken?.trim();
const skipLocalAuth = resolveSkipLocalAuth(remoteAuthToken, useRemoteProxy); const skipLocalAuth = resolveSkipLocalAuth(remoteAuthToken, useRemoteProxy);
+2 -2
View File
@@ -54,7 +54,7 @@ import {
appendThirdPartyWebSearchToolArgs, appendThirdPartyWebSearchToolArgs,
appendWebSearchTrace, appendWebSearchTrace,
createWebSearchTraceContext, createWebSearchTraceContext,
ensureWebSearchMcpOrThrow, ensureWebSearchMcpForLaunch,
getWebSearchHookEnv, getWebSearchHookEnv,
readWebSearchTraceRecords, readWebSearchTraceRecords,
syncWebSearchMcpToConfigDir, syncWebSearchMcpToConfigDir,
@@ -159,7 +159,7 @@ export class HeadlessExecutor {
); );
} }
ensureWebSearchMcpOrThrow(); ensureWebSearchMcpForLaunch();
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow(); const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir); syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir);
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir); syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
-4
View File
@@ -14,7 +14,6 @@ import {
} from '../../cliproxy'; } from '../../cliproxy';
import { getEffectiveEnvVars, getCompositeEnvVars } from '../../cliproxy/config/env-builder'; import { getEffectiveEnvVars, getCompositeEnvVars } from '../../cliproxy/config/env-builder';
import { resolveLifecyclePort } from '../../cliproxy/config/port-manager'; import { resolveLifecyclePort } from '../../cliproxy/config/port-manager';
import { ensureWebSearchMcpOrThrow } from '../../utils/websearch-manager';
import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis'; import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis';
import { import {
ensureProfileHooks as ensureImageAnalyzerHooks, ensureProfileHooks as ensureImageAnalyzerHooks,
@@ -38,9 +37,6 @@ export async function runCliproxyFlow(ctx: ProfileDispatchContext): Promise<void
const imageAnalysisMcpReady = const imageAnalysisMcpReady =
resolvedTarget === 'claude' ? ensureImageAnalysisMcpOrThrow() : true; resolvedTarget === 'claude' ? ensureImageAnalysisMcpOrThrow() : true;
if (resolvedTarget === 'claude') {
ensureWebSearchMcpOrThrow();
}
const provider = profileInfo.provider || (profileInfo.name as CLIProxyProvider); const provider = profileInfo.provider || (profileInfo.name as CLIProxyProvider);
const expandedCliproxySettingsPath = profileInfo.settingsPath const expandedCliproxySettingsPath = profileInfo.settingsPath
? expandPath(profileInfo.settingsPath) ? expandPath(profileInfo.settingsPath)
+2 -2
View File
@@ -5,7 +5,7 @@
*/ */
import { fail, info } from '../../utils/ui'; import { fail, info } from '../../utils/ui';
import { ensureWebSearchMcpOrThrow } from '../../utils/websearch-manager'; import { ensureWebSearchMcpForLaunch } from '../../utils/websearch-manager';
import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis'; import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis';
import { import {
ensureProfileHooks as ensureImageAnalyzerHooks, ensureProfileHooks as ensureImageAnalyzerHooks,
@@ -23,7 +23,7 @@ export async function runCopilotFlow(ctx: ProfileDispatchContext): Promise<void>
resolveProfileContinuityInheritance, resolveProfileContinuityInheritance,
} = ctx; } = ctx;
ensureWebSearchMcpOrThrow(); ensureWebSearchMcpForLaunch();
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow(); const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
if (resolvedTarget === 'claude') { if (resolvedTarget === 'claude') {
if (imageAnalysisMcpReady) { if (imageAnalysisMcpReady) {
+2 -2
View File
@@ -5,7 +5,7 @@
*/ */
import { fail, info } from '../../utils/ui'; import { fail, info } from '../../utils/ui';
import { ensureWebSearchMcpOrThrow } from '../../utils/websearch-manager'; import { ensureWebSearchMcpForLaunch } from '../../utils/websearch-manager';
import { ensureProfileHooks as ensureImageAnalyzerHooks } from '../../utils/hooks/image-analyzer-profile-hook-injector'; import { ensureProfileHooks as ensureImageAnalyzerHooks } from '../../utils/hooks/image-analyzer-profile-hook-injector';
import { installImageAnalyzerHook } from '../../utils/hooks'; import { installImageAnalyzerHook } from '../../utils/hooks';
import type { ProfileDispatchContext } from '../dispatcher-context'; import type { ProfileDispatchContext } from '../dispatcher-context';
@@ -19,7 +19,7 @@ export async function runCursorFlow(ctx: ProfileDispatchContext): Promise<void>
resolveProfileContinuityInheritance, resolveProfileContinuityInheritance,
} = ctx; } = ctx;
ensureWebSearchMcpOrThrow(); ensureWebSearchMcpForLaunch();
installImageAnalyzerHook(); installImageAnalyzerHook();
ensureImageAnalyzerHooks({ ensureImageAnalyzerHooks({
profileName: profileInfo.name, profileName: profileInfo.name,
+6 -3
View File
@@ -14,7 +14,7 @@ import {
validateAnthropicKey, validateAnthropicKey,
} from '../../utils/api-key-validator'; } from '../../utils/api-key-validator';
import { import {
ensureWebSearchMcpOrThrow, ensureWebSearchMcpForLaunch,
displayWebSearchStatus, displayWebSearchStatus,
getWebSearchHookEnv, getWebSearchHookEnv,
syncWebSearchMcpToConfigDir, syncWebSearchMcpToConfigDir,
@@ -94,15 +94,18 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
if (browserAttachRuntime?.warning) { if (browserAttachRuntime?.warning) {
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`); process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
} }
let shouldDisplayWebSearchStatus = true;
if (resolvedTarget === 'claude') { if (resolvedTarget === 'claude') {
ensureWebSearchMcpOrThrow(); shouldDisplayWebSearchStatus = ensureWebSearchMcpForLaunch();
if (browserRuntimeEnv) { if (browserRuntimeEnv) {
ensureBrowserMcpOrThrow(); ensureBrowserMcpOrThrow();
} }
} }
// Display WebSearch status (single line, equilibrium UX) // Display WebSearch status (single line, equilibrium UX)
displayWebSearchStatus(); if (shouldDisplayWebSearchStatus) {
displayWebSearchStatus();
}
const continuityInheritance = const continuityInheritance =
resolvedTarget === 'claude' resolvedTarget === 'claude'
+1
View File
@@ -64,6 +64,7 @@ export {
removeWebSearchMcpConfig, removeWebSearchMcpConfig,
uninstallWebSearchMcp, uninstallWebSearchMcp,
syncWebSearchMcpToConfigDir, syncWebSearchMcpToConfigDir,
ensureWebSearchMcpForLaunch,
ensureWebSearchMcpOrThrow, ensureWebSearchMcpOrThrow,
} from './websearch/mcp-installer'; } from './websearch/mcp-installer';
+1
View File
@@ -61,6 +61,7 @@ export {
removeWebSearchMcpConfig, removeWebSearchMcpConfig,
uninstallWebSearchMcp, uninstallWebSearchMcp,
syncWebSearchMcpToConfigDir, syncWebSearchMcpToConfigDir,
ensureWebSearchMcpForLaunch,
ensureWebSearchMcpOrThrow, ensureWebSearchMcpOrThrow,
} from './mcp-installer'; } from './mcp-installer';
+27
View File
@@ -438,3 +438,30 @@ export function ensureWebSearchMcpOrThrow(): void {
throw new Error('WebSearch is enabled, but CCS could not prepare the local WebSearch tool.'); throw new Error('WebSearch is enabled, but CCS could not prepare the local WebSearch tool.');
} }
} }
/**
* Prepare WebSearch for a user launch without blocking Claude startup.
*
* Returns true when the normal WebSearch status line is still accurate. A
* failed MCP prepare already prints a degraded-path warning, so callers should
* skip the ready/status line when this returns false.
*/
export function ensureWebSearchMcpForLaunch(): boolean {
const wsConfig = getWebSearchConfig();
if (!wsConfig.enabled) {
return true;
}
const ready = ensureWebSearchMcp();
if (!ready) {
process.stderr.write(
String(
warn(
'WebSearch is enabled, but CCS could not prepare the local WebSearch tool. This session will continue without local WebSearch.'
)
) + '\n'
);
}
return ready;
}
+19 -4
View File
@@ -61,7 +61,10 @@ function startMockUpstream(port: number): Promise<void> {
}); });
} }
function runCli(args: string[], env: Record<string, string>): Promise<{ code: number | null; stdout: string; stderr: string }> { function runCli(
args: string[],
env: Record<string, string>
): Promise<{ code: number | null; stdout: string; stderr: string }> {
return new Promise((resolve) => { return new Promise((resolve) => {
const child = spawn(process.execPath, [DIST_ENTRY, ...args], { const child = spawn(process.execPath, [DIST_ENTRY, ...args], {
env: { env: {
@@ -101,6 +104,11 @@ describe('openai provider routing e2e', () => {
const ccsDir = path.join(tempDir, '.ccs'); const ccsDir = path.join(tempDir, '.ccs');
const binDir = path.join(tempDir, 'bin'); const binDir = path.join(tempDir, 'bin');
const outputPath = path.join(tempDir, 'claude-output.json'); const outputPath = path.join(tempDir, 'claude-output.json');
const fakeClaudeRuntimePath = path.join(binDir, 'fake-claude.cjs');
const fakeClaudePath = path.join(
binDir,
process.platform === 'win32' ? 'claude.cmd' : 'claude'
);
fs.mkdirSync(ccsDir, { recursive: true }); fs.mkdirSync(ccsDir, { recursive: true });
fs.mkdirSync(binDir, { recursive: true }); fs.mkdirSync(binDir, { recursive: true });
@@ -123,8 +131,8 @@ describe('openai provider routing e2e', () => {
'utf8' 'utf8'
); );
fs.writeFileSync( fs.writeFileSync(
path.join(binDir, 'claude'), fakeClaudeRuntimePath,
`#!/usr/bin/env node `
const fs = require('fs'); const fs = require('fs');
(async () => { (async () => {
const response = await fetch(\`\${process.env.ANTHROPIC_BASE_URL}/v1/messages\`, { const response = await fetch(\`\${process.env.ANTHROPIC_BASE_URL}/v1/messages\`, {
@@ -153,6 +161,13 @@ const fs = require('fs');
process.exit(1); process.exit(1);
}); });
`, `,
'utf8'
);
fs.writeFileSync(
fakeClaudePath,
process.platform === 'win32'
? '@echo off\r\nnode "%~dp0fake-claude.cjs" %*\r\n'
: '#!/bin/sh\nexec node "$(dirname "$0")/fake-claude.cjs" "$@"\n',
{ mode: 0o755 } { mode: 0o755 }
); );
@@ -160,7 +175,7 @@ const fs = require('fs');
...process.env, ...process.env,
CCS_HOME: tempDir, CCS_HOME: tempDir,
CCS_E2E_OUTPUT: outputPath, CCS_E2E_OUTPUT: outputPath,
PATH: `${binDir}:${process.env.PATH || ''}`, PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`,
}); });
expect(result.code).toBe(0); expect(result.code).toBe(0);
@@ -4,7 +4,8 @@ import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
const STEERING_PROMPT_SNIPPET = 'prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches'; const STEERING_PROMPT_SNIPPET =
'prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches';
interface RunResult { interface RunResult {
status: number | null; status: number | null;
@@ -103,28 +104,40 @@ exit 0
fs.rmSync(tmpHome, { recursive: true, force: true }); fs.rmSync(tmpHome, { recursive: true, force: true });
}); });
it('fails before Claude launch when the local WebSearch tool runtime cannot be prepared', () => { it('continues without local WebSearch when the tool runtime cannot be prepared', () => {
if (process.platform === 'win32') return; if (process.platform === 'win32') return;
fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8'); fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8');
const result = runCcs(['glm', 'smoke'], baseEnv); const result = runCcs(['glm', 'smoke'], baseEnv);
expect(result.status).toBe(1); expect(result.status).toBe(0);
expect(result.stderr).toContain('could not prepare the local WebSearch tool'); expect(result.stderr).toContain('could not prepare the local WebSearch tool');
expect(fs.existsSync(claudeArgsLogPath)).toBe(false); expect(result.stderr).toContain('This session will continue without local WebSearch');
expect(fs.existsSync(claudeArgsLogPath)).toBe(true);
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
expect(launchedArgs).toContain('--disallowedTools');
expect(launchedArgs).toContain('WebSearch');
expect(launchedArgs).toContain('--append-system-prompt');
expect(launchedArgs).toContain(STEERING_PROMPT_SNIPPET);
}); });
it('fails before delegated headless launch when the local WebSearch tool runtime cannot be prepared', () => { it('continues delegated headless launch when the local WebSearch tool runtime cannot be prepared', () => {
if (process.platform === 'win32') return; if (process.platform === 'win32') return;
fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8'); fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8');
const result = runCcs(['glm', '-p', 'smoke'], baseEnv); const result = runCcs(['glm', '-p', 'smoke'], baseEnv);
expect(result.status).toBe(1); expect(result.status).toBe(0);
expect(result.stderr).toContain('could not prepare the local WebSearch tool'); expect(result.stderr).toContain('could not prepare the local WebSearch tool');
expect(fs.existsSync(claudeArgsLogPath)).toBe(false); expect(result.stderr).toContain('This session will continue without local WebSearch');
expect(fs.existsSync(claudeArgsLogPath)).toBe(true);
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
expect(launchedArgs).toContain('--disallowedTools');
expect(launchedArgs).toContain('WebSearch');
expect(launchedArgs).toContain('--append-system-prompt');
expect(launchedArgs).toContain(STEERING_PROMPT_SNIPPET);
}); });
it('keeps launch non-fatal when WebSearch is disabled', () => { it('keeps launch non-fatal when WebSearch is disabled', () => {
@@ -162,9 +175,7 @@ exit 0
expect(fs.existsSync(tracePath)).toBe(true); expect(fs.existsSync(tracePath)).toBe(true);
const traceEvents = readTraceEvents(tracePath); const traceEvents = readTraceEvents(tracePath);
const launchEvent = traceEvents.find( const launchEvent = traceEvents.find((event) => event.event === 'ccs_websearch_launch') as
(event) => event.event === 'ccs_websearch_launch'
) as
| { | {
launcher?: string; launcher?: string;
nativeWebSearchDisallowed?: boolean; nativeWebSearchDisallowed?: boolean;