Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/signals/contributor-open-pr-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ export type ContributorOpenPrMonitor = {

export async function buildContributorOpenPrMonitor(env: Env, login: string): Promise<ContributorOpenPrMonitor> {
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()),
);
Expand Down
26 changes: 23 additions & 3 deletions test/unit/contributor-open-pr-monitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof repositories.listRepositories>>);
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) =>
Expand All @@ -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);
Expand All @@ -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<ReturnType<typeof repositories.listRepositories>>);
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([
Expand Down