From ba9b1d83707db3b94e255f922ebbc5fccd5c2ec4 Mon Sep 17 00:00:00 2001 From: Ayobami Adegoke Date: Thu, 3 Sep 2026 18:33:54 +0100 Subject: [PATCH] fix(jobnet-search): degrade a null publicationDate to a null date instead of crashing the search (#418) (#419) date: job.publicationDate.slice(0, 10) trusted a TypeScript interface claim nothing validates at runtime: apiFetch casts the JSON body, so one ad with a null publication date threw TypeError inside the jobAds map and the whole search exited 1 as API_ERROR. The neighboring applicationDeadline field was already null-guarded with a 1900-01-01 sentinel. publicationDate is now typed nullable so the compiler enforces the guard, and the ad degrades per-item to date: null. New test verified to fail on the unfixed code with the exact production TypeError. --- .../jobnet-search/cli/src/commands/search.ts | 10 +++++++--- .../cli/tests/search-normalization.test.ts | 20 +++++++++++++++++++ CHANGELOG.md | 12 ++++++++++- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/.agents/skills/jobnet-search/cli/src/commands/search.ts b/.agents/skills/jobnet-search/cli/src/commands/search.ts index 6b6232d..9f978cd 100644 --- a/.agents/skills/jobnet-search/cli/src/commands/search.ts +++ b/.agents/skills/jobnet-search/cli/src/commands/search.ts @@ -18,7 +18,11 @@ export interface JobAdRaw { postalCode: number | null postalDistrictName: string | null country: string - publicationDate: string + // A TypeScript claim is not runtime validation: apiFetch casts the JSON + // body, so a null here arrives typed as string and .slice() throws, + // killing the whole search as API_ERROR (#418). Typed nullable so the + // compiler enforces the guard below. + publicationDate: string | null applicationDeadline: string | null applicationDeadlineStatus: string | null workHourPartTime: boolean @@ -102,7 +106,7 @@ export function createSearchOutput(data: SearchApiResponse, flags: SearchFlags) isFavorite: job.isFavorite, company: job.hiringOrgName, location: job.postalDistrictName ?? job.municipality ?? null, - date: job.publicationDate.slice(0, 10), + date: job.publicationDate ? job.publicationDate.slice(0, 10) : null, deadline: job.applicationDeadline && !job.applicationDeadline.startsWith("1900-01-01") ? job.applicationDeadline.slice(0, 10) : null, @@ -211,7 +215,7 @@ type JobAdResult = { occupation: string | null municipality: string | null postalCode: number | null - publicationDate: string + publicationDate: string | null applicationDeadline: string | null } diff --git a/.agents/skills/jobnet-search/cli/tests/search-normalization.test.ts b/.agents/skills/jobnet-search/cli/tests/search-normalization.test.ts index 84ba9a5..e2c7d7e 100644 --- a/.agents/skills/jobnet-search/cli/tests/search-normalization.test.ts +++ b/.agents/skills/jobnet-search/cli/tests/search-normalization.test.ts @@ -161,3 +161,23 @@ describe("Jobnet search normalization", () => { expect(output.results[1].deadline).toBe("2026-08-01"); }); }); + +describe("Jobnet null publicationDate degradation", () => { + // publicationDate: string was a TypeScript claim, not runtime validation - + // apiFetch casts the JSON body, so one ad with a null publication date + // threw TypeError from .slice() inside the jobAds map and killed the whole + // search as API_ERROR (#418). The neighboring applicationDeadline field is + // already guarded (null check + 1900-01-01 sentinel); this pins the same + // per-item degradation for publicationDate: date null, no throw. + test("an ad with a null publicationDate yields date: null instead of crashing the search", () => { + const data = apiResponse(); + data.jobAds[0].publicationDate = null; + + // The shared fixture flags carry limit: 1, which would slice off the + // second ad; lift the limit so the survives-alongside assertion is real. + const output = createSearchOutput(data, { ...flags, limit: undefined }); + + expect(output.results[0].date).toBeNull(); + expect(output.results[1].date).toBe("2026-07-02"); + }); +}); diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a0296b..1c1a33f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,13 +41,23 @@ per-file diff commands. ### Fixed +- **`jobnet-search` no longer dies over one ad with a null publication date** (#418, the + sibling of #416 from the same audit) - `date: job.publicationDate.slice(0, 10)` trusted + a TypeScript interface claim (`publicationDate: string`) that nothing validates at + runtime: `apiFetch` casts the JSON body, so one `null` threw `TypeError` inside the + `jobAds` map and the whole search of a default-ON portal exited 1 as `API_ERROR` - while + the neighboring `applicationDeadline` field was already null-guarded with a `1900-01-01` + sentinel check. The field is now typed nullable (so the compiler enforces the guard) and + degrades per-item to `date: null`, the shape the `seen_jobs.json` contract documents. + Pinned by a new case in `search-normalization.test.ts`, verified to fail on the unfixed + code with the exact production TypeError. + - **Placeholder-integrity tests in `python-tests` now skip on forks** (#405) - the dedicated `placeholder-integrity` job already gates on the upstream repo name, but `python-tests` ran `unittest discover` with no such guard, so forks that personalized files via `/setup` failed three sentinel checks permanently. Both test classes now use `@unittest.skipIf` on `GITHUB_REPOSITORY` (defaulting to upstream when unset so local pristine-template runs still execute). - - **`convert_salary_excel.py` no longer mistakes a title/citation row for the header row** (#414) - header-row detection accepted the first row in the first 10 where *any* cell merely contained a company-pattern word, with no check that the row actually looked like a header. A