diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 780540134c..80056c9a35 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -791,8 +791,12 @@ export async function fanOutRepoSignalSnapshotJobs( env: Env, requestedBy: "schedule" | "api" | "test", ): Promise { + // #5019: most of what generateSignalSnapshots produces (queue-health, config-quality, label-audit, + // contributor-intake-health, issue-quality, repo-outcome-patterns) is generic repo health, unrelated to + // gittensor-subnet membership. The gittensor-specific pieces (maintainer-lane/maintainer-cut-readiness) + // already degrade gracefully for !isRegistered internally, so no other change is needed here. const repositories = (await listRepositories(env)).filter( - (repo) => repo.isRegistered, + (repo) => repo.isInstalled, ); await Promise.all( repositories.map((repo, index) => { diff --git a/src/queue/signal-snapshot.ts b/src/queue/signal-snapshot.ts index 38787f0695..1a9c30ec4c 100644 --- a/src/queue/signal-snapshot.ts +++ b/src/queue/signal-snapshot.ts @@ -54,9 +54,13 @@ export async function generateSignalSnapshots( env: Env, repoFullName?: string, ): Promise { + // #5019: this is the function the enqueued generate-signal-snapshots job actually calls, and it + // independently re-filters by the same field fanOutRepoSignalSnapshotJobs already checked -- both + // filters must move to isInstalled together, or a job enqueued for an installed-but-not-registered + // repo would reach here and silently no-op (repositories would come back empty). const repositories = (await listRepositories(env)).filter( (repo) => - repo.isRegistered && (!repoFullName || repo.fullName === repoFullName), + repo.isInstalled && (!repoFullName || repo.fullName === repoFullName), ); for (const repo of repositories) { const trendSince = new Date( diff --git a/test/unit/queue-trends.test.ts b/test/unit/queue-trends.test.ts index b48e8d9092..4c678054f5 100644 --- a/test/unit/queue-trends.test.ts +++ b/test/unit/queue-trends.test.ts @@ -171,7 +171,8 @@ describe("queue trend windows", () => { it("persists a compact trend snapshot during signal generation", async () => { const env = createTestEnv(); - await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" }, default_branch: "main" }); + // generateSignalSnapshots now gates on isInstalled, not isRegistered (#5019). + await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" }, default_branch: "main" }, 601); await env.DB.prepare("update repositories set is_registered = 1 where full_name = ?").bind("owner/repo").run(); await persistRepoGithubTotalsSnapshot(env, totals(30, { openIssues: 10, openPrs: 2, merged: 5, closed: 1 })); await persistRepoGithubTotalsSnapshot(env, totals(0, { openIssues: 16, openPrs: 8, merged: 9, closed: 3 })); @@ -197,6 +198,28 @@ describe("queue trend windows", () => { windows: expect.arrayContaining([expect.objectContaining({ windowDays: 30, status: "ready", pullRequestGrowth: 6 })]), }); }); + + it("#5019: still generates a snapshot for an installed-but-not-registered repo (the enqueued job's own re-filter must not silently no-op)", async () => { + const env = createTestEnv(); + await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "acme/installed-only", private: false, owner: { login: "acme" }, default_branch: "main" }, 602); + + await generateSignalSnapshots(env, "acme/installed-only"); + + // A repo this instance never processed would have no snapshot row at all; getting a real (non-null) + // snapshot back proves the inner isInstalled filter actually let this repo through, not just the + // outer fan-out filter fixed by the same issue. + await expect(getRepoQueueTrendSnapshot(env, "acme/installed-only")).resolves.not.toBeNull(); + }); + + it("#5019: does not generate a snapshot for a registered-but-not-installed repo", async () => { + const env = createTestEnv(); + await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "acme/registered-only", private: false, owner: { login: "acme" } }); + await env.DB.prepare("update repositories set is_registered = 1 where full_name = ?").bind("acme/registered-only").run(); + + await generateSignalSnapshots(env, "acme/registered-only"); + + await expect(getRepoQueueTrendSnapshot(env, "acme/registered-only")).resolves.toBeNull(); + }); }); function totals(daysAgo: number, values: { openIssues: number; openPrs: number; merged: number; closed: number }): RepoGithubTotalsSnapshotRecord { diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index e8e9ac5ed8..20ac33ef81 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -59,7 +59,7 @@ import { listReviewSuppressions, setGlobalAgentFrozen, } from "../../src/db/repositories"; -import { agentMaintenanceHeadMatchesGate, buildBurdenForecasts, changedPathsForGuardrail, claimAiReviewLock, claimPrActuationLock, contributorEvidenceBatchSize, enrichOpenPullRequestsWithChangedFiles, processJob, reconcileLiveDuplicateSiblings, releaseAiReviewLock, releasePrActuationLock, reviewDurationMsSince, SWEEP_FANOUT_RESOLUTION_CONCURRENCY } from "../../src/queue/processors"; +import { agentMaintenanceHeadMatchesGate, buildBurdenForecasts, changedPathsForGuardrail, claimAiReviewLock, claimPrActuationLock, contributorEvidenceBatchSize, enrichOpenPullRequestsWithChangedFiles, fanOutRepoSignalSnapshotJobs, processJob, reconcileLiveDuplicateSiblings, releaseAiReviewLock, releasePrActuationLock, reviewDurationMsSince, SWEEP_FANOUT_RESOLUTION_CONCURRENCY } from "../../src/queue/processors"; import type { PullRequestRecord } from "../../src/types"; import { aiReviewCacheInputFingerprint } from "../../src/review/ai-review-cache-input"; import { fingerprint as reviewMemoryFingerprint } from "../../src/review/review-memory-match"; @@ -483,6 +483,29 @@ describe("queue processors", () => { await expect(getBurdenForecast(env, "acme/registered-not-installed")).resolves.toBeNull(); }); + it("fans out signal-snapshot jobs by isInstalled, not isRegistered (#5019 regression)", async () => { + const sent: import("../../src/types").JobMessage[] = []; + const env = createTestEnv({ + JOBS: { + async send(message: import("../../src/types").JobMessage) { + sent.push(message); + }, + } as unknown as Queue, + }); + vi.spyOn(repositoriesModule, "listRepositories").mockResolvedValue([ + // Installed but not gittensor-subnet-registered: signal-snapshot generation is generic repo-health + // tracking (queue-health, config-quality, label-audit, contributor-intake-health, issue-quality, + // repo-outcome-patterns), unrelated to subnet economics, so this repo MUST still be covered. + { fullName: "acme/installed-not-registered", owner: "acme", name: "installed-not-registered", isInstalled: true, isRegistered: false, isPrivate: false }, + // Subnet-registered but not installed on this instance: this repo must NOT be covered. + { fullName: "acme/registered-not-installed", owner: "acme", name: "registered-not-installed", isInstalled: false, isRegistered: true, isPrivate: false }, + ]); + + await fanOutRepoSignalSnapshotJobs(env, "test"); + + expect(sent).toEqual([expect.objectContaining({ type: "generate-signal-snapshots", repoFullName: "acme/installed-not-registered" })]); + }); + it("runs queued agent jobs through the queue processor", async () => { const queued: unknown[] = []; const env = createTestEnv({ @@ -893,9 +916,12 @@ describe("queue processors", () => { "we-promise/sure": { emission_share: 0.02, issue_discovery_share: 0, label_multipliers: {}, trusted_label_pipeline: false }, }, { kind: "raw-github", url: "fixture://registry" }, - "2026-05-25T00:00:00.000Z", + "2026-05-23T00:00:00.000Z", ), ); + // fanOutRepoSignalSnapshotJobs now gates on isInstalled, not isRegistered (#5019). + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, 9403); + await upsertRepositoryFromGitHub(env, { name: "sure", full_name: "we-promise/sure", private: true, owner: { login: "we-promise" } }, 9404); await processJob(env, { type: "generate-signal-snapshots", requestedBy: "schedule" });