fix(linkedin-search): reject fractional numeric flags (#371) (#393)

parseInt truncated values before validation, so --jobage 0.5 became 0 and silently omitted LinkedIn's freshness filter. Require whole numbers of at least 1 for every numeric search flag and guard the behavior with CLI regression tests.
This commit is contained in:
Ayobami Adegoke
2026-09-02 20:08:26 +02:00
committed by GitHub
parent 284dc4c2d0
commit c42806674b
3 changed files with 44 additions and 30 deletions
+9 -11
View File
@@ -126,9 +126,14 @@ async function main(): Promise<number> {
}
const parseIntFlag = (name: string, raw: string | boolean | string[]): number | null => {
const val = parseInt(raw as string, 10)
if (isNaN(val)) {
process.stderr.write(JSON.stringify({ error: `--${name} must be a number, got "${raw}"`, code: "BAD_ARG" }) + "\n")
// Number(), not parseInt(): parseInt truncates, so "--jobage 0.5"
// became 0 and silently dropped f_TPR from the request (#371).
// Whole numbers >= 1 only, matching the other portal CLIs.
const val = typeof raw === "string" ? Number(raw.trim()) : NaN
if (!Number.isInteger(val) || val < 1) {
process.stderr.write(
JSON.stringify({ error: `--${name} must be a whole number of at least 1, got "${raw}"`, code: "BAD_ARG" }) + "\n",
)
return null
}
return val
@@ -140,15 +145,8 @@ async function main(): Promise<number> {
flags.jobage = String(v)
}
if (flags["jobage-minutes"] !== undefined) {
const raw = flags["jobage-minutes"]
const v = parseIntFlag("jobage-minutes", raw)
const v = parseIntFlag("jobage-minutes", flags["jobage-minutes"])
if (v === null) return 1
if (v <= 0) {
process.stderr.write(
JSON.stringify({ error: `--jobage-minutes must be a positive number, got "${raw}"`, code: "BAD_ARG" }) + "\n",
)
return 1
}
flags["jobage-minutes"] = String(v)
}
if (flags.page !== undefined) {
@@ -12,7 +12,7 @@ function parsedStderr(stderr: string): { error?: string; code?: string } {
}
describe("LinkedIn CLI flag validation", () => {
describe("--jobage NaN validation", () => {
describe("numeric flag validation", () => {
test("non-numeric string exits 1 with BAD_ARG", async () => {
const result = await runCLI(["search", "-l", LOCATION, "--jobage", "foo"]);
expect(result.exitCode).not.toBe(0);
@@ -33,18 +33,34 @@ describe("LinkedIn CLI flag validation", () => {
expect(err.code).not.toBe("BAD_ARG");
});
test("float string truncated to integer, no error", async () => {
// parseInt("7.5") = 7, which is valid
const result = await runCLI(["search", "-l", LOCATION, "--jobage", "7.5", "--limit", "1"]);
const err = parsedStderr(result.stderr);
expect(err.code).not.toBe("BAD_ARG");
// Fractional values must be rejected, not truncated: parseInt("0.5") is 0,
// and jobage 0 makes buildTimeFilter return null, so f_TPR is silently
// omitted from the outbound request while the CLI exits 0 (#371).
for (const name of ["jobage", "jobage-minutes", "page", "limit"]) {
test(`--${name} fractional exits 1 with BAD_ARG instead of truncating`, async () => {
const result = await runCLI(["search", "-l", LOCATION, `--${name}`, "1.5"]);
expect(result.exitCode).not.toBe(0);
const err = parsedStderr(result.stderr);
expect(err.code).toBe("BAD_ARG");
expect(err.error).toMatch(new RegExp(name));
});
}
test("--jobage 0.5 exits 1 with BAD_ARG instead of dropping the freshness filter", async () => {
const result = await runCLI(["search", "-l", LOCATION, "--jobage", "0.5"]);
expect(result.exitCode).not.toBe(0);
expect(parsedStderr(result.stderr).code).toBe("BAD_ARG");
});
test("zero is accepted (falsy int should not be treated as missing)", async () => {
const result = await runCLI(["search", "-l", LOCATION, "--jobage", "0", "--limit", "1"]);
const err = parsedStderr(result.stderr);
expect(err.code).not.toBe("BAD_ARG");
});
for (const name of ["jobage", "jobage-minutes", "page", "limit"]) {
test(`--${name} 0 exits 1 with BAD_ARG`, async () => {
const result = await runCLI(["search", "-l", LOCATION, `--${name}`, "0"]);
expect(result.exitCode).not.toBe(0);
const err = parsedStderr(result.stderr);
expect(err.code).toBe("BAD_ARG");
expect(err.error).toMatch(new RegExp(name));
});
}
});
describe("--jobage-minutes validation", () => {
@@ -56,14 +72,6 @@ describe("LinkedIn CLI flag validation", () => {
expect(err.error).toMatch(/jobage-minutes/);
});
test("zero exits 1 with BAD_ARG", async () => {
const result = await runCLI(["search", "-l", LOCATION, "--jobage-minutes", "0"]);
expect(result.exitCode).not.toBe(0);
const err = parsedStderr(result.stderr);
expect(err.code).toBe("BAD_ARG");
expect(err.error).toMatch(/jobage-minutes/);
});
test("negative value is parsed as a missing value and exits 1 with BAD_ARG", async () => {
// parseFlags in cli.ts treats a next-token starting with "-" as absent
// (`next.startsWith("-")` → flag becomes boolean `true`), and there is no