diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 919ecbd7d0..d6423e618f 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -839,14 +839,17 @@ async function maybePublishPrPublicSurface( authorAssociation: pr.authorAssociation ?? null, minerStatus: "not_checked", }); + let publicSurfaceSkipped = false; if (prelim.skipped) { await auditPrVisibilitySkip(env, repoFullName, pr.number, author, prelim.skipReason ?? "skipped", webhook.deliveryId); - return; + publicSurfaceSkipped = true; } const needsMinerCheckForDetectedComment = - settings.commentMode === "detected_contributors_only" && (settings.publicSurface === "comment_and_label" || settings.publicSurface === "comment_only"); - if (!gateEnabled && prelim.actions.length === 1 && prelim.actions[0] === "none" && !needsMinerCheckForDetectedComment) return; - if (!author) return; + !publicSurfaceSkipped && + settings.commentMode === "detected_contributors_only" && + (settings.publicSurface === "comment_and_label" || settings.publicSurface === "comment_only"); + if (!gateEnabled && (publicSurfaceSkipped || (prelim.actions.length === 1 && prelim.actions[0] === "none" && !needsMinerCheckForDetectedComment))) return; + if (!author && !gateEnabled) return; if (gateEnabled && (pr.state !== "open" || webhook.action === "closed")) { const gateCheckResult = await createOrUpdateSkippedGateCheckRun(env, installationId, repoFullName, advisory, "PR closed before full evaluation."); @@ -863,10 +866,11 @@ async function maybePublishPrPublicSurface( ).catch(() => undefined); return; } - const prelimHasPublicOutput = needsMinerCheckForDetectedComment || prelim.actions.some((action) => action === "comment" || action === "label" || action === "check_run"); + const prelimHasPublicOutput = + !publicSurfaceSkipped && (needsMinerCheckForDetectedComment || prelim.actions.some((action) => action === "comment" || action === "label" || action === "check_run")); let official: Awaited> | null = null; let decision = prelim; - if (prelimHasPublicOutput) { + if (prelimHasPublicOutput && author) { const requireOfficialMiner = settings.publicAudienceMode === "gittensor_only"; official = await getCachedOfficialMinerDetection(env, author, { targetKey: `${repoFullName}#${pr.number}`, @@ -874,11 +878,13 @@ async function maybePublishPrPublicSurface( }); if (requireOfficialMiner && official.status === "unavailable") { await auditPrVisibilitySkip(env, repoFullName, pr.number, author, "miner_detection_unavailable", webhook.deliveryId); - return; + if (!gateEnabled) return; + publicSurfaceSkipped = true; } if (requireOfficialMiner && official.status !== "confirmed") { await auditPrVisibilitySkip(env, repoFullName, pr.number, author, "not_official_gittensor_miner", webhook.deliveryId); - return; + if (!gateEnabled) return; + publicSurfaceSkipped = true; } decision = decidePublicSurface({ settings, @@ -941,7 +947,7 @@ async function maybePublishPrPublicSurface( } if (!prelimHasPublicOutput) return; - if (!official) return; + if (publicSurfaceSkipped || !official || !author) return; const [github] = await Promise.all([fetchPublicContributorProfile(author)]); const contributorPullRequests: Awaited> = []; diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 8778cd586b..28d51b71cb 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -750,6 +750,145 @@ describe("queue processors", () => { expect(calls).toEqual({ minerList: 0, gateChecks: 2 }); }); + it("publishes an enabled gate when bot PR public output is skipped", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", + linkedIssueGateMode: "block", + }); + const calls = { gateChecks: 0, comments: 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") { + calls.minerList += 1; + return Response.json([]); + } + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gatebot123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/issues/53/comments")) { + calls.comments += 1; + return Response.json([]); + } + if (url.includes("/check-runs") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + expect(body).toMatchObject({ status: "in_progress", output: { title: "Gittensory Gate is evaluating" } }); + expect(body.conclusion).toBeUndefined(); + calls.gateChecks += 1; + return Response.json({ id: 910 }, { status: 201 }); + } + if (url.includes("/check-runs/910") && method === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + expect(body).toMatchObject({ status: "completed", conclusion: "failure", output: { title: "Gittensory Gate is blocking merge" } }); + calls.gateChecks += 1; + return Response.json({ id: 910 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-bot-public-skip", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 53, title: "Bot PR", state: "open", user: { login: "automation-bot", type: "Bot" }, head: { sha: "gatebot123" }, labels: [], body: "No issue link." }, + }, + }); + + expect(calls).toEqual({ gateChecks: 2, comments: 0, minerList: 0 }); + const audit = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#53") + .first<{ detail: string }>(); + expect(audit?.detail).toBe("bot_author"); + }); + + it("publishes an enabled gate when Gittensor-only public output is skipped for an unconfirmed miner", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositoryFromGitHub(env, { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, 123); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "all_prs", + publicAudienceMode: "gittensor_only", + publicSurface: "comment_only", + autoLabelEnabled: false, + checkRunMode: "off", + gateCheckMode: "enabled", + linkedIssueGateMode: "block", + }); + const calls = { minerList: 0, gateChecks: 0, comments: 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; + return Response.json([]); + } + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/commits/gateminer123/check-runs")) return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/issues/54/comments")) { + calls.comments += 1; + return Response.json([]); + } + if (url.includes("/check-runs") && method === "POST") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + expect(body).toMatchObject({ status: "in_progress", output: { title: "Gittensory Gate is evaluating" } }); + expect(body.conclusion).toBeUndefined(); + calls.gateChecks += 1; + return Response.json({ id: 920 }, { status: 201 }); + } + if (url.includes("/check-runs/920") && method === "PATCH") { + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string; output?: { title?: string } }; + expect(body).toMatchObject({ status: "completed", conclusion: "failure", output: { title: "Gittensory Gate is blocking merge" } }); + calls.gateChecks += 1; + return Response.json({ id: 920 }); + } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, { + type: "github-webhook", + deliveryId: "gate-unconfirmed-miner-public-skip", + eventName: "pull_request", + payload: { + action: "opened", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { number: 54, title: "Unconfirmed miner PR", state: "open", user: { login: "newbie" }, head: { sha: "gateminer123" }, labels: [], body: "No issue link." }, + }, + }); + + expect(calls).toEqual({ minerList: 1, gateChecks: 2, comments: 0 }); + const audit = await env.DB.prepare("select detail from audit_events where event_type = ? and target_key = ?") + .bind("github_app.pr_visibility_skipped", "JSONbored/gittensory#54") + .first<{ detail: string }>(); + expect(audit?.detail).toBe("not_official_gittensor_miner"); + }); + it("audits opt-in gate check permission failures without blocking webhook processing", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); await persistRegistrySnapshot(