diff --git a/src/execution.test.ts b/src/execution.test.ts index bd09ed974..47474d35a 100644 --- a/src/execution.test.ts +++ b/src/execution.test.ts @@ -3,7 +3,7 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import type { CliCommand } from './registry.js'; -import { executeCommand, prepareCommandArgs } from './execution.js'; +import { coerceAndValidateArgs, executeCommand, prepareCommandArgs } from './execution.js'; import { ArgumentError, TimeoutError, toEnvelope } from './errors.js'; import { cli, Strategy } from './registry.js'; import { withTimeoutMs } from './runtime.js'; @@ -12,6 +12,22 @@ import * as capRouting from './capabilityRouting.js'; import * as daemonClient from './browser/daemon-client.js'; import { BrowserCommandError } from './browser/daemon-client.js'; +describe('coerceAndValidateArgs', () => { + it('rejects fractional values for integer arguments', () => { + const args = [{ name: 'limit', type: 'int' as const, help: 'Result limit' }]; + + expect(() => coerceAndValidateArgs(args, { limit: '1.5' })).toThrow(ArgumentError); + }); + + it('rejects non-finite values for numeric arguments', () => { + const integerArgs = [{ name: 'limit', type: 'int' as const, help: 'Result limit' }]; + const numberArgs = [{ name: 'threshold', type: 'number' as const, help: 'Threshold' }]; + + expect(() => coerceAndValidateArgs(integerArgs, { limit: 'Infinity' })).toThrow(ArgumentError); + expect(() => coerceAndValidateArgs(numberArgs, { threshold: '-Infinity' })).toThrow(ArgumentError); + }); +}); + describe('executeCommand — non-browser timeout', () => { it('applies the user --timeout arg as the ceiling for non-browser commands', async () => { const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout'); diff --git a/src/execution.ts b/src/execution.ts index 585bc0344..cf8941654 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -67,9 +67,12 @@ export function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: CommandArgs): Comm if (val !== undefined && val !== null) { if (argDef.type === 'int' || argDef.type === 'number') { const num = Number(val); - if (Number.isNaN(num)) { + if (!Number.isFinite(num)) { throw new ArgumentError(`Argument "${argDef.name}" must be a valid number. Received: "${val}"`); } + if (argDef.type === 'int' && !Number.isInteger(num)) { + throw new ArgumentError(`Argument "${argDef.name}" must be a valid integer. Received: "${val}"`); + } result[argDef.name] = num; } else if (argDef.type === 'boolean' || argDef.type === 'bool') { if (typeof val === 'string') {