diff --git a/packages/loopover-engine/src/review/content-lane/content-repo-spec.ts b/packages/loopover-engine/src/review/content-lane/content-repo-spec.ts index 3775fda10b..a2998f7d96 100644 --- a/packages/loopover-engine/src/review/content-lane/content-repo-spec.ts +++ b/packages/loopover-engine/src/review/content-lane/content-repo-spec.ts @@ -66,6 +66,30 @@ export const AWESOME_CLAUDE_CONTENT_SPEC: ContentRepoSpec = { "submittedByUrl", "sourceSubmissionNumber", "sourceSubmissionUrl", + // snake_case aliases, matching urlFields/sourceUrlFields's pairing convention (#7445, same divergence class as + // #7250): protectedFrontmatterChanges compares before[field]/after[field] by the literal parsed key, so a + // protected field written in the legitimately-accepted snake_case convention (e.g. `download_url`) was invisible + // to this gate — a real protected-close bypass. `author`, `category`, `disclosure`, `slug` are single + // all-lowercase words (camelCase and snake_case are byte-identical), so no separate alias is needed for them. + "affiliate_url", + "author_profile_url", + "claim_status", + "claim_url", + "date_added", + "download_url", + "import_pr_number", + "import_pr_url", + "package_url", + "package_verified", + "pricing_model", + "reviewed_at", + "reviewed_by", + "reviewed_pr_number", + "submitted_at", + "submitted_by", + "submitted_by_url", + "source_submission_number", + "source_submission_url", ]), urlFields: new Set([ "documentationUrl", diff --git a/test/unit/content-lane-duplicates.test.ts b/test/unit/content-lane-duplicates.test.ts index 49b83c73e9..9d63f1846b 100644 --- a/test/unit/content-lane-duplicates.test.ts +++ b/test/unit/content-lane-duplicates.test.ts @@ -96,6 +96,24 @@ describe("protectedFrontmatterChanges", () => { const after = mdx({ title: "T", slug: "a", author: "Alice" }); expect(protectedFrontmatterChanges(before, after)).toEqual([]); }); + + it("flags a changed protected field written in the snake_case alias (e.g. download_url), matching the camelCase behavior (#7445)", () => { + // Before #7445, protectedFrontmatterFields listed only downloadUrl (camelCase), so an entry using the + // legitimately-accepted snake_case key was invisible to this gate even though urlFields already treats + // download_url as an equally-valid alias for duplicate detection. + const before = mdx({ title: "T", slug: "a", download_url: "https://example.com/old.zip" }); + const after = mdx({ title: "T", slug: "a", download_url: "https://example.com/new.zip" }); + expect(protectedFrontmatterChanges(before, after)).toEqual(["download_url"]); + }); + + it("every multi-word camelCase protectedFrontmatterFields member has its snake_case alias present too (#7445, mirrors #7250's urlFields pairing assertion)", () => { + const singleLowercaseWords = new Set(["author", "category", "disclosure", "slug"]); + const toSnakeCase = (field: string): string => field.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); + for (const field of AWESOME_CLAUDE_CONTENT_SPEC.protectedFrontmatterFields) { + if (singleLowercaseWords.has(field) || field.includes("_")) continue; + expect(AWESOME_CLAUDE_CONTENT_SPEC.protectedFrontmatterFields.has(toSnakeCase(field))).toBe(true); + } + }); }); describe("extractContentDuplicateSignals + strict match", () => {