fix(freehire-search): decode HTML entities and trim whitespace in titles (#505)

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 ` and the same title reached
`search` output. In a 93-result /scrape batch, 1 title carried `&` and
3 ended in whitespace; /scrape Step 4.75 reads an undecoded entity in a
title as a half-working parser.

Run the title through the existing decodeHtmlEntities (already used for
detail descriptions) and trim it; an empty or whitespace-only title still
falls back to "(untitled)". Three new parsing.test.ts cases pin it; all
three fail on the old pass-through.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sahil Kumar SahuandClaude Opus 5.5 authored and GitHub committed 2026-09-29 20:24:56 +02:00
1 parent 9187f894a5
commit 4f5520841f
3 files changed
+37 -2

No files matched your search

@@ -145,11 +145,15 @@ export interface JobDetailResult extends JobResult {
description: string | null 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 &amp; AI Innovation "), so it is decoded and trimmed.
*/
export function toResult(j: FreehireJob): JobResult { export function toResult(j: FreehireJob): JobResult {
return { return {
id: j.public_slug, id: j.public_slug,
title: j.title || "(untitled)", title: decodeHtmlEntities(j.title || "").trim() || "(untitled)",
company: j.company || null, company: j.company || null,
company_slug: j.company_slug || null, company_slug: j.company_slug || null,
location: j.location || null, location: j.location || null,
@@ -49,6 +49,24 @@ describe("toResult — reshape into the portal-skill contract", () => {
expect(r.date).toBeNull(); expect(r.date).toBeNull();
expect(r.work_mode).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 &amp; AI Innovation " })).title).toBe(
"Intern - Fullstack & AI Innovation",
);
expect(toResult(job({ title: "R&#38;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 &amp; Analytics Intern" })).title).toBe("Data & Analytics Intern");
});
}); });
describe("toDetail — adds cleaned description + enrichment", () => { describe("toDetail — adds cleaned description + enrichment", () => {
+13
View File
@@ -105,6 +105,19 @@ per-file diff commands.
### Fixed ### 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 &amp; 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 `&amp;` 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 - **`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` 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: printed only the currency and amounts, although freehire's enrichment records the period: