From 6ef295bf7b5b54dbfd2e6022375d010011a8b615 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Wed, 2 Sep 2026 01:05:06 +0530 Subject: [PATCH] fix(linkedin-search): accept LinkedIn job URLs with trailing slashes in detail command (#411) (#412) * fix(linkedin-search): accept LinkedIn job URLs with trailing slashes in detail command (#411) * docs(changelog): record linkedin-search trailing-slash fix (#411) --- .../cli/src/commands/detail.ts | 4 +- .../linkedin-search/cli/tests/parsing.test.ts | 53 +++++++++++++++++++ CHANGELOG.md | 8 +++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/.agents/skills/linkedin-search/cli/src/commands/detail.ts b/.agents/skills/linkedin-search/cli/src/commands/detail.ts index cb561c4..9435543 100644 --- a/.agents/skills/linkedin-search/cli/src/commands/detail.ts +++ b/.agents/skills/linkedin-search/cli/src/commands/detail.ts @@ -6,10 +6,10 @@ export interface DetailOpts { } /** Accept a raw job ID, a job-view URL, or a job URN. */ -function normalizeId(input: string): string | null { +export function normalizeId(input: string): string | null { const urn = input.match(/urn:li:jobPosting:(\d+)/) if (urn) return urn[1] - const url = input.match(/-(\d{6,})(?:\?|$)/) || input.match(/\/(\d{6,})(?:\?|$)/) + const url = input.match(/-(\d{6,})(?:[\/?]|$)/) || input.match(/\/(\d{6,})(?:[\/?]|$)/) if (url) return url[1] const bare = input.match(/^\d{6,}$/) if (bare) return input diff --git a/.agents/skills/linkedin-search/cli/tests/parsing.test.ts b/.agents/skills/linkedin-search/cli/tests/parsing.test.ts index 19905e5..8cef41b 100644 --- a/.agents/skills/linkedin-search/cli/tests/parsing.test.ts +++ b/.agents/skills/linkedin-search/cli/tests/parsing.test.ts @@ -1,5 +1,6 @@ import { describe, test, expect } from "bun:test"; import { parseJobCards, parseJobDetail, extractDivContent, minutesToTPR } from "../src/helpers"; +import { normalizeId } from "../src/commands/detail"; // Minimal search-card markup: parseJobCards splits on the job-posting URN and // needs an id, a base-search-card__title, and a full-link. Everything else is @@ -222,3 +223,55 @@ describe("minutesToTPR", () => { expect(minutesToTPR(-5)).toBeNull(); }); }); + +describe("normalizeId", () => { + test("extracts ID from raw numeric string", () => { + expect(normalizeId("1234567890")).toBe("1234567890"); + }); + + test("extracts ID from URN", () => { + expect(normalizeId("urn:li:jobPosting:1234567890")).toBe("1234567890"); + }); + + test("extracts ID from simple job view URL without trailing slash", () => { + expect(normalizeId("https://www.linkedin.com/jobs/view/1234567890")).toBe("1234567890"); + }); + + test("extracts ID from simple job view URL with trailing slash", () => { + expect(normalizeId("https://www.linkedin.com/jobs/view/1234567890/")).toBe("1234567890"); + }); + + test("extracts ID from simple job view URL with query parameter", () => { + expect(normalizeId("https://www.linkedin.com/jobs/view/1234567890?refId=abc")).toBe("1234567890"); + }); + + test("extracts ID from simple job view URL with trailing slash and query parameter", () => { + expect(normalizeId("https://www.linkedin.com/jobs/view/1234567890/?refId=abc")).toBe("1234567890"); + }); + + test("extracts ID from slug URL without trailing slash", () => { + expect(normalizeId("https://www.linkedin.com/jobs/view/software-engineer-1234567890")).toBe("1234567890"); + }); + + test("extracts ID from slug URL with trailing slash", () => { + expect(normalizeId("https://www.linkedin.com/jobs/view/software-engineer-1234567890/")).toBe("1234567890"); + }); + + test("extracts ID from slug URL with trailing slash and tracking query params", () => { + expect( + normalizeId("https://www.linkedin.com/jobs/view/software-engineer-at-company-1234567890/?trackingId=xyz&refId=123"), + ).toBe("1234567890"); + }); + + test("extracts ID from regional subdomain LinkedIn URL with trailing slash", () => { + expect(normalizeId("https://dk.linkedin.com/jobs/view/data-scientist-9876543210/")).toBe("9876543210"); + }); + + test("returns null for non-job URLs and invalid strings", () => { + expect(normalizeId("https://www.linkedin.com/feed/")).toBeNull(); + expect(normalizeId("not-a-url")).toBeNull(); + expect(normalizeId("12345")).toBeNull(); // fewer than 6 digits + expect(normalizeId("")).toBeNull(); + }); +}); + diff --git a/CHANGELOG.md b/CHANGELOG.md index d833be1..c26fedd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,14 @@ per-file diff commands. ### Fixed +- **`linkedin-search detail` accepts LinkedIn job URLs with trailing slashes** (#411) - + passing a job URL with a trailing slash (e.g., `https://www.linkedin.com/jobs/view//` + or a slugged variant with or without query strings) failed validation and exited 1 with + `BAD_ID` before any network request because the regex delimiter strictly expected `?` + or end-of-string immediately after the numeric ID. The boundary check now matches + `[\/?]`, correctly extracting IDs from browser-copied URLs, regional subdomains, and + links with tracking parameters. Pinned by eleven new cases in `parsing.test.ts`. + - **The `documents/interview/**` ignore rule no longer claims interview prep is written there** (#336). `/interview` saves its pack to `documents/applications/_/interview_prep_.md`, already ignored by