fix(freehire-search): reject fractional numeric flags instead of silently truncating (#373) (#374)

parseIntFlag used bare parseInt, so --jobage 0.5 truncated to 0, failed
the jobage > 0 guard in search.ts, and posted_within_days was silently
omitted from the outbound request while the CLI exited 0. Numeric flags
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. Five new validation cases, each verified
to fail on the unfixed code.
This commit is contained in:
Ayobami Adegoke
2026-08-27 19:04:59 +02:00
committed by GitHub
parent 75c15eeecc
commit 79cd383e58
3 changed files with 50 additions and 3 deletions
+10 -3
View File
@@ -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
@@ -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");
+14
View File
@@ -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.