From 129367dbca06fbcc1df930bb3a7e1f31e62409da Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:00:12 -0400 Subject: [PATCH] fix(freehire-search): require values for named search options (#568) * fix(freehire-search): require values for named search options * docs(changelog): record fix for #568 --- .agents/skills/freehire-search/cli/src/cli.ts | 12 +++ .../cli/tests/required-search-values.test.ts | 79 +++++++++++++++++++ CHANGELOG.md | 2 + 3 files changed, 93 insertions(+) create mode 100644 .agents/skills/freehire-search/cli/tests/required-search-values.test.ts diff --git a/.agents/skills/freehire-search/cli/src/cli.ts b/.agents/skills/freehire-search/cli/src/cli.ts index d87c8133..9c01ee74 100644 --- a/.agents/skills/freehire-search/cli/src/cli.ts +++ b/.agents/skills/freehire-search/cli/src/cli.ts @@ -170,6 +170,18 @@ async function main(): Promise { } if (cmd === "search") { + // These options take values, unlike bare --remote and --no-description. + // Treating a present-but-empty option as unset silently broadens the query. + for (const name of ["query", "company", "country", "region", "city", "seniority", "category", "skill", "description-format"]) { + const value = flags[name] + if (value !== undefined && (typeof value !== "string" || !value.trim())) { + process.stderr.write( + JSON.stringify({ error: `--${name} requires a non-empty value`, code: "BAD_ARG" }) + "\n", + ) + return 1 + } + } + const fmt = flags.format !== undefined ? String(flags.format) : "json" if (!["json", "table", "plain"].includes(fmt)) { process.stderr.write( diff --git a/.agents/skills/freehire-search/cli/tests/required-search-values.test.ts b/.agents/skills/freehire-search/cli/tests/required-search-values.test.ts new file mode 100644 index 00000000..cd12fbdd --- /dev/null +++ b/.agents/skills/freehire-search/cli/tests/required-search-values.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, test } from "bun:test"; +import { runCLI } from "./helpers"; + +describe("named search flags require their documented values", () => { + for (const name of ["query", "company", "country", "region", "city", "seniority", "category", "skill", "description-format"]) { + for (const value of [undefined, "", " "]) { + test(`--${name} with ${JSON.stringify(value)} fails before requesting jobs`, async () => { + const requests: string[] = []; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch(request) { + requests.push(request.url); + return Response.json({ data: [], meta: { total: 0 } }); + }, + }); + try { + const args = ["search", `--${name}`, ...(value === undefined ? [] : [value]), "--limit", "1"]; + const result = await runCLI(args, { FREEHIRE_API_URL: server.url.toString() }); + expect(result.exitCode).toBe(1); + expect(result.stdout).toBe(""); + expect(JSON.parse(result.stderr)).toMatchObject({ code: "BAD_ARG" }); + expect(JSON.parse(result.stderr).error).toContain(`--${name}`); + expect(requests).toEqual([]); + } finally { + server.stop(true); + } + }); + } + } + + test("preserves valid named filters and optional boolean flags", async () => { + const requests: URL[] = []; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch(request) { + requests.push(new URL(request.url)); + return Response.json({ data: [], meta: { total: 0 } }); + }, + }); + try { + const result = await runCLI([ + "search", "-q", "backend engineer", "--country", "DE, GB", "--region", "eu", + "--city", "Berlin", "--seniority", "senior", "--category", "backend", + "--skill", "go,kubernetes", "--company", "acme", "--remote", "--no-description", + "--description-format", "text", + ], { FREEHIRE_API_URL: server.url.toString() }); + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + expect(requests).toHaveLength(1); + const params = requests[0].searchParams; + expect(params.get("q")).toBe("backend engineer"); + expect(params.getAll("countries")).toEqual(["DE", "GB"]); + expect(params.getAll("skills")).toEqual(["go", "kubernetes"]); + expect(params.get("company_slug")).toBe("acme"); + expect(params.get("work_mode")).toBe("remote"); + expect(params.get("include_description")).toBe("false"); + } finally { + server.stop(true); + } + }); + + test("an unfiltered search remains valid when optional flags are omitted", async () => { + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch: () => Response.json({ data: [], meta: { total: 0 } }), + }); + try { + const result = await runCLI(["search"], { FREEHIRE_API_URL: server.url.toString() }); + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + expect(JSON.parse(result.stdout).results).toEqual([]); + } finally { + server.stop(true); + } + }); +}); diff --git a/CHANGELOG.md b/CHANGELOG.md index bc53e744..78f0e01e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -296,6 +296,8 @@ per-file diff commands. the shipped headings, and a derived test in each command's suite reads the quoted section names and requires each to be a `##` heading of the shipped file; both fail on master. +- **`freehire-search` rejects missing named-option values** (#568) - value-taking search options now reject a missing value or another flag before dispatch. + - **`jobnet-search` and `jobdanmark-search` search results now carry the contract's `id` field** (`search.ts` and `search-normalization.test.ts` in both CLIs, both `SKILL.md`s) - `/add-portal`'s contract says every search result has at least `id`, `title`, `company`,