From 3a184bc115e1400b600070578fb4374179779c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=BD=E5=B0=98?= Date: Mon, 20 Jul 2026 01:38:32 +0800 Subject: [PATCH] fix(jobbank): parse JobPosting entries nested in JSON-LD @graph (#190) Extracts the JSON-LD JobPosting lookup into a recursive parseJobPostingJsonLd helper that handles top-level objects, arrays, and @graph wrappers (including nested combinations), keeps skipping malformed scripts, and covers all four cases with network-free Bun tests. By @luochen211. Closes #189. --- .../jobbank-search/cli/src/commands/detail.ts | 29 +------------- .../skills/jobbank-search/cli/src/helpers.ts | 35 ++++++++++++++++ .../cli/tests/detail-jsonld.test.ts | 40 +++++++++++++++++++ 3 files changed, 77 insertions(+), 27 deletions(-) create mode 100644 .agents/skills/jobbank-search/cli/tests/detail-jsonld.test.ts diff --git a/.agents/skills/jobbank-search/cli/src/commands/detail.ts b/.agents/skills/jobbank-search/cli/src/commands/detail.ts index b752b3e..5da2d6e 100644 --- a/.agents/skills/jobbank-search/cli/src/commands/detail.ts +++ b/.agents/skills/jobbank-search/cli/src/commands/detail.ts @@ -1,7 +1,6 @@ import { defineCommand, option } from "@bunli/core" import { z } from "zod" -import { fetchWithUA, writeError, BASE_URL } from "../helpers.js" -import { parse as parseHtml } from "node-html-parser" +import { fetchWithUA, parseJobPostingJsonLd, writeError, BASE_URL } from "../helpers.js" export const detail = defineCommand({ name: "detail", @@ -39,31 +38,7 @@ export const detail = defineCommand({ if (signal.aborted) return - const root = parseHtml(html) - - // Find all ` + +describe("parseJobPostingJsonLd", () => { + test("finds a JobPosting inside an @graph", () => { + const html = script( + JSON.stringify({ + "@context": "https://schema.org", + "@graph": [ + { "@type": "WebPage", name: "Jobs" }, + { "@type": "JobPosting", title: "Data Engineer" }, + ], + }), + ) + + expect(parseJobPostingJsonLd(html)).toEqual({ + "@type": "JobPosting", + title: "Data Engineer", + }) + }) + + test("preserves top-level object and array support", () => { + expect(parseJobPostingJsonLd(script('{"@type":"JobPosting","title":"One"}'))?.title).toBe("One") + expect( + parseJobPostingJsonLd(script('[{"@type":"WebPage"},{"@type":"JobPosting","title":"Two"}]'))?.title, + ).toBe("Two") + }) + + test("skips malformed scripts and checks later JSON-LD", () => { + const html = `${script("{not-json")}${script('{"@type":"JobPosting","title":"Valid"}')}` + + expect(parseJobPostingJsonLd(html)?.title).toBe("Valid") + }) + + test("returns null when no JobPosting exists", () => { + expect(parseJobPostingJsonLd(script('{"@graph":[{"@type":"WebPage"}]}'))).toBeNull() + }) +})