Skip to content

Commit a3ac8cf

Browse files
committed
fix(queue): migrate signal-snapshot generation from isRegistered to isInstalled
fanOutRepoSignalSnapshotJobs gated per-repo signal-snapshot generation on repo.isRegistered, even though most of what it generates (queue- health, config-quality, label-audit, contributor-intake-health, issue-quality, repo-outcome-patterns) is general repo health, unrelated to subnet membership. The maintainer-lane/maintainer-cut-readiness pieces are gittensor-specific but already degrade gracefully for !isRegistered internally. generateSignalSnapshots (the function the enqueued job actually calls) independently re-filters by the same field -- both needed the swap, or a job enqueued for an installed-but-not-registered repo would reach this second filter and silently no-op. Closes #5019
1 parent 21d13b2 commit a3ac8cf

4 files changed

Lines changed: 62 additions & 5 deletions

File tree

src/queue/processors.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -791,8 +791,12 @@ export async function fanOutRepoSignalSnapshotJobs(
791791
env: Env,
792792
requestedBy: "schedule" | "api" | "test",
793793
): Promise<void> {
794+
// #5019: most of what generateSignalSnapshots produces (queue-health, config-quality, label-audit,
795+
// contributor-intake-health, issue-quality, repo-outcome-patterns) is generic repo health, unrelated to
796+
// gittensor-subnet membership. The gittensor-specific pieces (maintainer-lane/maintainer-cut-readiness)
797+
// already degrade gracefully for !isRegistered internally, so no other change is needed here.
794798
const repositories = (await listRepositories(env)).filter(
795-
(repo) => repo.isRegistered,
799+
(repo) => repo.isInstalled,
796800
);
797801
await Promise.all(
798802
repositories.map((repo, index) => {

src/queue/signal-snapshot.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,13 @@ export async function generateSignalSnapshots(
5454
env: Env,
5555
repoFullName?: string,
5656
): Promise<void> {
57+
// #5019: this is the function the enqueued generate-signal-snapshots job actually calls, and it
58+
// independently re-filters by the same field fanOutRepoSignalSnapshotJobs already checked -- both
59+
// filters must move to isInstalled together, or a job enqueued for an installed-but-not-registered
60+
// repo would reach here and silently no-op (repositories would come back empty).
5761
const repositories = (await listRepositories(env)).filter(
5862
(repo) =>
59-
repo.isRegistered && (!repoFullName || repo.fullName === repoFullName),
63+
repo.isInstalled && (!repoFullName || repo.fullName === repoFullName),
6064
);
6165
for (const repo of repositories) {
6266
const trendSince = new Date(

test/unit/queue-trends.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ describe("queue trend windows", () => {
171171

172172
it("persists a compact trend snapshot during signal generation", async () => {
173173
const env = createTestEnv();
174-
await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" }, default_branch: "main" });
174+
// generateSignalSnapshots now gates on isInstalled, not isRegistered (#5019).
175+
await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "owner/repo", private: false, owner: { login: "owner" }, default_branch: "main" }, 601);
175176
await env.DB.prepare("update repositories set is_registered = 1 where full_name = ?").bind("owner/repo").run();
176177
await persistRepoGithubTotalsSnapshot(env, totals(30, { openIssues: 10, openPrs: 2, merged: 5, closed: 1 }));
177178
await persistRepoGithubTotalsSnapshot(env, totals(0, { openIssues: 16, openPrs: 8, merged: 9, closed: 3 }));
@@ -197,6 +198,28 @@ describe("queue trend windows", () => {
197198
windows: expect.arrayContaining([expect.objectContaining({ windowDays: 30, status: "ready", pullRequestGrowth: 6 })]),
198199
});
199200
});
201+
202+
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 () => {
203+
const env = createTestEnv();
204+
await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "acme/installed-only", private: false, owner: { login: "acme" }, default_branch: "main" }, 602);
205+
206+
await generateSignalSnapshots(env, "acme/installed-only");
207+
208+
// A repo this instance never processed would have no snapshot row at all; getting a real (non-null)
209+
// snapshot back proves the inner isInstalled filter actually let this repo through, not just the
210+
// outer fan-out filter fixed by the same issue.
211+
await expect(getRepoQueueTrendSnapshot(env, "acme/installed-only")).resolves.not.toBeNull();
212+
});
213+
214+
it("#5019: does not generate a snapshot for a registered-but-not-installed repo", async () => {
215+
const env = createTestEnv();
216+
await upsertRepositoryFromGitHub(env, { name: "repo", full_name: "acme/registered-only", private: false, owner: { login: "acme" } });
217+
await env.DB.prepare("update repositories set is_registered = 1 where full_name = ?").bind("acme/registered-only").run();
218+
219+
await generateSignalSnapshots(env, "acme/registered-only");
220+
221+
await expect(getRepoQueueTrendSnapshot(env, "acme/registered-only")).resolves.toBeNull();
222+
});
200223
});
201224

202225
function totals(daysAgo: number, values: { openIssues: number; openPrs: number; merged: number; closed: number }): RepoGithubTotalsSnapshotRecord {

test/unit/queue.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import {
5959
listReviewSuppressions,
6060
setGlobalAgentFrozen,
6161
} from "../../src/db/repositories";
62-
import { agentMaintenanceHeadMatchesGate, buildBurdenForecasts, changedPathsForGuardrail, claimAiReviewLock, claimPrActuationLock, contributorEvidenceBatchSize, enrichOpenPullRequestsWithChangedFiles, processJob, reconcileLiveDuplicateSiblings, releaseAiReviewLock, releasePrActuationLock, reviewDurationMsSince, SWEEP_FANOUT_RESOLUTION_CONCURRENCY } from "../../src/queue/processors";
62+
import { agentMaintenanceHeadMatchesGate, buildBurdenForecasts, changedPathsForGuardrail, claimAiReviewLock, claimPrActuationLock, contributorEvidenceBatchSize, enrichOpenPullRequestsWithChangedFiles, fanOutRepoSignalSnapshotJobs, processJob, reconcileLiveDuplicateSiblings, releaseAiReviewLock, releasePrActuationLock, reviewDurationMsSince, SWEEP_FANOUT_RESOLUTION_CONCURRENCY } from "../../src/queue/processors";
6363
import type { PullRequestRecord } from "../../src/types";
6464
import { aiReviewCacheInputFingerprint } from "../../src/review/ai-review-cache-input";
6565
import { fingerprint as reviewMemoryFingerprint } from "../../src/review/review-memory-match";
@@ -483,6 +483,29 @@ describe("queue processors", () => {
483483
await expect(getBurdenForecast(env, "acme/registered-not-installed")).resolves.toBeNull();
484484
});
485485

486+
it("fans out signal-snapshot jobs by isInstalled, not isRegistered (#5019 regression)", async () => {
487+
const sent: import("../../src/types").JobMessage[] = [];
488+
const env = createTestEnv({
489+
JOBS: {
490+
async send(message: import("../../src/types").JobMessage) {
491+
sent.push(message);
492+
},
493+
} as unknown as Queue,
494+
});
495+
vi.spyOn(repositoriesModule, "listRepositories").mockResolvedValue([
496+
// Installed but not gittensor-subnet-registered: signal-snapshot generation is generic repo-health
497+
// tracking (queue-health, config-quality, label-audit, contributor-intake-health, issue-quality,
498+
// repo-outcome-patterns), unrelated to subnet economics, so this repo MUST still be covered.
499+
{ fullName: "acme/installed-not-registered", owner: "acme", name: "installed-not-registered", isInstalled: true, isRegistered: false, isPrivate: false },
500+
// Subnet-registered but not installed on this instance: this repo must NOT be covered.
501+
{ fullName: "acme/registered-not-installed", owner: "acme", name: "registered-not-installed", isInstalled: false, isRegistered: true, isPrivate: false },
502+
]);
503+
504+
await fanOutRepoSignalSnapshotJobs(env, "test");
505+
506+
expect(sent).toEqual([expect.objectContaining({ type: "generate-signal-snapshots", repoFullName: "acme/installed-not-registered" })]);
507+
});
508+
486509
it("runs queued agent jobs through the queue processor", async () => {
487510
const queued: unknown[] = [];
488511
const env = createTestEnv({
@@ -893,9 +916,12 @@ describe("queue processors", () => {
893916
"we-promise/sure": { emission_share: 0.02, issue_discovery_share: 0, label_multipliers: {}, trusted_label_pipeline: false },
894917
},
895918
{ kind: "raw-github", url: "fixture://registry" },
896-
"2026-05-25T00:00:00.000Z",
919+
"2026-05-23T00:00:00.000Z",
897920
),
898921
);
922+
// fanOutRepoSignalSnapshotJobs now gates on isInstalled, not isRegistered (#5019).
923+
await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, 9403);
924+
await upsertRepositoryFromGitHub(env, { name: "sure", full_name: "we-promise/sure", private: true, owner: { login: "we-promise" } }, 9404);
899925

900926
await processJob(env, { type: "generate-signal-snapshots", requestedBy: "schedule" });
901927

0 commit comments

Comments
 (0)