mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
fix(jobindex-search): reject a --sort other than score or date instead of sending it to the portal (#647)
* fix(jobindex-search): reject a --sort other than score or date instead of sending it to the portal --sort was a free z.string(), and jobindex does not reject an unknown sort: --sort newest came back exit 0 in an order that is neither relevance nor date, so the newest-first sort a caller asked for was silently not applied. Make it an enum of the two documented values, like --format, so any other value fails schema validation before a request is sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * changelog: place the #647 entry under [Unreleased] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Mads Lorentzen <madslorentzen_17@hotmail.com>
This commit is contained in:
3 files changed
+86
-1
No files matched your search
@@ -16,7 +16,9 @@ export const search = defineCommand({
|
|||||||
jobage: option(z.coerce.number().int().min(1).default(9999), {
|
jobage: option(z.coerce.number().int().min(1).default(9999), {
|
||||||
description: "Max age of posting in days: 1, 7, 14, 30, or 9999 (all)",
|
description: "Max age of posting in days: 1, 7, 14, 30, or 9999 (all)",
|
||||||
}),
|
}),
|
||||||
sort: option(z.string().default("score"), {
|
// An enum, not a free string: jobindex answers an unknown sort with an
|
||||||
|
// unrelated order and no error, so a typo silently lost the date sort.
|
||||||
|
sort: option(z.enum(["score", "date"]).default("score"), {
|
||||||
description: "Sort order: score (relevance) or date (newest first)",
|
description: "Sort order: score (relevance) or date (newest first)",
|
||||||
}),
|
}),
|
||||||
limit: option(z.coerce.number().int().min(1).optional(), {
|
limit: option(z.coerce.number().int().min(1).optional(), {
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
// --sort was a free z.string(), so any value went to jobindex verbatim. The
|
||||||
|
// portal does not reject an unknown sort: `--sort newest` came back exit 0 in
|
||||||
|
// an order that is neither relevance nor date (checked live 2026-10-18), with
|
||||||
|
// nothing telling the caller the documented `date` sort was not applied.
|
||||||
|
// The CLI runs against a loopback server so each outbound request is counted.
|
||||||
|
async function search(args: string[]) {
|
||||||
|
const requests: URL[] = [];
|
||||||
|
const server = Bun.serve({
|
||||||
|
hostname: "127.0.0.1",
|
||||||
|
port: 0,
|
||||||
|
fetch(request) {
|
||||||
|
requests.push(new URL(request.url));
|
||||||
|
return new Response('<script>var Stash = {"jobsearch":{"result_app":{"storeData":{"searchResponse":{"hitcount":0,"results":[]}}}}};</script>');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "jobindex-sort-"));
|
||||||
|
const preload = join(dir, "fetch.ts");
|
||||||
|
writeFileSync(preload, `
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = (input, init) => {
|
||||||
|
const url = new URL(input instanceof Request ? input.url : String(input));
|
||||||
|
if (url.protocol === "data:") return originalFetch(input, init);
|
||||||
|
if (url.origin !== "https://www.jobindex.dk" || url.pathname !== "/jobsoegning") {
|
||||||
|
throw new Error("Unexpected search endpoint");
|
||||||
|
}
|
||||||
|
return originalFetch(${JSON.stringify(server.url.origin)} + url.pathname + url.search, init);
|
||||||
|
};
|
||||||
|
`);
|
||||||
|
try {
|
||||||
|
const proc = Bun.spawn([
|
||||||
|
process.execPath, "run", "--preload", preload,
|
||||||
|
join(import.meta.dir, "../src/cli.ts"), "search", ...args,
|
||||||
|
], { stdout: "pipe", stderr: "pipe" });
|
||||||
|
const [stdout, stderr, exitCode] = await Promise.all([
|
||||||
|
new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited,
|
||||||
|
]);
|
||||||
|
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode, requests };
|
||||||
|
} finally {
|
||||||
|
server.stop(true);
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("--sort values", () => {
|
||||||
|
for (const value of ["newest", "Date", "relevance"]) {
|
||||||
|
test(`--sort ${value} is rejected before any request`, async () => {
|
||||||
|
const result = await search(["--query", "python", "--sort", value]);
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.stdout).toBe("");
|
||||||
|
const error = JSON.parse(result.stderr);
|
||||||
|
expect(error.ok).toBe(false);
|
||||||
|
expect(error.error.kind).toBe("validation");
|
||||||
|
expect(error.error.option).toBe("sort");
|
||||||
|
expect(result.requests).toHaveLength(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [args, expected] of [
|
||||||
|
[["--sort", "date"], "date"],
|
||||||
|
[["--sort", "score"], "score"],
|
||||||
|
[[], "score"],
|
||||||
|
] as const) {
|
||||||
|
test(`${args.join(" ") || "no --sort"} sends sort=${expected}`, async () => {
|
||||||
|
const result = await search(["--query", "python", ...args]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stderr).toBe("");
|
||||||
|
expect(result.requests).toHaveLength(1);
|
||||||
|
expect(result.requests[0].searchParams.get("sort")).toBe(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -131,6 +131,13 @@ per-file diff commands.
|
|||||||
the flag is still a location-only search. The Danish portals (#573) and `freehire-search`
|
the flag is still a location-only search. The Danish portals (#573) and `freehire-search`
|
||||||
(#568) already reject a value-less option.
|
(#568) already reject a value-less option.
|
||||||
|
|
||||||
|
- **`jobindex-search` rejects a `--sort` other than `score` or `date` instead of sending it to the
|
||||||
|
portal** (#647, `.agents/skills/jobindex-search/cli/src/commands/search.ts`,
|
||||||
|
`tests/sort-values.test.ts`) - the option was a free string, and jobindex does not reject an
|
||||||
|
unknown sort: `--sort newest` returned exit 0 in an order that was neither relevance nor date,
|
||||||
|
so the requested newest-first sort was silently not applied. The option is now an enum like
|
||||||
|
`--format`, and any other value exits 1 with the CLI's validation error before any request.
|
||||||
|
|
||||||
## [1.7.3] - 2026-10-19
|
## [1.7.3] - 2026-10-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Reference in new issue
Block a user