From 0883958d43170658c85bb07c4ebebc9a21745f9e Mon Sep 17 00:00:00 2001 From: Ayobami Adegoke Date: Wed, 2 Sep 2026 20:34:53 +0100 Subject: [PATCH] fix(jobbank-search): degrade an unparseable pubDate to a null date instead of crashing the search (#416) (#417) new Date() yields an Invalid Date whose toISOString() throws RangeError, and normalizeSearchItem runs inside an unguarded items.map(), so one malformed RSS item killed the entire search with {"error": "Invalid Date", "code": "API_ERROR"} and exit 1. The un-CDATA'd fallback capture in parseRssItems can deliver exactly such a value. An unparseable pubDate now degrades to the same shape as an absent one (posted "", date null); every other item survives. Three new cases pin the malformed shapes, each failing on the unfixed code. --- .../jobbank-search/cli/src/commands/search.ts | 8 +++++++- .../cli/tests/search-normalization.test.ts | 15 +++++++++++++++ CHANGELOG.md | 10 ++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/.agents/skills/jobbank-search/cli/src/commands/search.ts b/.agents/skills/jobbank-search/cli/src/commands/search.ts index 9f4664a..cfc064b 100644 --- a/.agents/skills/jobbank-search/cli/src/commands/search.ts +++ b/.agents/skills/jobbank-search/cli/src/commands/search.ts @@ -5,7 +5,13 @@ import { rssFetch, fetchWithUA, writeError, parseRssDescription, extractJobIdFro export function normalizeSearchItem(item: RssItem): Record { const parsed = parseRssDescription(item.description) const id = extractJobIdFromUrl(item.link) - const posted = item.pubDate ? new Date(item.pubDate).toISOString() : "" + // Guard the parse: new Date() is an Invalid Date whose + // toISOString() throws RangeError, and this runs inside an unguarded + // items.map() - one bad feed item would kill the whole search as + // API_ERROR (#416). An unparseable pubDate degrades to the same shape + // as an absent one: posted "", date null. + const parsedDate = item.pubDate ? new Date(item.pubDate) : null + const posted = parsedDate && !Number.isNaN(parsedDate.getTime()) ? parsedDate.toISOString() : "" return { id, title: item.title, diff --git a/.agents/skills/jobbank-search/cli/tests/search-normalization.test.ts b/.agents/skills/jobbank-search/cli/tests/search-normalization.test.ts index d41f42c..73f0a27 100644 --- a/.agents/skills/jobbank-search/cli/tests/search-normalization.test.ts +++ b/.agents/skills/jobbank-search/cli/tests/search-normalization.test.ts @@ -27,6 +27,21 @@ describe("Jobbank search normalization", () => { expect(result.date).toBeNull(); }); + // A present-but-unparseable pubDate must degrade to the same null-date shape + // as an absent one, never throw: toISOString() on an Invalid Date raises + // RangeError, and normalizeSearchItem runs inside an unguarded items.map(), + // so one bad feed item killed the whole search as API_ERROR (#416). The + // un-CDATA'd fallback capture in parseRssItems can deliver exactly such a + // value. + for (const bad of ["date unavailable", "2026-09-02T08:00:00+02:00x", "I går"]) { + test(`emits a null date instead of throwing on unparseable pubDate ${JSON.stringify(bad)}`, () => { + const result = normalizeSearchItem({ ...rssItem(), pubDate: bad }); + + expect(result.posted).toBe(""); + expect(result.date).toBeNull(); + }); + } + test("keeps the native fields alongside the contract date (additive)", () => { const result = normalizeSearchItem(rssItem()); diff --git a/CHANGELOG.md b/CHANGELOG.md index 5208a9f..6d0168a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,16 @@ per-file diff commands. ### Fixed +- **`jobbank-search` no longer dies over one malformed feed date** (#416) - `new Date()` + on a present-but-unparseable `pubDate` yields an Invalid Date whose `toISOString()` + throws `RangeError`, and `normalizeSearchItem` runs inside an unguarded `items.map()`, + so a single bad RSS item killed the entire search with `{"error": "Invalid Date", + "code": "API_ERROR"}` and exit 1 - a whole default-ON portal lost to one item, with + the error pointing at the API. The un-CDATA'd fallback capture in `parseRssItems` can + deliver exactly such a value. An unparseable `pubDate` now degrades to the same shape + as an absent one (`posted` empty, `date: null`, per the `seen_jobs.json` contract that + #391 put this field on), and every other item survives. Pinned by three new cases in + `search-normalization.test.ts`, each verified to fail on the unfixed code. - **`linkedin-search` rejects fractional numeric flags instead of silently changing the query** (#371) - bare `parseInt` truncated values before validation, so `--jobage 0.5` became `0` and silently omitted LinkedIn's `f_TPR` freshness filter