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/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,8 +791,12 @@ export async function fanOutRepoSignalSnapshotJobs(
env: Env,
requestedBy: "schedule" | "api" | "test",
): Promise<void> {
// #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) => {
Expand Down
6 changes: 5 additions & 1 deletion src/queue/signal-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ export async function generateSignalSnapshots(
env: Env,
repoFullName?: string,
): Promise<void> {
// #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(
Expand Down
25 changes: 24 additions & 1 deletion test/unit/queue-trends.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
Expand All @@ -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 {
Expand Down
30 changes: 28 additions & 2 deletions test/unit/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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" });

Expand Down