diff --git a/packages/cli/src/acp-integration/acpAgent.test.ts b/packages/cli/src/acp-integration/acpAgent.test.ts index 3a9c2bfffc1..d953efeca01 100644 --- a/packages/cli/src/acp-integration/acpAgent.test.ts +++ b/packages/cli/src/acp-integration/acpAgent.test.ts @@ -2849,6 +2849,43 @@ describe('QwenAgent MCP SSE/HTTP support', () => { await agentPromise; }); + it('qwen/settings/setMcpServer rejects malformed timeout strings', async () => { + const settings = makeCoreSettings(); + const { agent, agentPromise } = await bootCoreSettingsAgent(settings); + + await expect( + agent.extMethod('qwen/settings/setMcpServer', { + scope: 'user', + name: 'bad-timeout', + server: { transport: 'stdio', command: 'node', timeout: '10ms' }, + }), + ).rejects.toThrowError(/Expected a positive integer/); + + await expect( + agent.extMethod('qwen/settings/setMcpServer', { + scope: 'user', + name: 'fractional-timeout', + server: { transport: 'stdio', command: 'node', timeout: '1.5' }, + }), + ).rejects.toThrowError(/Expected a positive integer/); + + await agent.extMethod('qwen/settings/setMcpServer', { + scope: 'user', + name: 'valid-timeout', + server: { transport: 'stdio', command: 'node', timeout: '1500' }, + }); + + const persisted = vi + .mocked(settings.setValue) + .mock.calls.find((call) => call[1] === 'mcpServers')?.[2] as { + 'valid-timeout': { timeout: number }; + }; + expect(persisted['valid-timeout'].timeout).toBe(1500); + + mockConnectionState.resolve(); + await agentPromise; + }); + it('qwen/settings/removeMcpServer drops the named server and rejects a missing name', async () => { const settings = makeCoreSettings(); (settings.user.settings as Record)['mcpServers'] = { @@ -2947,6 +2984,24 @@ describe('QwenAgent MCP SSE/HTTP support', () => { await agentPromise; }); + it('qwen/settings/setHook rejects malformed timeout strings', async () => { + const settings = makeCoreSettings(); + const { agent, agentPromise } = await bootCoreSettingsAgent(settings); + + await expect( + agent.extMethod('qwen/settings/setHook', { + scope: 'user', + event: 'PreToolUse', + hook: { + hooks: [{ type: 'command', command: 'echo hi', timeout: '10ms' }], + }, + }), + ).rejects.toThrowError(/Expected a positive integer/); + + mockConnectionState.resolve(); + await agentPromise; + }); + it('qwen/settings/setHook replaces in place at a valid index and appends for out-of-range', async () => { const settings = makeCoreSettings(); (settings.user.settings as Record)['hooks'] = { diff --git a/packages/cli/src/acp-integration/acpAgent.ts b/packages/cli/src/acp-integration/acpAgent.ts index 641ed35430f..2381f4efae0 100644 --- a/packages/cli/src/acp-integration/acpAgent.ts +++ b/packages/cli/src/acp-integration/acpAgent.ts @@ -1757,10 +1757,17 @@ function normalizeStringRecord( function normalizeOptionalNumber(value: unknown): number | undefined { if (value === undefined || value === null || value === '') return undefined; - const numberValue = - typeof value === 'number' ? value : Number.parseInt(String(value), 10); - if (!Number.isFinite(numberValue) || numberValue <= 0) { - throw RequestError.invalidParams(undefined, 'Expected a positive number'); + let numberValue: number; + if (typeof value === 'number') { + numberValue = value; + } else if (typeof value === 'string') { + const trimmed = value.trim(); + numberValue = /^\d+$/.test(trimmed) ? Number(trimmed) : Number.NaN; + } else { + numberValue = Number.NaN; + } + if (!Number.isInteger(numberValue) || numberValue <= 0) { + throw RequestError.invalidParams(undefined, 'Expected a positive integer'); } return numberValue; }