diff --git a/src/api/routes.ts b/src/api/routes.ts index 5b7e930a5c..d6bd5f4ce0 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -4748,11 +4748,18 @@ function buildDigestItems(args: { rateLimits: Awaited>; }) { const items: Array<{ kind: "summary" | "review-now" | "queue" | "drift" | "install"; title: string; detail: string; meta?: string }> = []; + // Lead with `installed` (repos this instance actually operates on) rather than `registered` (gittensor-subnet + // membership, an opt-in plugin -- see gittensor-wire.ts). "0 registered" is the normal, expected headline for + // any operator who hasn't opted into the gittensor plugin and must not read as broken (#5026). + const installed = args.repositories.filter((repo) => repo.isInstalled).length; const registered = args.repositories.filter((repo) => repo.isRegistered).length; items.push({ kind: "summary", - title: `${registered} registered repositories tracked`, - detail: `${args.repositories.length} repositories are present in the local LoopOver data cache.`, + title: `${installed} installed repositories tracked`, + detail: + registered > 0 + ? `${args.repositories.length} repositories are present in the local LoopOver data cache; ${registered} registered with the gittensor plugin.` + : `${args.repositories.length} repositories are present in the local LoopOver data cache.`, meta: "registry", }); const unhealthy = args.health.filter((record) => record.status !== "healthy"); diff --git a/src/services/operator-dashboard.ts b/src/services/operator-dashboard.ts index 6907979add..50c688f145 100644 --- a/src/services/operator-dashboard.ts +++ b/src/services/operator-dashboard.ts @@ -184,7 +184,13 @@ export async function buildOperatorDashboardPayload( metrics: [ { label: "Active sessions", value: String(activeSessions), delta: "browser + CLI/MCP" }, { label: "Installations", value: String(installations.length), delta: `${installedRepos} installed repos` }, - { label: "Registered repos", value: String(registeredRepos), delta: registry ? `${registry.repoCount} in latest registry` : "registry missing" }, + { + label: "Registered repos", + value: String(registeredRepos), + // A null registry is the normal, expected state for any operator who hasn't opted into the + // gittensor plugin (see gittensor-wire.ts) -- "missing" reads as broken when it isn't (#5026). + delta: registry ? `${registry.repoCount} in latest registry` : "gittensor plugin not enabled", + }, { label: "Digest subscriptions", value: String(digestSubscriptions), delta: "store-only" }, { label: "Product events", value: String(usageSummary.totalEvents), delta: `last ${windowDays} days` }, { label: "Active users", value: String(usageSummary.activeActors), delta: `hashed, last ${windowDays} days` }, diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index b33cd2ce27..01689c96c1 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -509,6 +509,72 @@ describe("api routes", () => { expect(legacyPerRepoDrift.status).toBe(404); }); + it("reads registered-repo copy as an honest, non-alarming state whether or not the gittensor plugin is opted into (#5026)", async () => { + const app = createApp(); + + // No registry snapshot ever persisted: the normal, expected state for an operator who hasn't opted + // into the gittensor plugin. "registered: 0" must not read as broken. + const unregisteredEnv = createTestEnv(); + await upsertRepositoryFromGitHub( + unregisteredEnv, + { name: "installed-only", full_name: "acme/installed-only", private: false, owner: { login: "acme" }, default_branch: "main" }, + 321, + ); + const unregisteredOperator = await app.request("/v1/app/operator-dashboard", { headers: apiHeaders(unregisteredEnv) }, unregisteredEnv); + expect(unregisteredOperator.status).toBe(200); + await expect(unregisteredOperator.json()).resolves.toMatchObject({ + metrics: expect.arrayContaining([ + expect.objectContaining({ label: "Registered repos", value: "0", delta: "gittensor plugin not enabled" }), + ]), + }); + const unregisteredDigest = await app.request("/v1/app/digest", { headers: apiHeaders(unregisteredEnv) }, unregisteredEnv); + expect(unregisteredDigest.status).toBe(200); + await expect(unregisteredDigest.json()).resolves.toMatchObject({ + items: expect.arrayContaining([ + expect.objectContaining({ + kind: "summary", + title: "1 installed repositories tracked", + detail: "1 repositories are present in the local LoopOver data cache.", + }), + ]), + }); + + // A registry snapshot IS persisted (gittensor plugin opted in somewhere): both copy surfaces should + // report the registered count honestly instead of the unregistered-branch wording. + const registeredEnv = createTestEnv(); + await upsertRepositoryFromGitHub( + registeredEnv, + { name: "installed-and-registered", full_name: "acme/installed-and-registered", private: false, owner: { login: "acme" }, default_branch: "main" }, + 654, + ); + await persistRegistrySnapshot( + registeredEnv, + normalizeRegistryPayload( + { "acme/installed-and-registered": { emission_share: 0.01, issue_discovery_share: 0, label_multipliers: {}, trusted_label_pipeline: false } }, + { kind: "raw-github", url: "fixture://registered-registry" }, + "2026-06-01T00:00:00.000Z", + ), + ); + const registeredOperator = await app.request("/v1/app/operator-dashboard", { headers: apiHeaders(registeredEnv) }, registeredEnv); + expect(registeredOperator.status).toBe(200); + await expect(registeredOperator.json()).resolves.toMatchObject({ + metrics: expect.arrayContaining([ + expect.objectContaining({ label: "Registered repos", value: "1", delta: "1 in latest registry" }), + ]), + }); + const registeredDigest = await app.request("/v1/app/digest", { headers: apiHeaders(registeredEnv) }, registeredEnv); + expect(registeredDigest.status).toBe(200); + await expect(registeredDigest.json()).resolves.toMatchObject({ + items: expect.arrayContaining([ + expect.objectContaining({ + kind: "summary", + title: "1 installed repositories tracked", + detail: "1 repositories are present in the local LoopOver data cache; 1 registered with the gittensor plugin.", + }), + ]), + }); + }); + it("serves upstream ruleset status, ruleset snapshots, and drift reports through private APIs", async () => { const app = createApp(); const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); @@ -2351,7 +2417,7 @@ describe("api routes", () => { const emptyOperator = await app.request("/v1/app/operator-dashboard", { headers: apiHeaders(emptyEnv) }, emptyEnv); expect(emptyOperator.status).toBe(200); await expect(emptyOperator.json()).resolves.toMatchObject({ - metrics: expect.arrayContaining([expect.objectContaining({ label: "Registered repos", delta: "registry missing" })]), + metrics: expect.arrayContaining([expect.objectContaining({ label: "Registered repos", delta: "gittensor plugin not enabled" })]), recommendationQuality: expect.objectContaining({ empty: true, sparse: false,