diff --git a/.agents/skills/freehire-search/cli/src/cli.ts b/.agents/skills/freehire-search/cli/src/cli.ts index 4766f58..b0d66a0 100644 --- a/.agents/skills/freehire-search/cli/src/cli.ts +++ b/.agents/skills/freehire-search/cli/src/cli.ts @@ -112,9 +112,16 @@ best-effort, no SLA. Override with FREEHIRE_API_URL to use a self-hosted backend ` function 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, + // which fails search.ts's `jobage > 0` guard and silently drops + // posted_within_days from the outbound request while exiting 0 (#373). + // Whole numbers >= 1 only — the Danish CLIs' z.coerce.number().int().min(1) + // contract; 0 is rejected rather than kept as a "no filter" alias. + 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 diff --git a/.agents/skills/freehire-search/cli/tests/cli-flag-validation.test.ts b/.agents/skills/freehire-search/cli/tests/cli-flag-validation.test.ts index f29637b..9691d6a 100644 --- a/.agents/skills/freehire-search/cli/tests/cli-flag-validation.test.ts +++ b/.agents/skills/freehire-search/cli/tests/cli-flag-validation.test.ts @@ -25,6 +25,32 @@ describe("freehire CLI flag validation", () => { }); } + // Fractional values must be rejected, not truncated: parseInt("0.5") is 0, + // and jobage 0 fails search.ts's `> 0` guard, so posted_within_days is + // silently omitted from the outbound request while the CLI exits 0 — + // the discarded-filter failure the UNKNOWN_FLAG guard exists to prevent (#373). + for (const name of ["jobage", "page", "limit"]) { + test(`--${name} fractional exits 1 with BAD_ARG instead of truncating`, async () => { + const result = await runCLI(["search", `--${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 (truncates to 0 on master, dropping the freshness filter) exits 1 with BAD_ARG", async () => { + const result = await runCLI(["search", "--jobage", "0.5"]); + expect(result.exitCode).not.toBe(0); + expect(parsedStderr(result.stderr).code).toBe("BAD_ARG"); + }); + + test("--jobage 0 exits 1 with BAD_ARG (0 silently disables the filter, like the Danish CLIs' min(1))", async () => { + const result = await runCLI(["search", "--jobage", "0"]); + expect(result.exitCode).not.toBe(0); + expect(parsedStderr(result.stderr).code).toBe("BAD_ARG"); + }); + test("valid integers produce no BAD_ARG", async () => { const result = await runCLI(["search", "--jobage", "7", "--page", "1", "--limit", "1"]); expect(parsedStderr(result.stderr).code).not.toBe("BAD_ARG"); diff --git a/CHANGELOG.md b/CHANGELOG.md index 9483aee..ad42af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,20 @@ per-file diff commands. ## [Unreleased] +### Fixed + +- **`freehire-search` fractional numeric flags no longer silently change the query** (#373) - + `parseIntFlag` used bare `parseInt`, so a fractional value was truncated instead of + rejected: `--jobage 0.5` became `0`, failed the `jobage > 0` guard, and the + `posted_within_days` freshness filter was silently omitted from the outbound request + while the CLI exited 0 - on a default-ON `/scrape` portal, exactly the + discarded-filter failure the CLI's own `UNKNOWN_FLAG` guard documents. Numeric flags + (`--jobage`/`--page`/`--limit`) now accept whole numbers >= 1 only, mirroring the + Danish CLIs' `z.coerce.number().int().min(1)` contract, and reject everything else + with the stderr-JSON `BAD_ARG` error. The sibling of #371 (`linkedin-search`), which + remains with its reporter. Pinned by five new cases in `cli-flag-validation.test.ts`, + each verified to fail on the unfixed code. + ### Added - **pypdf ATS text-layer fallback** - `/apply` Step 5d and `tools/verify_pdf.py` extract the CV PDF text layer with **pypdf** first (BSD, `pip install pypdf`) so Windows machines without Poppler still get a mechanical parseability check. Poppler `pdftotext -layout -enc UTF-8` remains the fallback; if both are missing the check still degrades to a visual keyword review. No extra cache or installer. `05-cv-templates.md` `framework_version` 1.4.2 → 1.4.3.