mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-05 16:04:43 +00:00
fix(cli): reject fractional integer options before rounding
Validate decimal integrality before Number conversion can hide a fraction. Keep exact integer scientific notation, signed capture indices, and schedule limit updates working. Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { parseSafeInteger } from "../../utils/integer.js";
|
||||
import type { DaemonTarget } from "../../utils/daemon-target.js";
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import type { CommandError, CommandOptions } from "../../output/index.js";
|
||||
@@ -441,9 +442,8 @@ function buildNewAgentConfigPatch(
|
||||
}
|
||||
|
||||
function parsePositiveInt(value: string, flag: string): number {
|
||||
const parsed = Number(value);
|
||||
const isPositiveInteger = Number.isSafeInteger(parsed) && parsed > 0;
|
||||
if (!isPositiveInteger) {
|
||||
const parsed = parseSafeInteger(value);
|
||||
if (parsed === undefined || parsed <= 0) {
|
||||
throw {
|
||||
code: "INVALID_INTEGER",
|
||||
message: `${flag} must be a positive integer`,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseSafeInteger } from "../../utils/integer.js";
|
||||
import type { Command } from "commander";
|
||||
import { renderError, toCommandError } from "../../output/render.js";
|
||||
import {
|
||||
@@ -85,9 +86,8 @@ function parseLineNumber(flag: string, value?: string): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = Number(value);
|
||||
const isIntegerLineNumber = value.trim().length > 0 && Number.isSafeInteger(parsed);
|
||||
if (!isIntegerLineNumber) {
|
||||
const parsed = parseSafeInteger(value);
|
||||
if (parsed === undefined) {
|
||||
throw {
|
||||
code: "INVALID_LINE_NUMBER",
|
||||
message: `Invalid ${flag} value: ${value}`,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export function parseSafeInteger(value: string): number | undefined {
|
||||
const input = value.trim();
|
||||
const match = /^[+-]?(\d*)(?:\.(\d*))?(?:e([+-]?\d+))?$/i.exec(input);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const digits = match[1] + (match[2] ?? "");
|
||||
const parsed = Number(input);
|
||||
if (digits.length === 0 || !Number.isSafeInteger(parsed)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check the decimal value before floating-point rounding can hide a fraction.
|
||||
const decimalPlaces = (match[2]?.length ?? 0) - Number(match[3] ?? "0");
|
||||
if (decimalPlaces > 0 && /[1-9]/.test(digits.slice(-decimalPlaces))) {
|
||||
return undefined;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
@@ -40,6 +40,10 @@ async function createQuietTerminal() {
|
||||
}
|
||||
|
||||
test.each([
|
||||
["--end", "4503599627370496.5"],
|
||||
["--start", "1.0000000000000001"],
|
||||
["--end", "4.5035996273704965e15"],
|
||||
["--end", "1e-324"],
|
||||
["--start", "1.5"],
|
||||
["--end", "1.5"],
|
||||
["--start", "1abc"],
|
||||
@@ -71,6 +75,9 @@ test.each([
|
||||
test.each([
|
||||
{ args: [], lineCount: 10 },
|
||||
{ args: ["--start", "0", "--end", "1"], lineCount: 2 },
|
||||
{ args: ["--end", "1.0"], lineCount: 2 },
|
||||
{ args: ["--end", "10e-1"], lineCount: 2 },
|
||||
{ args: ["--end", ".1e1"], lineCount: 2 },
|
||||
{ args: ["--start", "-2", "--end", "-1"], lineCount: 2 },
|
||||
{ args: ["--scrollback", "--start", "7", "--end", "1"], lineCount: 2 },
|
||||
])("CLI terminal capture preserves valid ranges $args", async ({ args, lineCount }) => {
|
||||
|
||||
@@ -43,36 +43,42 @@ function makeTempDir(prefix: string): string {
|
||||
return dir;
|
||||
}
|
||||
|
||||
test.each(["2.5", "2runs", "9007199254740993", "0", "-1", ""])(
|
||||
"CLI schedule create rejects max-runs %j without saving a schedule",
|
||||
async (maxRuns) => {
|
||||
const cwd = makeTempDir("schedule-cli-integer-");
|
||||
const result = await runPaseoCli({
|
||||
daemon: ctx.daemon,
|
||||
args: [
|
||||
"schedule",
|
||||
"create",
|
||||
"Integer limit",
|
||||
"--cron",
|
||||
"0 0 1 1 *",
|
||||
"--provider",
|
||||
"claude",
|
||||
"--cwd",
|
||||
cwd,
|
||||
"--max-runs",
|
||||
maxRuns,
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(result.exitCode, result.stdout + result.stderr).toBe(1);
|
||||
expect(JSON.parse(result.stderr)).toEqual({
|
||||
error: { code: "INVALID_INTEGER", message: "--max-runs must be a positive integer" },
|
||||
});
|
||||
expect((await ctx.client.scheduleList()).schedules).toEqual([]);
|
||||
},
|
||||
);
|
||||
test.each([
|
||||
"4503599627370496.5",
|
||||
"1.0000000000000001",
|
||||
"2.5",
|
||||
"2runs",
|
||||
"9007199254740993",
|
||||
"0",
|
||||
"-1",
|
||||
"",
|
||||
])("CLI schedule create rejects max-runs %j without saving a schedule", async (maxRuns) => {
|
||||
const cwd = makeTempDir("schedule-cli-integer-");
|
||||
const result = await runPaseoCli({
|
||||
daemon: ctx.daemon,
|
||||
args: [
|
||||
"schedule",
|
||||
"create",
|
||||
"Integer limit",
|
||||
"--cron",
|
||||
"0 0 1 1 *",
|
||||
"--provider",
|
||||
"claude",
|
||||
"--cwd",
|
||||
cwd,
|
||||
"--max-runs",
|
||||
maxRuns,
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(result.exitCode, result.stdout + result.stderr).toBe(1);
|
||||
expect(JSON.parse(result.stderr)).toEqual({
|
||||
error: { code: "INVALID_INTEGER", message: "--max-runs must be a positive integer" },
|
||||
});
|
||||
expect((await ctx.client.scheduleList()).schedules).toEqual([]);
|
||||
});
|
||||
|
||||
test.each(["2.5", "2runs"])(
|
||||
test.each(["2.5", "2runs", "4503599627370496.5"])(
|
||||
"CLI schedule update rejects max-runs %j without changing the schedule",
|
||||
async (maxRuns) => {
|
||||
const cwd = makeTempDir("schedule-cli-update-integer-");
|
||||
|
||||
@@ -30,6 +30,8 @@ export function runPaseoCli({ daemon, args }: CliOptions): Promise<CliResult> {
|
||||
{
|
||||
env: { ...process.env, PASEO_HOME: daemon.paseoHome },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
timeout: 20_000,
|
||||
killSignal: "SIGKILL",
|
||||
},
|
||||
);
|
||||
let stdout = "";
|
||||
@@ -41,6 +43,12 @@ export function runPaseoCli({ daemon, args }: CliOptions): Promise<CliResult> {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.once("error", reject);
|
||||
child.once("close", (exitCode) => resolve({ exitCode, stdout, stderr }));
|
||||
child.once("close", (exitCode, signal) => {
|
||||
if (signal) {
|
||||
reject(new Error(`CLI exited on ${signal}:\n${stdout}\n${stderr}`));
|
||||
} else {
|
||||
resolve({ exitCode, stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user