From bcba687fbf470b06f33466c2b8df69a63403f07b Mon Sep 17 00:00:00 2001 From: Mads Lorentzen Date: Wed, 19 Aug 2026 20:50:55 +0200 Subject: [PATCH] fix(jobnet-search): map the 1900-01-01 deadline sentinel to null in detail search guards the API's undisclosed-deadline sentinel and a test pins it; detail dumped the raw response, so the same field for the same job behaved two ways, and an undisclosed deadline stored via detail read as 126 years expired - /rank's sweep would retire the job on sight. All three output formats now flow through prepareDetail. Review finding F33 (2026-08-19). Co-Authored-By: Claude Opus 5 (1M context) --- .../jobnet-search/cli/src/commands/detail.ts | 24 ++++++++++++++-- .../cli/tests/detail-formatting.test.ts | 28 ++++++++++++++++++- CHANGELOG.md | 7 +++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/.agents/skills/jobnet-search/cli/src/commands/detail.ts b/.agents/skills/jobnet-search/cli/src/commands/detail.ts index c3cd25a..08ed460 100644 --- a/.agents/skills/jobnet-search/cli/src/commands/detail.ts +++ b/.agents/skills/jobnet-search/cli/src/commands/detail.ts @@ -59,6 +59,23 @@ export interface DetailApiResponse { user: string | null } +/** + * Normalize a raw detail response before any output format sees it. + * + * The API's "deadline not disclosed" sentinel is 1900-01-01 (it arrives with + * isApplicationDeadlineASAP / an applicationDeadlineStatus of NotDisclosed). + * The search command already maps that sentinel to null; detail must agree, + * or an undisclosed deadline reads as 126 years expired and /rank's expiry + * sweep retires the job the moment it is stored. + */ +export function prepareDetail(data: DetailApiResponse): DetailApiResponse { + const deadline = data.application.deadlineDate + if (deadline && deadline.startsWith("1900-01-01")) { + data.application.deadlineDate = null + } + return data +} + export const detail = defineCommand({ name: "detail", description: "Full detail for a single job ad", @@ -77,9 +94,10 @@ export const detail = defineCommand({ } try { - const data = await apiFetch( - `/FindJob/JobAdDetails/${id}`, - { incrementViews: "false" } + const data = prepareDetail( + await apiFetch(`/FindJob/JobAdDetails/${id}`, { + incrementViews: "false", + }), ) if (signal.aborted) return diff --git a/.agents/skills/jobnet-search/cli/tests/detail-formatting.test.ts b/.agents/skills/jobnet-search/cli/tests/detail-formatting.test.ts index 28dde67..ce3799e 100644 --- a/.agents/skills/jobnet-search/cli/tests/detail-formatting.test.ts +++ b/.agents/skills/jobnet-search/cli/tests/detail-formatting.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { formatDetailPlain, type DetailApiResponse } from "../src/commands/detail"; +import { formatDetailPlain, prepareDetail, type DetailApiResponse } from "../src/commands/detail"; function detail(overrides: Partial = {}): DetailApiResponse { return { @@ -93,3 +93,29 @@ describe("formatDetailPlain", () => { expect(formatted).not.toContain("Apply:"); }); }); + + +describe("prepareDetail deadline sentinel", () => { + // The API's "deadline not disclosed" sentinel is 1900-01-01 (paired with + // isApplicationDeadlineASAP / applicationDeadlineStatus). search maps it to + // null and has a test pinning that; detail dumped the raw response, so an + // undisclosed deadline read as 126 years expired and /rank's sweep would + // retire the job instantly (review finding F33, 2026-08-19). + test("maps the 1900-01-01 undisclosed sentinel to null", () => { + const data = detail(); + data.application.deadlineDate = "1900-01-01T00:00:00+01:00"; + expect(prepareDetail(data).application.deadlineDate).toBeNull(); + }); + + test("keeps a real deadline unchanged", () => { + const data = detail(); + data.application.deadlineDate = "2026-09-01T00:00:00+02:00"; + expect(prepareDetail(data).application.deadlineDate).toBe("2026-09-01T00:00:00+02:00"); + }); + + test("keeps a null deadline null", () => { + const data = detail(); + data.application.deadlineDate = null; + expect(prepareDetail(data).application.deadlineDate).toBeNull(); + }); +}); diff --git a/CHANGELOG.md b/CHANGELOG.md index 9655818..8f38d6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,13 @@ per-file diff commands. ### Fixed +- **`jobnet-search detail` no longer leaks the `1900-01-01` undisclosed-deadline + sentinel** - `search` maps the API's sentinel to `null` (with a test pinning it), but + `detail` dumped the raw response, so a posting whose deadline is simply not disclosed + contributed a deadline 126 years in the past to stored data, and `/rank`'s expiry + sweep would retire the job instantly. All three output formats now flow through a + `prepareDetail` normalization that maps the sentinel to `null`. Pinned in + `tests/detail-formatting.test.ts`. - **CI's placeholder guard now watches the CV's actual personal-data lines** - the sentinel for `cv/main_example.tex` was `[YOUR_NAME]`, whose only occurrences are a header comment and the hyperref `pdftitle`; `/setup`'s documented edit replaces the