mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 02:14:32 +00:00
fix: degrade WebSearch launch provisioning failures (#1571)
This commit is contained in:
1 parent
813a7a043a
commit
1a0398579a
13 files changed
+225
-33
No files matched your search
@@ -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) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export {
|
|||||||
removeWebSearchMcpConfig,
|
removeWebSearchMcpConfig,
|
||||||
uninstallWebSearchMcp,
|
uninstallWebSearchMcp,
|
||||||
syncWebSearchMcpToConfigDir,
|
syncWebSearchMcpToConfigDir,
|
||||||
|
ensureWebSearchMcpForLaunch,
|
||||||
ensureWebSearchMcpOrThrow,
|
ensureWebSearchMcpOrThrow,
|
||||||
} from './websearch/mcp-installer';
|
} from './websearch/mcp-installer';
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export {
|
|||||||
removeWebSearchMcpConfig,
|
removeWebSearchMcpConfig,
|
||||||
uninstallWebSearchMcp,
|
uninstallWebSearchMcp,
|
||||||
syncWebSearchMcpToConfigDir,
|
syncWebSearchMcpToConfigDir,
|
||||||
|
ensureWebSearchMcpForLaunch,
|
||||||
ensureWebSearchMcpOrThrow,
|
ensureWebSearchMcpOrThrow,
|
||||||
} from './mcp-installer';
|
} from './mcp-installer';
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user