mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 20:15:40 +00:00
fix: fail closed on websearch launch provisioning (#1621)
This commit is contained in:
1 parent
0527e276b1
commit
f2843ad2ce
3 files changed
+49
-58
No files matched your search
@@ -141,7 +141,7 @@ describe('execClaudeWithCLIProxy browser flag validation', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('degrades WebSearch provisioning failures for CLIProxy launches', async () => {
|
it('fails closed on WebSearch provisioning failures for CLIProxy launches', async () => {
|
||||||
makeWebSearchProvisioningFail();
|
makeWebSearchProvisioningFail();
|
||||||
|
|
||||||
const markerPath = path.join(tmpHome, 'fake-claude-launched');
|
const markerPath = path.join(tmpHome, 'fake-claude-launched');
|
||||||
@@ -176,26 +176,30 @@ describe('execClaudeWithCLIProxy browser flag validation', () => {
|
|||||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await execClaudeWithCLIProxy(
|
await expect(
|
||||||
fakeClaudePath,
|
execClaudeWithCLIProxy(
|
||||||
'gemini',
|
fakeClaudePath,
|
||||||
[
|
'gemini',
|
||||||
'--proxy-host',
|
[
|
||||||
'127.0.0.1',
|
'--proxy-host',
|
||||||
'--proxy-port',
|
'127.0.0.1',
|
||||||
String(address.port),
|
'--proxy-port',
|
||||||
'--proxy-auth-token',
|
String(address.port),
|
||||||
'SECRET_TOKEN_FOR_VALIDATION',
|
'--proxy-auth-token',
|
||||||
'--remote-only',
|
'SECRET_TOKEN_FOR_VALIDATION',
|
||||||
'--print',
|
'--remote-only',
|
||||||
'hello',
|
'--print',
|
||||||
],
|
'hello',
|
||||||
{}
|
],
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
).rejects.toThrow(
|
||||||
|
'WebSearch is enabled, but CCS could not prepare the local WebSearch tool.'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await waitForFile(markerPath)).toBe(true);
|
expect(await waitForFile(markerPath)).toBe(false);
|
||||||
expect(requestCount).toBeGreaterThan(0);
|
expect(requestCount).toBeGreaterThan(0);
|
||||||
expect(exitSpy).toHaveBeenCalledWith(0);
|
expect(exitSpy).not.toHaveBeenCalledWith(0);
|
||||||
} finally {
|
} finally {
|
||||||
exitSpy.mockRestore();
|
exitSpy.mockRestore();
|
||||||
logSpy.mockRestore();
|
logSpy.mockRestore();
|
||||||
|
|||||||
@@ -440,28 +440,14 @@ export function ensureWebSearchMcpOrThrow(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare WebSearch for a user launch without blocking Claude startup.
|
* Prepare WebSearch for a user launch.
|
||||||
*
|
*
|
||||||
* Returns true when the normal WebSearch status line is still accurate. A
|
* WebSearch-enabled launches must fail closed when the managed local MCP
|
||||||
* failed MCP prepare already prints a degraded-path warning, so callers should
|
* runtime cannot be prepared. Otherwise CCS would still suppress Claude's
|
||||||
* skip the ready/status line when this returns false.
|
* native WebSearch and inject fallback steering while the constrained MCP
|
||||||
|
* search path is unavailable.
|
||||||
*/
|
*/
|
||||||
export function ensureWebSearchMcpForLaunch(): boolean {
|
export function ensureWebSearchMcpForLaunch(): boolean {
|
||||||
const wsConfig = getWebSearchConfig();
|
ensureWebSearchMcpOrThrow();
|
||||||
if (!wsConfig.enabled) {
|
return true;
|
||||||
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,6 +61,11 @@ describe('settings profile WebSearch launch', () => {
|
|||||||
path.join(ccsDir, 'config.json'),
|
path.join(ccsDir, 'config.json'),
|
||||||
JSON.stringify({ profiles: { glm: settingsPath } }, null, 2) + '\n'
|
JSON.stringify({ profiles: { glm: settingsPath } }, null, 2) + '\n'
|
||||||
);
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ccsDir, 'config.yaml'),
|
||||||
|
['version: 12', 'websearch:', ' enabled: true', ''].join('\n'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
settingsPath,
|
settingsPath,
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
@@ -93,6 +98,7 @@ exit 0
|
|||||||
CCS_HOME: tmpHome,
|
CCS_HOME: tmpHome,
|
||||||
CCS_CLAUDE_PATH: fakeClaudePath,
|
CCS_CLAUDE_PATH: fakeClaudePath,
|
||||||
CCS_DEBUG: '1',
|
CCS_DEBUG: '1',
|
||||||
|
CCS_SKIP_PREFLIGHT: '1',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,40 +110,30 @@ exit 0
|
|||||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('continues without local WebSearch when the tool runtime cannot be prepared', () => {
|
it('fails closed 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', 'smoke'], baseEnv);
|
const result = runCcs(['glm', 'smoke'], baseEnv);
|
||||||
|
|
||||||
expect(result.status).toBe(0);
|
expect(result.status).not.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(result.stderr).toContain('This session will continue without local WebSearch');
|
expect(result.stderr).not.toContain('This session will continue without local WebSearch');
|
||||||
expect(fs.existsSync(claudeArgsLogPath)).toBe(true);
|
expect(fs.existsSync(claudeArgsLogPath)).toBe(false);
|
||||||
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('continues delegated headless launch when the local WebSearch tool runtime cannot be prepared', () => {
|
it('fails closed for 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(0);
|
expect(result.status).not.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(result.stderr).toContain('This session will continue without local WebSearch');
|
expect(result.stderr).not.toContain('This session will continue without local WebSearch');
|
||||||
expect(fs.existsSync(claudeArgsLogPath)).toBe(true);
|
expect(fs.existsSync(claudeArgsLogPath)).toBe(false);
|
||||||
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', () => {
|
||||||
@@ -145,7 +141,7 @@ exit 0
|
|||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(ccsDir, 'config.yaml'),
|
path.join(ccsDir, 'config.yaml'),
|
||||||
'version: 12\nwebsearch:\n enabled: false\n',
|
['version: 12', 'websearch:', ' enabled: false', ''].join('\n'),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8');
|
fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8');
|
||||||
@@ -165,6 +161,11 @@ exit 0
|
|||||||
it('writes a source-side launch trace for settings profiles when tracing is enabled', () => {
|
it('writes a source-side launch trace for settings profiles when tracing is enabled', () => {
|
||||||
if (process.platform === 'win32') return;
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ccsDir, 'config.yaml'),
|
||||||
|
['version: 12', 'websearch:', ' enabled: false', ''].join('\n'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
const tracePath = path.join(ccsDir, 'logs', 'websearch-trace.jsonl');
|
const tracePath = path.join(ccsDir, 'logs', 'websearch-trace.jsonl');
|
||||||
const result = runCcs(['glm', 'smoke'], {
|
const result = runCcs(['glm', 'smoke'], {
|
||||||
...baseEnv,
|
...baseEnv,
|
||||||
|
|||||||
Reference in new issue
Block a user