mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
fix(portal-clis): emit the contract's id field from jobnet and jobdanmark search results (#518)
/add-portal's contract says every search result carries at least id, title, company, location, date, url, and the four other shipped CLIs emit id. When #340 added the company/location/date/url aliases to these two, id was left out: jobnet exposed the value only as jobAdId, jobdanmark only as slug, so a consumer reading every portal's JSON through one shape had to special-case both to call detail. Purely additive: id equals jobAdId on jobnet and slug on jobdanmark, the native keys stay, nothing is renamed. Each SKILL.md notes the alias. The additive-contract test in each CLI now asserts id and its equality with the native key; both fail on master.
This commit is contained in:
1 parent
e9115b02f8
commit
895c02191d
7 files changed
+30
-4
No files matched your search
@@ -224,6 +224,6 @@ All errors are written to **stderr** as `{ "error": "...", "code": "..." }` and
|
|||||||
- All data is from the public Jobdanmark.dk API — no credentials required.
|
- All data is from the public Jobdanmark.dk API — no credentials required.
|
||||||
- Pagination is 1-indexed (`--page 1` is the first page). 30 items per page, server-enforced.
|
- Pagination is 1-indexed (`--page 1` is the first page). 30 items per page, server-enforced.
|
||||||
- The `detail` command fetches the HTML job page and parses embedded JSON-LD when available, with a rendered-HTML fallback for pages that omit structured data. It does not use a separate JSON API.
|
- The `detail` command fetches the HTML job page and parses embedded JSON-LD when available, with a rendered-HTML fallback for pages that omit structured data. It does not use a separate JSON API.
|
||||||
- `slug` in search results is extracted from the API's relative `url` field (the path after `/job/`).
|
- `slug` in search results is extracted from the API's relative `url` field (the path after `/job/`); it is also emitted as `id`, the portal-contract name every search CLI shares.
|
||||||
- `applicationDeadline` in search results can be `null` (no deadline set).
|
- `applicationDeadline` in search results can be `null` (no deadline set).
|
||||||
- Job type values for filters: `fuldtid`, `deltid`, `fleksjob`, `elev`, `studiejob`, `praktik`.
|
- Job type values for filters: `fuldtid`, `deltid`, `fleksjob`, `elev`, `studiejob`, `praktik`.
|
||||||
@@ -74,6 +74,10 @@ export function normalizeItem(item: ApiSearchItem): Record<string, unknown> {
|
|||||||
applicationDeadline: item.applicationDeadline ?? null,
|
applicationDeadline: item.applicationDeadline ?? null,
|
||||||
url: fullUrl,
|
url: fullUrl,
|
||||||
slug,
|
slug,
|
||||||
|
// /scrape contract aliases (add-portal.md: every result carries at least
|
||||||
|
// id, title, company, location, date, url). `id` is the slug - the value
|
||||||
|
// `detail` takes - under the name every other portal CLI uses.
|
||||||
|
id: slug,
|
||||||
company: item.companyName,
|
company: item.companyName,
|
||||||
location: extractCity(item.companyAddress),
|
location: extractCity(item.companyAddress),
|
||||||
date: toContractDate(item.publishedDate),
|
date: toContractDate(item.publishedDate),
|
||||||
|
|||||||
@@ -20,16 +20,19 @@ function item(): ApiSearchItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("Jobdanmark search normalization", () => {
|
describe("Jobdanmark search normalization", () => {
|
||||||
test("additively emits the /scrape contract fields (company, location, date, deadline)", () => {
|
test("additively emits the /scrape contract fields (id, company, location, date, deadline, url)", () => {
|
||||||
const result = normalizeItem(item());
|
const result = normalizeItem(item());
|
||||||
|
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
|
id: "softwareudvikler-til-statens-it",
|
||||||
company: "Statens It",
|
company: "Statens It",
|
||||||
location: "Ballerup",
|
location: "Ballerup",
|
||||||
date: "2026-07-27",
|
date: "2026-07-27",
|
||||||
deadline: "2026-08-17",
|
deadline: "2026-08-17",
|
||||||
url: "https://jobdanmark.dk/job/softwareudvikler-til-statens-it",
|
url: "https://jobdanmark.dk/job/softwareudvikler-til-statens-it",
|
||||||
});
|
});
|
||||||
|
// `id` is the contract name for the value `detail <slug>` consumes.
|
||||||
|
expect(result.id).toBe(result.slug);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("maps a missing address zip and a null deadline to null", () => {
|
test("maps a missing address zip and a null deadline to null", () => {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ Key flags:
|
|||||||
bun run .agents/skills/jobnet-search/cli/src/cli.ts detail <jobAdId> [--format json|plain]
|
bun run .agents/skills/jobnet-search/cli/src/cli.ts detail <jobAdId> [--format json|plain]
|
||||||
```
|
```
|
||||||
|
|
||||||
`jobAdId` is the UUID from `search` results (the `jobAdId` field). Returns the complete job
|
`jobAdId` is the UUID from `search` results (the `jobAdId` field, also emitted as `id`, the portal-contract name every search CLI shares). Returns the complete job
|
||||||
description, contact persons, application deadline, employer details, and direct application URL.
|
description, contact persons, application deadline, employer details, and direct application URL.
|
||||||
|
|
||||||
### Search occupation types
|
### Search occupation types
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ export function createSearchOutput(data: SearchApiResponse, flags: SearchFlags)
|
|||||||
workPlaceAddress: job.workPlaceAddress ?? "",
|
workPlaceAddress: job.workPlaceAddress ?? "",
|
||||||
isSeen: job.isSeen,
|
isSeen: job.isSeen,
|
||||||
isFavorite: job.isFavorite,
|
isFavorite: job.isFavorite,
|
||||||
|
// /scrape contract aliases (add-portal.md: every result carries at least
|
||||||
|
// id, title, company, location, date, url). `id` is the jobAdId - the
|
||||||
|
// value `detail` takes - under the name every other portal CLI uses.
|
||||||
|
id: job.jobAdId,
|
||||||
company: job.hiringOrgName,
|
company: job.hiringOrgName,
|
||||||
location: job.postalDistrictName ?? job.municipality ?? null,
|
location: job.postalDistrictName ?? job.municipality ?? null,
|
||||||
date: job.publicationDate ? job.publicationDate.slice(0, 10) : null,
|
date: job.publicationDate ? job.publicationDate.slice(0, 10) : null,
|
||||||
|
|||||||
@@ -128,11 +128,12 @@ describe("Jobnet search normalization", () => {
|
|||||||
expect("description" in output.results[0]).toBe(false);
|
expect("description" in output.results[0]).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("additively emits the /scrape contract fields (company, location, date, deadline, url)", () => {
|
test("additively emits the /scrape contract fields (id, company, location, date, deadline, url)", () => {
|
||||||
const output = createSearchOutput(apiResponse(), { ...flags, limit: undefined });
|
const output = createSearchOutput(apiResponse(), { ...flags, limit: undefined });
|
||||||
|
|
||||||
expect(output.results).toHaveLength(2);
|
expect(output.results).toHaveLength(2);
|
||||||
expect(output.results[0]).toMatchObject({
|
expect(output.results[0]).toMatchObject({
|
||||||
|
id: "job-1",
|
||||||
company: "Acme",
|
company: "Acme",
|
||||||
location: null,
|
location: null,
|
||||||
date: "2026-07-01",
|
date: "2026-07-01",
|
||||||
@@ -140,6 +141,7 @@ describe("Jobnet search normalization", () => {
|
|||||||
url: "https://jobnet.dk/find-job/job-1",
|
url: "https://jobnet.dk/find-job/job-1",
|
||||||
});
|
});
|
||||||
expect(output.results[1]).toMatchObject({
|
expect(output.results[1]).toMatchObject({
|
||||||
|
id: "job-2",
|
||||||
company: "Example Co",
|
company: "Example Co",
|
||||||
location: "København Ø",
|
location: "København Ø",
|
||||||
date: "2026-07-02",
|
date: "2026-07-02",
|
||||||
@@ -148,6 +150,8 @@ describe("Jobnet search normalization", () => {
|
|||||||
});
|
});
|
||||||
expect(output.results[0].hiringOrgName).toBe("Acme");
|
expect(output.results[0].hiringOrgName).toBe("Acme");
|
||||||
expect(output.results[1].applicationDeadline).toBe("2026-08-01T23:59:00+02:00");
|
expect(output.results[1].applicationDeadline).toBe("2026-08-01T23:59:00+02:00");
|
||||||
|
// `id` is the contract name for the value `detail <jobAdId>` consumes.
|
||||||
|
expect(output.results[0].id).toBe(output.results[0].jobAdId);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("maps Jobnet's undisclosed-deadline sentinel (1900-01-01) to null", () => {
|
test("maps Jobnet's undisclosed-deadline sentinel (1900-01-01) to null", () => {
|
||||||
|
|||||||
@@ -15,6 +15,17 @@ per-file diff commands.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **`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`,
|
||||||
|
`location`, `date`, `url`, and the four other shipped CLIs emit `id`. When #340 added the
|
||||||
|
`company`/`location`/`date`/`url` aliases to these two, `id` was left out: jobnet exposed the
|
||||||
|
value only as `jobAdId`, jobdanmark only as `slug`, so a consumer reading every portal's
|
||||||
|
JSON through one shape had to special-case both to call `detail`. Purely additive: `id`
|
||||||
|
equals `jobAdId` on jobnet and `slug` on jobdanmark, the native keys stay, nothing is
|
||||||
|
renamed. The additive-contract test in each CLI now asserts `id` and its equality with the
|
||||||
|
native key; both fail on master.
|
||||||
|
|
||||||
- **`jobdanmark-search detail` renders a JSON-LD description as text instead of passing
|
- **`jobdanmark-search detail` renders a JSON-LD description as text instead of passing
|
||||||
the markup through** (`.agents/skills/jobdanmark-search/cli/src/commands/detail.ts`,
|
the markup through** (`.agents/skills/jobdanmark-search/cli/src/commands/detail.ts`,
|
||||||
`tests/detail-jsonld.test.ts`) - the JSON-LD branch emitted `jobPosting.description`
|
`tests/detail-jsonld.test.ts`) - the JSON-LD branch emitted `jobPosting.description`
|
||||||
|
|||||||
Reference in new issue
Block a user