Files
ai-job-search/.agents/skills/linkedin-search/cli/tests/helpers.ts
T
Rudy Mizrahi Celekli 2017521d2e test(portal-clis): isolate valid flag controls from live services (#581)
* test(portal-clis): isolate valid flag controls from live services

The LinkedIn and Freehire positive controls spawned live search CLIs and only checked for the absence of BAD_ARG, so attempted portal requests still left a green suite. Use loopback responses and assert successful output plus the constructed freshness, pagination and result-limit contract.

LinkedIn cards are verbatim excerpts from portapulito's actual captured response in PR #153; fixture provenance and original hashes are retained. Four Danish validation suites already stop at required-argument guards and need no change. This isolates these controls rather than claiming a whole-CI egress policy.

* docs(changelog): record fix for #581
2026-10-18 06:57:43 +02:00

73 lines
2.3 KiB
TypeScript

import { join } from "path";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
const CLI_PATH = join(import.meta.dir, "../src/cli.ts");
export interface CLIResult {
stdout: string;
stderr: string;
exitCode: number;
}
export async function runCLI(args: string[], preload?: string): Promise<CLIResult> {
const proc = Bun.spawn(["bun", "run", ...(preload ? ["--preload", preload] : []), CLI_PATH, ...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 };
}
/** Run valid search controls through the real CLI with a saved, offline response. */
export async function runSearchFixture(args: string[]): Promise<{ result: CLIResult; requests: URL[] }> {
const requests: URL[] = [];
const server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
async fetch(request) {
requests.push(new URL(request.url));
return new Response(await Bun.file(join(import.meta.dir, "fixtures/search-cards.json")).json());
},
});
const dir = mkdtempSync(join(tmpdir(), "linkedin-valid-flags-"));
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.origin !== "https://www.linkedin.com" || url.pathname !== "/jobs-guest/jobs/api/seeMoreJobPostings/search") {
throw new Error("Unexpected search fixture request");
}
return originalFetch(${JSON.stringify(server.url.origin)} + url.pathname + url.search, init);
};
`);
try {
return { result: await runCLI(args, preload), requests };
} finally {
server.stop(true);
rmSync(dir, { recursive: true, force: true });
}
}
export function parseJSON<T = unknown>(result: CLIResult): T {
if (result.exitCode !== 0) {
throw new Error(
`CLI exited with code ${result.exitCode}. stderr: ${result.stderr}`
);
}
try {
return JSON.parse(result.stdout) as T;
} catch {
throw new Error(
`Failed to parse JSON. stdout: ${result.stdout}\nstderr: ${result.stderr}`
);
}
}