diff --git a/.agents/skills/freehire-search/cli/src/helpers.ts b/.agents/skills/freehire-search/cli/src/helpers.ts index cf4e4e89..d658e3ad 100644 --- a/.agents/skills/freehire-search/cli/src/helpers.ts +++ b/.agents/skills/freehire-search/cli/src/helpers.ts @@ -145,11 +145,15 @@ export interface JobDetailResult extends JobResult { description: string | null } -/** Reshape a freehire job into the contract search-result fields. */ +/** + * Reshape a freehire job into the contract search-result fields. The title comes + * from the source ATS as indexed, which can carry HTML entities and stray + * whitespace ("Intern - Fullstack & AI Innovation "), so it is decoded and trimmed. + */ export function toResult(j: FreehireJob): JobResult { return { id: j.public_slug, - title: j.title || "(untitled)", + title: decodeHtmlEntities(j.title || "").trim() || "(untitled)", company: j.company || null, company_slug: j.company_slug || null, location: j.location || null, diff --git a/.agents/skills/freehire-search/cli/tests/parsing.test.ts b/.agents/skills/freehire-search/cli/tests/parsing.test.ts index d5f7179b..f4c6881a 100644 --- a/.agents/skills/freehire-search/cli/tests/parsing.test.ts +++ b/.agents/skills/freehire-search/cli/tests/parsing.test.ts @@ -49,6 +49,24 @@ describe("toResult — reshape into the portal-skill contract", () => { expect(r.date).toBeNull(); expect(r.work_mode).toBeNull(); }); + + test("decodes HTML entities and trims whitespace in the title", () => { + // Real title from the index (freehire slug intern-fullstack-amp-ai-innovation-noise-edaawqq2). + expect(toResult(job({ title: "Intern - Fullstack & AI Innovation " })).title).toBe( + "Intern - Fullstack & AI Innovation", + ); + expect(toResult(job({ title: "R&D Engineer" })).title).toBe("R&D Engineer"); + expect(toResult(job({ title: "SDE Intern - Frontend " })).title).toBe("SDE Intern - Frontend"); + }); + + test("a whitespace-only title still falls back to (untitled)", () => { + expect(toResult(job({ title: " " })).title).toBe("(untitled)"); + expect(toResult(job({ title: "" })).title).toBe("(untitled)"); + }); + + test("detail inherits the cleaned title", () => { + expect(toDetail(job({ title: "Data & Analytics Intern" })).title).toBe("Data & Analytics Intern"); + }); }); describe("toDetail — adds cleaned description + enrichment", () => { diff --git a/CHANGELOG.md b/CHANGELOG.md index a32ccaf6..02d907cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,19 @@ per-file diff commands. ### Fixed +- **`freehire-search` titles no longer carry HTML entities or stray whitespace** + (`.agents/skills/freehire-search/cli/src/helpers.ts`) - titles are indexed as the source ATS + wrote them and `toResult` passed them through untouched, so `detail + intern-fullstack-amp-ai-innovation-noise-edaawqq2` printed `Intern - Fullstack & AI + Innovation ` (trailing space included), and the same title reached `search` output. In a + 93-result `/scrape` batch (`--country IN --seniority intern`), 1 title carried `&` and 3 + ended in whitespace. `/scrape` Step 4.75 reads undecoded entities in titles as a + half-working parser, so by that rule a single such posting marks a healthy portal + degraded. The title now goes through the CLI's existing `decodeHtmlEntities` (already used + for `detail` descriptions) and is trimmed; an empty or whitespace-only title still falls + back to `(untitled)`. Pinned by three new `parsing.test.ts` cases, which fail on the old + pass-through. + - **`freehire-search detail` keeps the salary period, so a yearly figure no longer reads as monthly** (`.agents/skills/freehire-search/cli/src/helpers.ts`, `SKILL.md`) - `formatSalary` printed only the currency and amounts, although freehire's enrichment records the period: