diff --git a/migrations/0012_official_miner_detection_cache.sql b/migrations/0012_official_miner_detection_cache.sql new file mode 100644 index 0000000000..cc0c86c14c --- /dev/null +++ b/migrations/0012_official_miner_detection_cache.sql @@ -0,0 +1 @@ +CREATE TABLE IF NOT EXISTS official_miner_detections (login TEXT PRIMARY KEY NOT NULL, status TEXT NOT NULL CHECK (status IN ('confirmed', 'not_found', 'unavailable')), snapshot_json TEXT NOT NULL DEFAULT '{}', error TEXT, fetched_at TEXT NOT NULL, expires_at TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP); diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 303ad10e83..8de27569a3 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -22,6 +22,7 @@ import { issueQualityReports, issues, githubRateLimitObservations, + officialMinerDetections, pullRequestFiles, pullRequestDetailSyncState, pullRequestReviews, @@ -90,6 +91,7 @@ import type { ScoringModelSnapshotRecord, SignalSnapshotRecord, } from "../types"; +import type { GittensorContributorSnapshot, OfficialGittensorMinerDetection } from "../gittensor/api"; import { jsonString, nowIso, parseJson, repoParts } from "../utils/json"; const MAX_STORED_BODY_CHARS = 4000; @@ -745,6 +747,101 @@ export async function hasRecentAuditEvent(env: Env, actor: string, eventType: st return rows.length > 0; } +export async function getFreshOfficialMinerDetection(env: Env, login: string, now = nowIso()): Promise { + const [row] = await getDb(env.DB).select().from(officialMinerDetections).where(and(eq(officialMinerDetections.login, login.toLowerCase()), gte(officialMinerDetections.expiresAt, now))).limit(1); + return row ? toOfficialMinerDetection(row) : null; +} + +export async function upsertOfficialMinerDetection(env: Env, login: string, detection: OfficialGittensorMinerDetection, ttlMs: number, fetchedAtMs = Date.now()): Promise { + const fetchedAt = new Date(fetchedAtMs).toISOString(); + const cacheableDetection = toCacheableOfficialMinerDetection(detection); + const values = { + login: login.toLowerCase(), status: cacheableDetection.status, + snapshotJson: cacheableDetection.status === "confirmed" ? jsonString(cacheableDetection.snapshot) : "{}", + error: cacheableDetection.status === "unavailable" ? cacheableDetection.error : null, fetchedAt, + expiresAt: new Date(fetchedAtMs + ttlMs).toISOString(), updatedAt: fetchedAt, + }; + await getDb(env.DB).insert(officialMinerDetections).values(values).onConflictDoUpdate({ target: officialMinerDetections.login, set: values }); +} + +function toCacheableOfficialMinerDetection(detection: OfficialGittensorMinerDetection): OfficialGittensorMinerDetection { + return detection.status === "confirmed" ? { status: "confirmed", snapshot: toCacheableGittensorSnapshot(detection.snapshot) } : detection; +} + +function toCacheableGittensorSnapshot(snapshot: Partial): GittensorContributorSnapshot { + return { + source: "gittensor_api", + githubId: String(snapshot.githubId ?? ""), + githubUsername: String(snapshot.githubUsername ?? ""), + uid: optionalNumber(snapshot.uid), + failedReason: typeof snapshot.failedReason === "string" ? snapshot.failedReason : snapshot.failedReason === null ? null : undefined, + evaluatedAt: typeof snapshot.evaluatedAt === "string" ? snapshot.evaluatedAt : undefined, + updatedAt: typeof snapshot.updatedAt === "string" ? snapshot.updatedAt : undefined, + isEligible: Boolean(snapshot.isEligible), + credibility: finiteNumber(snapshot.credibility), + eligibleRepoCount: finiteNumber(snapshot.eligibleRepoCount), + issueDiscoveryScore: finiteNumber(snapshot.issueDiscoveryScore), + issueTokenScore: finiteNumber(snapshot.issueTokenScore), + issueCredibility: finiteNumber(snapshot.issueCredibility), + isIssueEligible: Boolean(snapshot.isIssueEligible), + issueEligibleRepoCount: finiteNumber(snapshot.issueEligibleRepoCount), + alphaPerDay: finiteNumber(snapshot.alphaPerDay), + taoPerDay: finiteNumber(snapshot.taoPerDay), + usdPerDay: finiteNumber(snapshot.usdPerDay), + totals: { + pullRequests: finiteNumber(snapshot.totals?.pullRequests), + mergedPullRequests: finiteNumber(snapshot.totals?.mergedPullRequests), + openPullRequests: finiteNumber(snapshot.totals?.openPullRequests), + closedPullRequests: finiteNumber(snapshot.totals?.closedPullRequests), + openIssues: finiteNumber(snapshot.totals?.openIssues), + closedIssues: finiteNumber(snapshot.totals?.closedIssues), + solvedIssues: finiteNumber(snapshot.totals?.solvedIssues), + validSolvedIssues: finiteNumber(snapshot.totals?.validSolvedIssues), + }, + repositories: Array.isArray(snapshot.repositories) + ? snapshot.repositories.map((repo) => ({ + repoFullName: String(repo.repoFullName ?? ""), + pullRequests: finiteNumber(repo.pullRequests), + mergedPullRequests: finiteNumber(repo.mergedPullRequests), + openPullRequests: finiteNumber(repo.openPullRequests), + closedPullRequests: finiteNumber(repo.closedPullRequests), + openIssues: finiteNumber(repo.openIssues), + closedIssues: finiteNumber(repo.closedIssues), + solvedIssues: finiteNumber(repo.solvedIssues), + validSolvedIssues: finiteNumber(repo.validSolvedIssues), + isEligible: Boolean(repo.isEligible), + isIssueEligible: Boolean(repo.isIssueEligible), + credibility: finiteNumber(repo.credibility), + issueCredibility: finiteNumber(repo.issueCredibility), + totalScore: finiteNumber(repo.totalScore), + baseTotalScore: finiteNumber(repo.baseTotalScore), + })) + : [], + pullRequests: Array.isArray(snapshot.pullRequests) + ? snapshot.pullRequests.map((pr) => ({ + repoFullName: String(pr.repoFullName ?? ""), + number: finiteNumber(pr.number), + title: String(pr.title ?? ""), + state: String(pr.state ?? ""), + mergedAt: typeof pr.mergedAt === "string" ? pr.mergedAt : pr.mergedAt === null ? null : undefined, + label: typeof pr.label === "string" ? pr.label : pr.label === null ? null : undefined, + score: finiteNumber(pr.score), + baseScore: finiteNumber(pr.baseScore), + tokenScore: finiteNumber(pr.tokenScore), + })) + : [], + issueLabels: Array.isArray(snapshot.issueLabels) ? snapshot.issueLabels.filter((label): label is string => typeof label === "string") : [], + }; +} + +function finiteNumber(value: unknown): number { + return typeof value === "number" && Number.isFinite(value) ? value : 0; +} + +function optionalNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + export async function recordAiUsageEvent( env: Env, event: { @@ -2118,6 +2215,16 @@ function toInstallationHealthRecord(row: typeof installationHealth.$inferSelect) }; } +function toOfficialMinerDetection(row: typeof officialMinerDetections.$inferSelect): OfficialGittensorMinerDetection { + if (row.status === "confirmed") { + const snapshot = parseJson | null>(row.snapshotJson, null); + return snapshot?.githubId && snapshot.githubUsername + ? { status: "confirmed", snapshot: toCacheableGittensorSnapshot(snapshot) } + : { status: "unavailable", error: "cached Gittensor miner snapshot is invalid" }; + } + return row.status === "unavailable" ? { status: "unavailable", error: row.error ?? "cached Gittensor API unavailable" } : { status: "not_found" }; +} + function toAuthSessionRecord(row: typeof authSessions.$inferSelect): AuthSessionRecord { return { id: row.id, diff --git a/src/db/schema.ts b/src/db/schema.ts index 6838023967..b70fd7fd51 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -570,6 +570,13 @@ export const contributorScoringProfiles = sqliteTable("contributor_scoring_profi generatedAt: text("generated_at").notNull().default("CURRENT_TIMESTAMP"), }); +export const officialMinerDetections = sqliteTable("official_miner_detections", { + login: text("login").primaryKey(), status: text("status").notNull(), + snapshotJson: text("snapshot_json").notNull().default("{}"), error: text("error"), + fetchedAt: text("fetched_at").notNull(), expiresAt: text("expires_at").notNull(), + updatedAt: text("updated_at").notNull().default("CURRENT_TIMESTAMP"), +}); + export const issueQualityReports = sqliteTable( "issue_quality_reports", { diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 15c0640a2e..801739157f 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -2,6 +2,7 @@ import { countOpenIssues, countOpenPullRequests, getLatestRepoGithubTotalsSnapshot, + getFreshOfficialMinerDetection, getPullRequest, getRepository, getRepositorySettings, @@ -27,6 +28,7 @@ import { persistSignalSnapshot, recordWebhookEvent, replaceCollisionEdges, + upsertOfficialMinerDetection, upsertBurdenForecast, upsertContributorEvidence, upsertContributorScoringProfile, @@ -43,7 +45,7 @@ import { refreshContributorActivity, refreshInstallationHealth, } from "../github/backfill"; -import { contributorRepoStatsFromGittensor, fetchGittensorContributorSnapshot, fetchOfficialGittensorMiner, type GittensorContributorSnapshot } from "../gittensor/api"; +import { contributorRepoStatsFromGittensor, fetchGittensorContributorSnapshot, fetchOfficialGittensorMiner, type GittensorContributorSnapshot, type OfficialGittensorMinerDetection } from "../gittensor/api"; import { createOrUpdateCheckRun, getInstallationId } from "../github/app"; import { createOrUpdateAgentCommandComment, createOrUpdatePrIntelligenceComment } from "../github/comments"; import { @@ -85,6 +87,9 @@ import { decidePublicSurface } from "../signals/settings-preview"; import type { ContributorEvidenceRecord, GitHubWebhookPayload, JobMessage, JsonValue } from "../types"; import { errorMessage } from "../utils/json"; +const OFFICIAL_MINER_DETECTION_TTL_MS = 5 * 60 * 1000; +const OFFICIAL_MINER_DETECTION_UNAVAILABLE_TTL_MS = 60 * 1000; + export async function processJob(env: Env, message: JobMessage): Promise { switch (message.type) { case "refresh-registry": @@ -549,16 +554,12 @@ async function maybePublishPrPublicSurface( } if (!author) return; - const official = await fetchOfficialGittensorMiner(author); + const official = await getCachedOfficialMinerDetection(env, author, { + targetKey: `${repoFullName}#${pr.number}`, + deliveryId: webhook.deliveryId, + }); if (official.status === "unavailable") { - await recordAuditEvent(env, { - eventType: "github_app.miner_detection_unavailable", - actor: author, - targetKey: `${repoFullName}#${pr.number}`, - outcome: "error", - detail: official.error, - metadata: { deliveryId: webhook.deliveryId }, - }); + await auditPrVisibilitySkip(env, repoFullName, pr.number, author, "miner_detection_unavailable", webhook.deliveryId); return; } if (official.status !== "confirmed") { @@ -686,7 +687,9 @@ async function maybeProcessGittensoryMentionCommand(env: Env, deliveryId: string const [repo, cachedPullRequest] = await Promise.all([getRepository(env, repoFullName), getPullRequest(env, repoFullName, issue.number)]); const pullRequestAuthor = cachedPullRequest?.authorLogin ?? issue.user?.login ?? null; - const official = pullRequestAuthor ? await fetchOfficialGittensorMiner(pullRequestAuthor) : undefined; + const official = pullRequestAuthor + ? await getCachedOfficialMinerDetection(env, pullRequestAuthor, { targetKey: `${repoFullName}#${issue.number}`, deliveryId }) + : undefined; const authorization = isAuthorizedCommandActor({ commenterLogin: commenter, commenterAssociation: payload.comment?.author_association ?? issue.author_association, @@ -755,6 +758,28 @@ async function auditPrVisibilitySkip( }); } +async function getCachedOfficialMinerDetection(env: Env, login: string, context: { targetKey: string; deliveryId: string }): Promise { + const cached = await getFreshOfficialMinerDetection(env, login); + if (cached) { + await auditMinerDetectionCache(env, "github_app.miner_detection_cache_hit", login, context, cached.status); + if (cached.status === "unavailable") await auditMinerDetectionUnavailable(env, login, context, cached.error); + return cached; + } + await auditMinerDetectionCache(env, "github_app.miner_detection_cache_miss", login, context, "miss"); + const detection = await fetchOfficialGittensorMiner(login); + await upsertOfficialMinerDetection(env, login, detection, detection.status === "unavailable" ? OFFICIAL_MINER_DETECTION_UNAVAILABLE_TTL_MS : OFFICIAL_MINER_DETECTION_TTL_MS); + if (detection.status === "unavailable") await auditMinerDetectionUnavailable(env, login, context, detection.error); + return detection; +} + +async function auditMinerDetectionUnavailable(env: Env, actor: string, context: { targetKey: string; deliveryId: string }, detail: string): Promise { + await recordAuditEvent(env, { eventType: "github_app.miner_detection_unavailable", actor, targetKey: context.targetKey, outcome: "error", detail, metadata: { deliveryId: context.deliveryId } }); +} + +async function auditMinerDetectionCache(env: Env, eventType: "github_app.miner_detection_cache_hit" | "github_app.miner_detection_cache_miss", actor: string, context: { targetKey: string; deliveryId: string }, detail: string): Promise { + await recordAuditEvent(env, { eventType, actor, targetKey: context.targetKey, outcome: "completed", detail, metadata: { deliveryId: context.deliveryId } }); +} + function officialGittensorContributorDetection( snapshot: GittensorContributorSnapshot, currentPr: Awaited>, diff --git a/test/unit/db-parsers.test.ts b/test/unit/db-parsers.test.ts index 97b721a042..9849f3c2a3 100644 --- a/test/unit/db-parsers.test.ts +++ b/test/unit/db-parsers.test.ts @@ -2,9 +2,11 @@ import { describe, expect, it } from "vitest"; import { getLatestScorePreview, getLatestScoringModelSnapshot, + getFreshOfficialMinerDetection, listPullRequestDetailSyncStates, listRepoSyncSegments, listRepoSyncStates, + upsertOfficialMinerDetection, } from "../../src/db/repositories"; import { createTestEnv } from "../helpers/d1"; @@ -114,4 +116,192 @@ describe("database row parser hardening", () => { await expect(getLatestScorePreview(env, "owner/repo", "target-bad-target")).resolves.toMatchObject({ targetType: "planned_pr" }); await expect(getLatestScorePreview(env, "owner/repo", "missing")).resolves.toBeNull(); }); + + it("fails closed for malformed or incomplete cached official miner detections", async () => { + const env = createTestEnv(); + for (const [login, status, snapshotJson, error] of [ + ["broken", "confirmed", "{}", null], + ["outage", "unavailable", "{}", null], + ]) { + await env.DB.prepare( + `insert into official_miner_detections ( + login, status, snapshot_json, error, fetched_at, expires_at, updated_at + ) values (?, ?, ?, ?, '2026-05-29T00:00:00.000Z', '2099-01-01T00:00:00.000Z', '2026-05-29T00:00:00.000Z')`, + ) + .bind(login, status, snapshotJson, error) + .run(); + } + + await expect(getFreshOfficialMinerDetection(env, "missing")).resolves.toBeNull(); + await expect(getFreshOfficialMinerDetection(env, "broken")).resolves.toEqual({ status: "unavailable", error: "cached Gittensor miner snapshot is invalid" }); + await expect(getFreshOfficialMinerDetection(env, "outage")).resolves.toEqual({ status: "unavailable", error: "cached Gittensor API unavailable" }); + }); + + it("allowlists cached official miner snapshot fields", async () => { + const env = createTestEnv(); + await upsertOfficialMinerDetection( + env, + "oktofeesh1", + { + status: "confirmed", + snapshot: { + source: "gittensor_api", + githubId: "123", + githubUsername: "oktofeesh1", + uid: 7, + hotkey: "must-not-cache", + wallet: "must-not-cache", + coldkey: "must-not-cache", + failedReason: "needs more history", + evaluatedAt: "2026-05-29T00:00:00.000Z", + updatedAt: "2026-05-29T00:01:00.000Z", + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + issueDiscoveryScore: 0, + issueTokenScore: 0, + issueCredibility: 1, + isIssueEligible: false, + issueEligibleRepoCount: 0, + alphaPerDay: 0, + taoPerDay: 0, + usdPerDay: 0, + totals: { + pullRequests: 1, + mergedPullRequests: 1, + openPullRequests: 0, + closedPullRequests: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + }, + repositories: [ + { + repoFullName: "JSONbored/gittensory", + pullRequests: 1, + mergedPullRequests: 1, + openPullRequests: 0, + closedPullRequests: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + isEligible: true, + isIssueEligible: false, + credibility: 1, + issueCredibility: 1, + totalScore: 0, + baseTotalScore: 0, + hotkey: "must-not-cache", + }, + { wallet: "must-not-cache" }, + ], + pullRequests: [ + { + repoFullName: "JSONbored/gittensory", + number: 1, + title: "Fix cache", + state: "open", + mergedAt: "2026-05-29T00:00:00.000Z", + label: "bug", + score: 0, + baseScore: 0, + tokenScore: 0, + wallet: "must-not-cache", + }, + { hotkey: "must-not-cache" }, + ], + issueLabels: ["bug"], + } as never, + }, + 60_000, + Date.parse("2026-05-29T00:00:00.000Z"), + ); + + const raw = await env.DB.prepare("select snapshot_json from official_miner_detections where login = ?").bind("oktofeesh1").first<{ snapshot_json: string }>(); + expect(raw?.snapshot_json).not.toMatch(/hotkey|coldkey|wallet|must-not-cache/i); + const cached = await getFreshOfficialMinerDetection(env, "oktofeesh1", "2026-05-29T00:00:30.000Z"); + expect(JSON.stringify(cached)).not.toMatch(/hotkey|coldkey|wallet|must-not-cache/i); + expect(cached).toMatchObject({ status: "confirmed", snapshot: { githubId: "123", githubUsername: "oktofeesh1", uid: 7 } }); + }); + + it("normalizes sparse cached official miner snapshots without preserving unknown fields", async () => { + const env = createTestEnv(); + await upsertOfficialMinerDetection( + env, + "minimal", + { + status: "confirmed", + snapshot: { + source: "gittensor_api", + githubId: "456", + githubUsername: "minimal", + uid: "not-a-number", + failedReason: null, + evaluatedAt: 123, + updatedAt: 123, + totals: null, + repositories: "not-an-array", + pullRequests: "not-an-array", + issueLabels: ["bug", 7], + wallet: "must-not-cache", + hotkey: "must-not-cache", + } as never, + }, + 60_000, + Date.parse("2026-05-29T00:00:00.000Z"), + ); + + const cached = await getFreshOfficialMinerDetection(env, "minimal", "2026-05-29T00:00:30.000Z"); + expect(JSON.stringify(cached)).not.toMatch(/hotkey|coldkey|wallet|must-not-cache/i); + expect(cached).toMatchObject({ + status: "confirmed", + snapshot: { + githubId: "456", + githubUsername: "minimal", + uid: undefined, + failedReason: null, + evaluatedAt: undefined, + updatedAt: undefined, + totals: { + pullRequests: 0, + mergedPullRequests: 0, + openPullRequests: 0, + closedPullRequests: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + }, + repositories: [], + pullRequests: [], + issueLabels: ["bug"], + }, + }); + }); + + it("drops unknown fields even when cached miner identity fields are missing", async () => { + const env = createTestEnv(); + await upsertOfficialMinerDetection( + env, + "anonymous", + { + status: "confirmed", + snapshot: { + source: "gittensor_api", + wallet: "must-not-cache", + coldkey: "must-not-cache", + hotkey: "must-not-cache", + issueLabels: "not-an-array", + } as never, + }, + 60_000, + Date.parse("2026-05-29T00:00:00.000Z"), + ); + + const raw = await env.DB.prepare("select snapshot_json from official_miner_detections where login = ?").bind("anonymous").first<{ snapshot_json: string }>(); + expect(raw?.snapshot_json).not.toMatch(/hotkey|coldkey|wallet|must-not-cache/i); + expect(JSON.parse(raw?.snapshot_json ?? "{}")).toMatchObject({ githubId: "", githubUsername: "", issueLabels: [] }); + }); }); diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index c62355c69e..7250fa161f 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -766,7 +766,7 @@ describe("queue processors", () => { const method = init?.method ?? "GET"; if (url === "https://api.gittensor.io/miners") { calls.minerList += 1; - return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); + return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", hotkey: "must-not-cache", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); } if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); @@ -914,7 +914,7 @@ describe("queue processors", () => { ]); }); - it("supports label-only public surfaces for confirmed miners", async () => { + it("uses cached confirmed miner detection for label-only public surfaces", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); await persistRegistrySnapshot( env, @@ -932,23 +932,26 @@ describe("queue processors", () => { createMissingLabel: false, checkRunMode: "off", }); - const calls = { comments: 0, labels: 0 }; + const calls = { comments: 0, labels: 0, minerList: 0 }; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); const method = init?.method ?? "GET"; - if (url === "https://api.gittensor.io/miners") return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); + } if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); - if (url.includes("/issues/45/comments")) { + if (url.includes("/comments")) { calls.comments += 1; return Response.json([]); } - if (url.includes("/issues/45/labels") && method === "GET") return Response.json([]); - if (url.includes("/issues/45/labels") && method === "POST") { + if (url.includes("/labels") && method === "GET") return Response.json([]); + if (url.includes("/labels") && method === "POST") { calls.labels += 1; return Response.json([{ name: "gittensor" }]); } @@ -966,8 +969,107 @@ describe("queue processors", () => { pull_request: { number: 45, title: "Miner label-only work", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, }, }); + await processJob(env, { + type: "github-webhook", + deliveryId: "label-only-cached", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + pull_request: { number: 46, title: "Miner label-only follow-up", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }); - expect(calls).toEqual({ comments: 0, labels: 1 }); + expect(calls).toEqual({ comments: 0, labels: 2, minerList: 1 }); + const cacheAudit = await env.DB.prepare("select event_type, detail from audit_events where actor = ? order by created_at") + .bind("oktofeesh1") + .all<{ event_type: string; detail: string | null }>(); + expect(cacheAudit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", detail: "miss" }), + expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", detail: "confirmed" }), + ]), + ); + const cached = await env.DB.prepare("select status from official_miner_detections where login = ?").bind("oktofeesh1").first<{ status: string }>(); + expect(cached?.status).toBe("confirmed"); + const snapshot = await env.DB.prepare("select snapshot_json from official_miner_detections where login = ?").bind("oktofeesh1").first<{ snapshot_json: string }>(); + expect(snapshot?.snapshot_json).not.toContain("must-not-cache"); + }); + + it("keeps GitHub-history-only contributors quiet through not_found cache hits and expiry", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { + number: 9, + title: "Historical merged work", + state: "closed", + merged_at: "2026-05-22T00:00:00.000Z", + user: { login: "newbie" }, + author_association: "NONE", + labels: [{ name: "feature" }], + body: "Previously merged.", + }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_and_label", + autoLabelEnabled: true, + checkRunMode: "off", + }); + const calls = { minerList: 0, publicOutput: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + return Response.json([]); + } + if (url.includes("/access_tokens") || url.includes("/comments") || url.includes("/labels")) { + calls.publicOutput += 1; + return Response.json({}); + } + return new Response("not found", { status: 404 }); + }); + const basePayload = { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + }; + + for (const number of [47, 48]) { + await processJob(env, { + type: "github-webhook", + deliveryId: `not-found-cache-${number}`, + eventName: "pull_request", + payload: { + ...basePayload, + pull_request: { number, title: "Contributor work", state: "open", user: { login: "newbie" }, labels: [], body: "Fixes #1" }, + }, + }); + } + await env.DB.prepare("update official_miner_detections set expires_at = ? where login = ?").bind("2000-01-01T00:00:00.000Z", "newbie").run(); + await processJob(env, { + type: "github-webhook", + deliveryId: "not-found-cache-expired", + eventName: "pull_request", + payload: { + ...basePayload, + pull_request: { number: 49, title: "Contributor follow-up", state: "open", user: { login: "newbie" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(calls).toEqual({ minerList: 2, publicOutput: 0 }); + const audit = await env.DB.prepare("select event_type, detail from audit_events where actor = ? order by created_at") + .bind("newbie") + .all<{ event_type: string; detail: string | null }>(); + expect(audit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", detail: "miss" }), + expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", detail: "not_found" }), + expect.objectContaining({ event_type: "github_app.pr_visibility_skipped", detail: "not_official_gittensor_miner" }), + ]), + ); + const cached = await env.DB.prepare("select status from official_miner_detections where login = ?").bind("newbie").first<{ status: string }>(); + expect(cached?.status).toBe("not_found"); }); it("fails closed when official miner detection is unavailable", async () => { @@ -987,17 +1089,116 @@ describe("queue processors", () => { }, }; - vi.stubGlobal("fetch", async () => new Response("gittensor unavailable", { status: 503 })); + const calls = { minerList: 0 }; + vi.stubGlobal("fetch", async () => { + calls.minerList += 1; + return new Response("gittensor unavailable", { status: 503 }); + }); await expect(processJob(env, { type: "github-webhook", deliveryId: "miner-unavailable", eventName: "pull_request", payload })).resolves.toBeUndefined(); + await expect( + processJob(env, { + type: "github-webhook", + deliveryId: "miner-unavailable-cached", + eventName: "pull_request", + payload: { ...payload, pull_request: { ...payload.pull_request, number: 11 } }, + }), + ).resolves.toBeUndefined(); + expect(calls.minerList).toBe(1); const audit = await env.DB.prepare("select event_type, outcome, detail from audit_events where target_key = ?") .bind("JSONbored/gittensory#10") .all<{ event_type: string; outcome: string; detail: string }>(); expect(audit.results).toEqual( expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", outcome: "completed", detail: "miss" }), + expect.objectContaining({ event_type: "github_app.miner_detection_unavailable", outcome: "error", detail: expect.stringContaining("Gittensor API failed") }), + expect.objectContaining({ event_type: "github_app.pr_visibility_skipped", outcome: "completed", detail: "miner_detection_unavailable" }), + ]), + ); + const cachedAudit = await env.DB.prepare("select event_type, outcome, detail from audit_events where target_key = ?") + .bind("JSONbored/gittensory#11") + .all<{ event_type: string; outcome: string; detail: string }>(); + expect(cachedAudit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", outcome: "completed", detail: "unavailable" }), expect.objectContaining({ event_type: "github_app.miner_detection_unavailable", outcome: "error", detail: expect.stringContaining("Gittensor API failed") }), + expect.objectContaining({ event_type: "github_app.pr_visibility_skipped", outcome: "completed", detail: "miner_detection_unavailable" }), ]), ); + const cached = await env.DB.prepare("select status from official_miner_detections where login = ?").bind("oktofeesh1").first<{ status: string }>(); + expect(cached?.status).toBe("unavailable"); + }); + + it("recovers confirmed miners after the unavailable cache window expires", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicSurface: "label_only", + autoLabelEnabled: true, + createMissingLabel: false, + checkRunMode: "off", + }); + let officialSource: "down" | "confirmed" = "down"; + const calls = { minerList: 0, labels: 0 }; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + calls.minerList += 1; + if (officialSource === "down") return new Response("gittensor unavailable", { status: 503 }); + return Response.json([{ githubUsername: "oktofeesh1", githubId: "123", hotkey: "must-not-cache", totalPrs: 1, totalMergedPrs: 1, isEligible: true, credibility: 1 }]); + } + if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1" }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([]); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/labels") && method === "GET") return Response.json([]); + if (url.includes("/labels") && method === "POST") { + calls.labels += 1; + return Response.json([{ name: "gittensor" }]); + } + return new Response("not found", { status: 404 }); + }); + const basePayload = { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: true, owner: { login: "JSONbored" } }, + }; + + for (const number of [12, 13]) { + await processJob(env, { + type: "github-webhook", + deliveryId: `miner-unavailable-recovery-${number}`, + eventName: "pull_request", + payload: { + ...basePayload, + pull_request: { number, title: "Miner recovery", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }); + } + expect(calls).toEqual({ minerList: 1, labels: 0 }); + await env.DB.prepare("update official_miner_detections set expires_at = ? where login = ?").bind("2000-01-01T00:00:00.000Z", "oktofeesh1").run(); + officialSource = "confirmed"; + + await processJob(env, { + type: "github-webhook", + deliveryId: "miner-unavailable-recovered", + eventName: "pull_request", + payload: { + ...basePayload, + pull_request: { number: 14, title: "Miner recovery confirmed", state: "open", user: { login: "oktofeesh1" }, labels: [], body: "Fixes #1" }, + }, + }); + + expect(calls).toEqual({ minerList: 2, labels: 1 }); + const cached = await env.DB.prepare("select status, snapshot_json from official_miner_detections where login = ?") + .bind("oktofeesh1") + .first<{ status: string; snapshot_json: string }>(); + expect(cached?.status).toBe("confirmed"); + expect(cached?.snapshot_json).not.toMatch(/hotkey|wallet|coldkey|must-not-cache/i); }); it("responds to authorized @gittensory mention commands with one public-safe comment", async () => { @@ -1107,11 +1308,17 @@ describe("queue processors", () => { }, }); - expect(calls).toEqual({ commentsCreated: 4, token: 4, minerList: 4 }); + expect(calls).toEqual({ commentsCreated: 4, token: 4, minerList: 1 }); const audit = await env.DB.prepare("select event_type, detail from audit_events where target_key = ? order by created_at") .bind("JSONbored/gittensory#77") .all<{ event_type: string; detail: string | null }>(); - expect(audit.results).toEqual(expect.arrayContaining([expect.objectContaining({ event_type: "github_app.agent_command_replied" })])); + expect(audit.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ event_type: "github_app.agent_command_replied" }), + expect.objectContaining({ event_type: "github_app.miner_detection_cache_miss", detail: "miss" }), + expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", detail: "confirmed" }), + ]), + ); }); it("skips unauthorized, bot, and non-PR @gittensory mention commands without public output", async () => {