diff --git a/src/review/content-lane/registry-logic.ts b/src/review/content-lane/registry-logic.ts index 921b018b2e..a61c3c4748 100644 --- a/src/review/content-lane/registry-logic.ts +++ b/src/review/content-lane/registry-logic.ts @@ -909,9 +909,11 @@ export type ContentLaneDeliverableCheck = { verdict: "not-applicable" } | { verd /** * Determine whether a PR's changed files deliver the content-lane file its linked issue implies. PURE — no I/O, * no registry-specific hardcoding; entirely driven by `spec` (the caller's already-resolved RegistryLaneSpec) - * and the text inputs. `changedFiles` are matched the SAME way classifyRegistryPrScope matches them - * (canonicalized: lowercased + `./`-stripped + `\`→`/`), so an uppercase / `./`-prefixed / `\`-separated - * changed path is recognized identically to how the rest of the content lane already treats it. + * and the text inputs. `changedFiles` AND issue-body path tokens are matched the SAME way + * classifyRegistryPrScope matches them (canonicalized: lowercased + `./`-stripped + `\`→`/`), so an + * uppercase / `./`-prefixed / `\`-separated path is recognized identically to how the rest of the content + * lane already treats it. The ORIGINAL issue-body token is still returned as `mentionedPath` (it is quoted + * into the public PR comment and must match what the contributor/maintainer see in the issue). * * Two independent, deterministic signals decide whether the issue implies a delivery, checked in this order: * 1. A literal path in the issue BODY matching the spec (the general case — works for any hand-written issue @@ -930,7 +932,9 @@ export function checkContentLaneDeliverable( issueTitle?: string, ): ContentLaneDeliverableCheck { const matchesSpec = (candidate: string): boolean => spec.entryFilePattern.test(candidate) || (spec.providerFilePattern?.test(candidate) ?? false); - const mentionedPath = extractPathTokens(issueText).find(matchesSpec); + // Mirror classifyRegistryPrScope's matchesPattern: canonicalize at the point of test, keep the original + // string for anything user-visible (`mentionedPath` in the public PR comment). + const mentionedPath = extractPathTokens(issueText).find((token) => matchesSpec(canonicalize(token))); const titleImplies = !mentionedPath && Boolean(issueTitle && spec.issueTitleImpliesEntryPattern?.test(issueTitle)); if (!mentionedPath && !titleImplies) return { verdict: "not-applicable" }; const delivered = changedFiles.some((file) => matchesSpec(canonicalize(file))); diff --git a/test/unit/content-lane-registry-logic.test.ts b/test/unit/content-lane-registry-logic.test.ts index f017c0798b..6c5eea07cb 100644 --- a/test/unit/content-lane-registry-logic.test.ts +++ b/test/unit/content-lane-registry-logic.test.ts @@ -669,6 +669,34 @@ describe("checkContentLaneDeliverable (generic, spec-driven — #content-lane-de expect(checkContentLaneDeliverable(spec, issueText, ["registry\\subnets\\foo.json"]).verdict).toBe("delivered"); }); + // #9667: entryFilePattern/providerFilePattern are compiled by globToRegExp against a CANONICALIZED path (no + // `i` flag), so they only match lowercase input. The changedFiles side already canonicalized; the issue-body + // side did not — a mixed-case body token produced no mentionedPath and the gate silently returned + // not-applicable. Canonicalize each token at the point of test (same as classifyRegistryPrScope) while + // `.find` still returns the ORIGINAL token so mentionedPath quotes the issue body verbatim. + it("canonicalizes mixed-case issue-body path tokens while preserving the original mentionedPath (#9667)", () => { + const issueText = "Add the missing surfaces to Registry/Subnets/Foo.json."; + expect(checkContentLaneDeliverable(spec, issueText, ["tests/foo-verify.test.mjs"])).toEqual({ + verdict: "missing", + mentionedPath: "Registry/Subnets/Foo.json", + }); + expect(checkContentLaneDeliverable(spec, issueText, ["registry/subnets/foo.json"])).toEqual({ verdict: "delivered" }); + }); + + it("keeps all-lowercase issue-body path behavior byte-identical (#9667)", () => { + const issueText = "Add the missing surfaces to registry/subnets/foo.json."; + expect(checkContentLaneDeliverable(spec, issueText, ["tests/foo-verify.test.mjs"])).toEqual({ + verdict: "missing", + mentionedPath: "registry/subnets/foo.json", + }); + expect(checkContentLaneDeliverable(spec, issueText, ["registry/subnets/foo.json"])).toEqual({ verdict: "delivered" }); + }); + + it("stays not-applicable when the issue body names a path that matches no spec pattern (#9667)", () => { + const issueText = "Update the docs at Docs/Guides/Setup.md."; + expect(checkContentLaneDeliverable(spec, issueText, ["tests/unrelated.test.mjs"])).toEqual({ verdict: "not-applicable" }); + }); + it("is not-applicable for a spec with no providerFilePattern configured, even if the issue mentions an entry-shaped path (guards the optional-chaining branch)", () => { const entryOnlySpec: RegistryLaneSpec = { entryFilePattern: SUBNET_ENTRY_PATTERN, collectionField: "surfaces" }; const issueText = "Add registry/subnets/foo.json.";