mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 08:36:25 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
07cec1f227
commit
bcba687fbf
@@ -59,6 +59,23 @@ export interface DetailApiResponse {
|
|||||||
user: string | null
|
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({
|
export const detail = defineCommand({
|
||||||
name: "detail",
|
name: "detail",
|
||||||
description: "Full detail for a single job ad",
|
description: "Full detail for a single job ad",
|
||||||
@@ -77,9 +94,10 @@ export const detail = defineCommand({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await apiFetch<DetailApiResponse>(
|
const data = prepareDetail(
|
||||||
`/FindJob/JobAdDetails/${id}`,
|
await apiFetch<DetailApiResponse>(`/FindJob/JobAdDetails/${id}`, {
|
||||||
{ incrementViews: "false" }
|
incrementViews: "false",
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (signal.aborted) return
|
if (signal.aborted) return
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
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> = {}): DetailApiResponse {
|
function detail(overrides: Partial<DetailApiResponse> = {}): DetailApiResponse {
|
||||||
return {
|
return {
|
||||||
@@ -93,3 +93,29 @@ describe("formatDetailPlain", () => {
|
|||||||
expect(formatted).not.toContain("Apply:");
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -120,6 +120,13 @@ per-file diff commands.
|
|||||||
|
|
||||||
### Fixed
|
### 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
|
- **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
|
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
|
header comment and the hyperref `pdftitle`; `/setup`'s documented edit replaces the
|
||||||
|
|||||||
Reference in New Issue
Block a user