Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/review/content-lane/registry-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)));
Expand Down
28 changes: 28 additions & 0 deletions test/unit/content-lane-registry-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down
Loading