From 12b757189aef2a4c79d9e2af0a3811735dc4ce94 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:36:47 -0400 Subject: [PATCH] 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 --- .../cli/src/commands/occupations.ts | 7 +- .../cli/tests/occupation-table-cli.test.ts | 75 +++++++++++++++++++ CHANGELOG.md | 2 + 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 .agents/skills/jobnet-search/cli/tests/occupation-table-cli.test.ts diff --git a/.agents/skills/jobnet-search/cli/src/commands/occupations.ts b/.agents/skills/jobnet-search/cli/src/commands/occupations.ts index 3f8676e3..6beac7cb 100644 --- a/.agents/skills/jobnet-search/cli/src/commands/occupations.ts +++ b/.agents/skills/jobnet-search/cli/src/commands/occupations.ts @@ -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}`) } diff --git a/.agents/skills/jobnet-search/cli/tests/occupation-table-cli.test.ts b/.agents/skills/jobnet-search/cli/tests/occupation-table-cli.test.ts new file mode 100644 index 00000000..4751acf3 --- /dev/null +++ b/.agents/skills/jobnet-search/cli/tests/occupation-table-cli.test.ts @@ -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}`); +}); diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cc69abb..4865b318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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