diff --git a/.agents/skills/freehire-search/SKILL.md b/.agents/skills/freehire-search/SKILL.md index 22d7344b..2ac24545 100644 --- a/.agents/skills/freehire-search/SKILL.md +++ b/.agents/skills/freehire-search/SKILL.md @@ -191,7 +191,7 @@ dictionaries never guess). So: tracking (apply/save) needs a key, and this skill deliberately does not touch it: it is **search + detail only**. - `id` in search results is the freehire `public_slug` — pass it as-is to `detail`. -- `date` is the posting date (`posted_at`); it may be `null` for undated postings. +- `date` is the posting date as `YYYY-MM-DD` (the date part of the API's `posted_at` timestamp, the shape every portal CLI emits); it may be `null` for undated postings. - Facet values are controlled vocabularies. Use `/api/v1/jobs/facets` to see the live values (with counts) for a query before filtering. - The API retries 429/5xx with exponential backoff; an unreachable API exits diff --git a/.agents/skills/freehire-search/cli/src/helpers.ts b/.agents/skills/freehire-search/cli/src/helpers.ts index d658e3ad..a27a6434 100644 --- a/.agents/skills/freehire-search/cli/src/helpers.ts +++ b/.agents/skills/freehire-search/cli/src/helpers.ts @@ -113,8 +113,10 @@ export interface FreehireJob { /** * A search result in the portal-skill contract shape. `id` is the public_slug - * (what `detail ` consumes) and `date` is the posting date; missing values - * are `null`, never omitted. The extra facet fields are a permitted superset. + * (what `detail ` consumes) and `date` is the posting date as `YYYY-MM-DD` + * (the contract shape every portal CLI emits; the API's ISO timestamp is cut to + * its date part); missing values are `null`, never omitted. The extra facet + * fields are a permitted superset. * * `description` is the posting's full text in the format the search asked the API * for — the agent search endpoint hydrates it server-side, so it arrives already @@ -150,6 +152,18 @@ export interface JobDetailResult extends JobResult { * from the source ATS as indexed, which can carry HTML entities and stray * whitespace ("Intern - Fullstack & AI Innovation "), so it is decoded and trimmed. */ +/** + * The portal-contract date (`YYYY-MM-DD`) from the API's `posted_at`, which is + * an ISO timestamp (`2026-07-06T00:00:00Z`). Every other portal CLI emits the + * date part only, and `/scrape` stores it as `posted_date: "YYYY-MM-DD"`; the + * table and plain renderers already cut it, so only JSON consumers saw the raw + * timestamp. Null when absent or not a date. + */ +export function contractDate(value: string | null | undefined): string | null { + const m = /^(\d{4}-\d{2}-\d{2})/.exec(value ?? "") + return m ? m[1] : null +} + export function toResult(j: FreehireJob): JobResult { return { id: j.public_slug, @@ -157,7 +171,7 @@ export function toResult(j: FreehireJob): JobResult { company: j.company || null, company_slug: j.company_slug || null, location: j.location || null, - date: j.posted_at, + date: contractDate(j.posted_at), url: j.url, work_mode: j.work_mode || null, regions: j.regions, diff --git a/.agents/skills/freehire-search/cli/tests/commands.test.ts b/.agents/skills/freehire-search/cli/tests/commands.test.ts index 6edada67..a01ec9fb 100644 --- a/.agents/skills/freehire-search/cli/tests/commands.test.ts +++ b/.agents/skills/freehire-search/cli/tests/commands.test.ts @@ -98,7 +98,7 @@ describe("runSearch (mocked fetch)", () => { expect(parsed.meta).toEqual({ count: 1, page: 1, total: 42 }); expect(parsed.results).toHaveLength(1); expect(parsed.results[0].id).toBe("backend-engineer-acme-ab12cd34"); - expect(parsed.results[0].date).toBe("2026-07-06T00:00:00Z"); + expect(parsed.results[0].date).toBe("2026-07-06"); }); test("queries the agent endpoint asking for full descriptions", async () => { diff --git a/.agents/skills/freehire-search/cli/tests/parsing.test.ts b/.agents/skills/freehire-search/cli/tests/parsing.test.ts index f4c6881a..f42a29fa 100644 --- a/.agents/skills/freehire-search/cli/tests/parsing.test.ts +++ b/.agents/skills/freehire-search/cli/tests/parsing.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "bun:test"; -import { cleanHtml, normalizeSlug, toResult, toDetail, type FreehireJob } from "../src/helpers"; +import { cleanHtml, contractDate, normalizeSlug, toResult, toDetail, type FreehireJob } from "../src/helpers"; function job(overrides: Partial = {}): FreehireJob { return { @@ -28,7 +28,8 @@ describe("toResult — reshape into the portal-skill contract", () => { test("maps public_slug -> id and posted_at -> date", () => { const r = toResult(job()); expect(r.id).toBe("backend-engineer-acme-ab12cd34"); - expect(r.date).toBe("2026-07-06T00:00:00Z"); + // The contract shape is the date part only, like every other portal CLI. + expect(r.date).toBe("2026-07-06"); }); test("carries the required contract fields", () => { @@ -118,6 +119,23 @@ describe("cleanHtml", () => { }); }); +describe("contractDate", () => { + test("cuts the API's ISO timestamp to YYYY-MM-DD", () => { + expect(contractDate("2026-07-06T00:00:00Z")).toBe("2026-07-06"); + expect(contractDate("2026-07-06T15:30:00+02:00")).toBe("2026-07-06"); + expect(contractDate("2026-07-06")).toBe("2026-07-06"); + }); + test("is null for an absent or non-date value", () => { + expect(contractDate(null)).toBeNull(); + expect(contractDate(undefined)).toBeNull(); + expect(contractDate("")).toBeNull(); + expect(contractDate("soon")).toBeNull(); + }); + test("detail carries the same shape as search", () => { + expect(toDetail(job()).date).toBe("2026-07-06"); + }); +}); + describe("normalizeSlug", () => { test("accepts a bare slug", () => { expect(normalizeSlug("golang-zensar-2bxu6dxm")).toBe("golang-zensar-2bxu6dxm"); diff --git a/CHANGELOG.md b/CHANGELOG.md index d8eb9d8a..7424b14d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,16 @@ per-file diff commands. ### Fixed +- **`freehire-search` emits the contract's `YYYY-MM-DD` date instead of the API's ISO timestamp** + (`.agents/skills/freehire-search/cli/src/helpers.ts`, `tests/parsing.test.ts`, + `tests/commands.test.ts`, `SKILL.md`) - `toResult` passed `posted_at` through verbatim, so + search and detail JSON carried `"date": "2026-07-06T00:00:00Z"` while every other portal CLI + emits the date part only and `/scrape` stores it as `posted_date: "YYYY-MM-DD"`. The table + and plain renderers already cut the value to ten characters, so only JSON consumers - the + `/scrape` path - saw the raw timestamp. A `contractDate()` helper now cuts the timestamp to + its date part and yields `null` for an absent or non-date value; the two tests that pinned + the raw timestamp now expect the date and fail on master, plus three cases on the helper. + - **`/rank`, `/notion-sync` and `/upskill` name `job_scraper/seen_jobs.json` by its full path** (follow-up to #608, `.claude/commands/rank.md`, `.claude/commands/notion-sync.md`, `.claude/skills/upskill/SKILL.md`, `tests/test_scrape_seen_jobs_path.py`) - #608 qualified the