fix(jobnet-search): preserve full occupation identifiers in tables (#586)

* fix(jobnet-search): preserve full occupation identifiers in tables

The documented ESCO concept URI is 72 characters long, but the table renderer cut every identifier to 70, losing the last two UUID characters. Size the identifier column from its contents, retaining the existing minimum width and readable label alignment.

Exercise the actual documented occupations CLI with the README API response served over loopback. Table output must retain the same complete URI and UUID as JSON and plain output; no live portal requests.

* test(jobnet-search): cover Windows occupation fixture newlines
This commit is contained in:
Rudy Mizrahi Celekli authored and GitHub committed 2026-10-18 07:36:47 +02:00
1 parent 176b675080
commit 12b757189a
3 files changed
+82 -2

No files matched your search

@@ -64,9 +64,12 @@ export const occupations = defineCommand({
})
function outputTable(data: Occupation[]): void {
console.log("conceptUriDa preferredLabelDa")
// The full URI is an identifier: truncating it cuts the UUID used to build
// search filters. Widen the column instead of abbreviating its value.
const uriWidth = data.reduce((width, o) => Math.max(width, o.conceptUriDa.length), 70)
console.log(`${"conceptUriDa".padEnd(uriWidth)} preferredLabelDa`)
for (const o of data) {
const uri = o.conceptUriDa.substring(0, 70).padEnd(70)
const uri = o.conceptUriDa.padEnd(uriWidth)
const label = o.preferredLabelDa
console.log(`${uri} ${label}`)
}
@@ -0,0 +1,75 @@
import { expect, test } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
async function occupations(format: "json" | "plain" | "table", lineEnding: "LF" | "CRLF") {
const source = await Bun.file(join(import.meta.dir, "../README.md")).text();
const checkout = source.replace(/\r\n/g, "\n").replace(/\n/g, lineEnding === "CRLF" ? "\r\n" : "\n");
expect(checkout.includes("\r\n")).toBe(lineEnding === "CRLF");
const readme = checkout.replace(/\r\n/g, "\n");
const section = readme.split("## `occupations`")[1].split("\n---")[0];
// Use the repository's documented API response unchanged, not a shortened
// synthetic identifier that would conceal the formatting failure.
const fixture = JSON.parse(section.match(/```json\n([\s\S]*?)```/)![1]);
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(fixture);
},
});
const dir = mkdtempSync(join(tmpdir(), "jobnet-occupation-"));
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://jobnet.dk" || url.pathname !== "/bff/OccupationSearch") {
throw new Error("Unexpected occupation fixture request");
}
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"), "occupations",
"--search-string", "sygeplejerske", "--per-page", "5", "--format", format,
], { stdout: "pipe", stderr: "pipe" });
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(requests).toHaveLength(1);
expect(requests[0].searchParams.get("searchString")).toBe("sygeplejerske");
expect(requests[0].searchParams.get("pageSize")).toBe("5");
return { stdout, fixture };
} finally {
server.stop(true);
rmSync(dir, { recursive: true, force: true });
}
}
test.each(["LF", "CRLF"] as const)("occupation table preserves the documented full ESCO URI and UUID with %s newlines", async (lineEnding) => {
const { stdout, fixture } = await occupations("table", lineEnding);
const uri = fixture[0].conceptUriDa;
expect(uri.length).toBeGreaterThan(70);
expect(stdout.trim().split("\n")[1].split(/\s+/)[0]).toBe(uri);
expect(stdout).toContain(uri.split("/").at(-1));
});
test.each(["LF", "CRLF"] as const)("occupation JSON retains the same complete identifier with %s newlines", async (lineEnding) => {
const { stdout, fixture } = await occupations("json", lineEnding);
expect(JSON.parse(stdout)).toEqual(fixture);
});
test.each(["LF", "CRLF"] as const)("occupation plain output retains the same complete identifier with %s newlines", async (lineEnding) => {
const { stdout, fixture } = await occupations("plain", lineEnding);
expect(stdout).toContain(`uri: ${fixture[0].conceptUriDa}`);
expect(stdout).toContain(`label: ${fixture[0].preferredLabelDa}`);
});
+2
View File
@@ -159,6 +159,8 @@ per-file diff commands.
`UnicodeEncodeError` when writing stdout or `UnicodeDecodeError` in subprocess pipes, terminating
triage runs before the report could be emitted.
- **`jobnet-search occupations` preserves full identifiers in table output** - ESCO URIs and UUIDs are no longer truncated, with documentation-based regression checks covering LF and CRLF checkouts (#586).
- **`jobnet-search` routes external ads to employer `jobAdUrl` rather than broken `find-job` links**
(#538, `.agents/skills/jobnet-search/cli/src/commands/search.ts`, `.agents/skills/jobnet-search/cli/README.md`,
`.agents/skills/jobnet-search/cli/tests/search-normalization.test.ts`) - external job postings on