From 05eeb1020d501a2ee393aa5fdc59bcf5e50ba058 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:14:39 -0700 Subject: [PATCH] fix(signals): scope contributor-open-pr-monitor to isInstalled, not isRegistered buildContributorOpenPrMonitor scoped a contributor's open-PR "next steps" packet to isRegistered repos. Every field it produces (classification, reasons, next steps, guidance) is generic PR-hygiene advice with zero gittensor-specific data (no reward-risk, no decision-pack fields), so per the product decision on #5025, it's now available for any self-host operator's installed repos regardless of gittensor-subnet opt-in status -- consistent with #5021/#5022/#5024's isRegistered->isInstalled fixes. Since no gittensor-specific fields exist on this endpoint today, no field-trimming was needed: gittensor stays a pure additive plugin here by not having added anything to this surface in the first place. Closes #5025 --- src/signals/contributor-open-pr-monitor.ts | 6 ++++- test/unit/contributor-open-pr-monitor.test.ts | 26 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/signals/contributor-open-pr-monitor.ts b/src/signals/contributor-open-pr-monitor.ts index b32d6475b4..051a5e8f39 100644 --- a/src/signals/contributor-open-pr-monitor.ts +++ b/src/signals/contributor-open-pr-monitor.ts @@ -52,7 +52,11 @@ export type ContributorOpenPrMonitor = { export async function buildContributorOpenPrMonitor(env: Env, login: string): Promise { const [pullRequests, repositories] = await Promise.all([listContributorPullRequests(env, login), listRepositories(env)]); - const registered = new Set(repositories.filter((repo) => repo.isRegistered).map((repo) => repo.fullName.toLowerCase())); + // #5025: scoped to isInstalled, not isRegistered -- every field this monitor produces (classification, + // reasons, next steps) is generic PR-hygiene guidance with no gittensor-specific data (no reward-risk, no + // decision-pack fields), so it's available to any self-host operator's installed repos regardless of + // gittensor-subnet opt-in, consistent with #5021/#5022/#5024's isRegistered->isInstalled migration. + const registered = new Set(repositories.filter((repo) => repo.isInstalled).map((repo) => repo.fullName.toLowerCase())); const openByContributor = pullRequests.filter( (pr) => pr.state === "open" && sameLogin(pr.authorLogin, login) && registered.has(pr.repoFullName.toLowerCase()), ); diff --git a/test/unit/contributor-open-pr-monitor.test.ts b/test/unit/contributor-open-pr-monitor.test.ts index fcd27c28d3..88af97621f 100644 --- a/test/unit/contributor-open-pr-monitor.test.ts +++ b/test/unit/contributor-open-pr-monitor.test.ts @@ -154,15 +154,15 @@ describe("contributor open PR monitor", () => { expect(mapPendingClassToWorkClassification(nativeDraft, { changeRequestCount: 0, checkFailureCount: 0, duplicateProne: false, missingTests: false })).toBe("draft"); }); - it("builds contributor-wide monitor answer from registered repos only", async () => { + it("builds contributor-wide monitor answer from installed repos only", async () => { const env = createTestEnv(); vi.spyOn(repositories, "listRepositories").mockResolvedValue([ { fullName: "entrius/allways-ui", owner: "entrius", name: "allways-ui", isInstalled: true, isRegistered: true, isPrivate: false }, - { fullName: "other/unregistered", owner: "other", name: "unregistered", isInstalled: true, isRegistered: false, isPrivate: true }, + { fullName: "other/uninstalled", owner: "other", name: "uninstalled", isInstalled: false, isRegistered: true, isPrivate: true }, ] as Awaited>); vi.spyOn(repositories, "listContributorPullRequests").mockResolvedValue([ pr({ number: 10 }), - pr({ number: 11, repoFullName: "other/unregistered", authorLogin: "miner-a" }), + pr({ number: 11, repoFullName: "other/uninstalled", authorLogin: "miner-a" }), ]); vi.spyOn(repositories, "listPullRequests").mockResolvedValue([pr({ number: 10 }), pr({ number: 11 })]); vi.spyOn(repositories, "listPullRequestReviews").mockImplementation(async (_env, _repo, pullNumber) => @@ -174,6 +174,8 @@ describe("contributor open PR monitor", () => { { repoFullName: "entrius/allways-ui", pullNumber: 10, path: "src/a.test.ts", additions: 5, deletions: 0, changes: 5, status: "added", payload: {} }, ]); + // #other/uninstalled is registered on the gittensor subnet but never installed on this self-host + // instance -- it must NOT be covered, since the monitor is scoped to repos this instance operates on. const monitor = await buildContributorOpenPrMonitor(env, "miner-a"); expect(monitor.openPrCount).toBe(1); expect(monitor.registeredRepoCount).toBe(1); @@ -184,6 +186,24 @@ describe("contributor open PR monitor", () => { expect(monitor.guidance.length).toBeGreaterThan(0); }); + it("#5025: covers an installed-but-not-subnet-registered repo, since the monitor's guidance is generic and unrelated to gittensor-subnet economics", async () => { + const env = createTestEnv(); + vi.spyOn(repositories, "listRepositories").mockResolvedValue([ + { fullName: "acme/installed-not-registered", owner: "acme", name: "installed-not-registered", isInstalled: true, isRegistered: false, isPrivate: false }, + ] as Awaited>); + vi.spyOn(repositories, "listContributorPullRequests").mockResolvedValue([pr({ number: 20, repoFullName: "acme/installed-not-registered", authorLogin: "miner-a" })]); + vi.spyOn(repositories, "listPullRequests").mockResolvedValue([pr({ number: 20, repoFullName: "acme/installed-not-registered" })]); + vi.spyOn(repositories, "listPullRequestReviews").mockResolvedValue([approvedReview(20)]); + vi.spyOn(repositories, "listCheckSummaries").mockResolvedValue([]); + vi.spyOn(repositories, "listPullRequestFiles").mockResolvedValue([]); + + const monitor = await buildContributorOpenPrMonitor(env, "miner-a"); + + expect(monitor.openPrCount).toBe(1); + expect(monitor.pullRequests).toHaveLength(1); + expect(monitor.pullRequests[0]).toMatchObject({ number: 20, repoFullName: "acme/installed-not-registered" }); + }); + it("loads signals and files with each PR casing in a case-variant repo group", async () => { const env = createTestEnv(); vi.spyOn(repositories, "listRepositories").mockResolvedValue([