From c42806674bc14ae4ded82cd652b146964226a55a Mon Sep 17 00:00:00 2001 From: Ayobami Adegoke Date: Wed, 2 Sep 2026 19:08:26 +0100 Subject: [PATCH] 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. --- .agents/skills/linkedin-search/cli/src/cli.ts | 20 ++++---- .../cli/tests/cli-flag-validation.test.ts | 46 +++++++++++-------- CHANGELOG.md | 8 ++++ 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/.agents/skills/linkedin-search/cli/src/cli.ts b/.agents/skills/linkedin-search/cli/src/cli.ts index f697398..5d413a5 100644 --- a/.agents/skills/linkedin-search/cli/src/cli.ts +++ b/.agents/skills/linkedin-search/cli/src/cli.ts @@ -126,9 +126,14 @@ async function main(): Promise { } 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 { 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) { diff --git a/.agents/skills/linkedin-search/cli/tests/cli-flag-validation.test.ts b/.agents/skills/linkedin-search/cli/tests/cli-flag-validation.test.ts index 1fa7747..cf09bac 100644 --- a/.agents/skills/linkedin-search/cli/tests/cli-flag-validation.test.ts +++ b/.agents/skills/linkedin-search/cli/tests/cli-flag-validation.test.ts @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index fa91af1..5208a9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,14 @@ per-file diff commands. ### Fixed +- **`linkedin-search` rejects fractional numeric flags instead of silently changing + the query** (#371) - bare `parseInt` truncated values before validation, so + `--jobage 0.5` became `0` and silently omitted LinkedIn's `f_TPR` freshness filter + while the CLI reported no argument error. `--jobage`, `--jobage-minutes`, `--page`, + and `--limit` now accept whole numbers >= 1 only and reject fractions and zero with + the stderr-JSON `BAD_ARG` contract, matching the other portal CLIs. Pinned by eight + cases verified to fail on the unfixed CLI. Reported by @Meet6338-X. + - **`linkedin-search detail` accepts LinkedIn job URLs with trailing slashes** (#411) - passing a job URL with a trailing slash (e.g., `https://www.linkedin.com/jobs/view//` or a slugged variant with or without query strings) failed validation and exited 1 with