From eb399999ceefd112540f1ca2b5f3fdc04e677a42 Mon Sep 17 00:00:00 2001 From: bitfathers94 <237535319+bitfathers94@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:20:14 +0000 Subject: [PATCH 1/2] fix(orb): compute miner status for the six confirmed_miner-widenable @loopover PR commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gate-override, resolve, pause, resume, explain, and generate-tests each call authorizePrActionActor without needsMinerDetection: true, so the official miner-status field stays undefined and evaluateCommandAuthorization always falls into miner_detection_unavailable whenever a confirmed_miner role match would otherwise apply. That made the shipped commandAuthorization policy's documented confirmed_miner widening a dead feature for these commands: a repo widening any of them to confirmed_miner could never actually authorize a confirmed miner on their own PR. Pass needsMinerDetection: true at all six call sites (both generate-tests handlers, including the PR-panel one that previously passed an explicit false), mirroring the already-correct review command. No other authorization behavior changes: the miner lookup only fires when confirmed_miner is in the resolved allowed-roles and the commenter is the PR author, so maintainer/collaborator and non-widened paths are untouched. Adds regression tests exercising the previously-dead widening path directly: a confirmed miner pausing their own PR, and a confirmed miner dispatching generate-tests on their own PR — both denied with miner_detection_unavailable before this fix. Closes #8685 --- src/queue/processors.ts | 13 +++++------ test/unit/queue-3.test.ts | 32 ++++++++++++++++++++++++++++ test/unit/queue-5.test.ts | 45 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index f4623c4de7..3a64b8acfe 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -11757,6 +11757,7 @@ async function maybeProcessGateOverrideCommand( commandName: "gate-override" as LoopOverMentionCommandName, settings, pr, + needsMinerDetection: true, }); if (!authorization.authorized) { await recordAuditEvent(env, { @@ -11952,7 +11953,7 @@ async function maybeProcessResolveCommand(env: Env, deliveryId: string, payload: const [pr, settings] = await Promise.all([getPullRequest(env, req.repoFullName, req.pr.number), resolveRepositorySettings(env, req.repoFullName)]); const targetKey = `${req.repoFullName}#${req.pr.number}`; if (!pr) { await recordAuditEvent(env, { eventType: "github_app.finding_resolved_skipped", actor: req.actor, targetKey, outcome: "completed", detail: "cached_pr_missing", metadata: { deliveryId, repoFullName: req.repoFullName, reason: "cached_pr_missing" } }); await recordGithubProductUsage(env, "finding_resolved_skipped", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "skipped", metadata: { reason: "cached_pr_missing" } }); return true; } - const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "resolve" as LoopOverMentionCommandName, settings, pr }); + const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "resolve" as LoopOverMentionCommandName, settings, pr, needsMinerDetection: true }); if (!authorization.authorized) { await recordAuditEvent(env, { eventType: "github_app.finding_resolved_denied", actor: req.actor, targetKey, outcome: "denied", detail: authorization.reason, metadata: { deliveryId, repoFullName: req.repoFullName, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "resolve") } }); await recordGithubProductUsage(env, "finding_resolved_denied", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "denied", metadata: { reason: authorization.reason, actorKind: authorization.actorKind, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "resolve") } }); return true; } const findingRef = normalizeResolveFindingRef(command.reason); if (!findingRef.ok) { await recordAuditEvent(env, { eventType: "github_app.finding_resolved_skipped", actor: req.actor, targetKey, outcome: "completed", detail: findingRef.reason, metadata: { deliveryId, repoFullName: req.repoFullName, reason: findingRef.reason } }); await recordGithubProductUsage(env, "finding_resolved_skipped", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "skipped", metadata: { reason: findingRef.reason } }); return true; } @@ -12061,7 +12062,7 @@ async function maybeProcessPauseCommand(env: Env, deliveryId: string, payload: G await recordAutoreviewPausedSkip(env, deliveryId, req.repoFullName, targetKey, req.actor, "cached_pr_missing"); return true; } - const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "pause" as LoopOverMentionCommandName, settings, pr }); + const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "pause" as LoopOverMentionCommandName, settings, pr, needsMinerDetection: true }); if (!authorization.authorized) { await recordAuditEvent(env, { eventType: "github_app.autoreview_paused_denied", actor: req.actor, targetKey, outcome: "denied", detail: authorization.reason, metadata: { deliveryId, repoFullName: req.repoFullName, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "pause") } }); await recordGithubProductUsage(env, "autoreview_paused_denied", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "denied", metadata: { reason: authorization.reason, actorKind: authorization.actorKind, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "pause") } }); @@ -12104,7 +12105,7 @@ async function maybeProcessResumeCommand(env: Env, deliveryId: string, payload: await recordAutoreviewResumedSkip(env, deliveryId, req.repoFullName, targetKey, req.actor, "cached_pr_missing"); return true; } - const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "resume" as LoopOverMentionCommandName, settings, pr }); + const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "resume" as LoopOverMentionCommandName, settings, pr, needsMinerDetection: true }); if (!authorization.authorized) { await recordAuditEvent(env, { eventType: "github_app.autoreview_resumed_denied", actor: req.actor, targetKey, outcome: "denied", detail: authorization.reason, metadata: { deliveryId, repoFullName: req.repoFullName, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "resume") } }); await recordGithubProductUsage(env, "autoreview_resumed_denied", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "denied", metadata: { reason: authorization.reason, actorKind: authorization.actorKind, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "resume") } }); @@ -12171,7 +12172,7 @@ async function maybeProcessExplainCommand(env: Env, deliveryId: string, payload: await recordFindingExplainedSkip(env, deliveryId, req.repoFullName, targetKey, req.actor, "cached_pr_missing"); return true; } - const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "explain" as LoopOverMentionCommandName, settings, pr }); + const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "explain" as LoopOverMentionCommandName, settings, pr, needsMinerDetection: true }); if (!authorization.authorized) { await recordAuditEvent(env, { eventType: "github_app.finding_explained_denied", actor: req.actor, targetKey, outcome: "denied", detail: authorization.reason, metadata: { deliveryId, repoFullName: req.repoFullName, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "explain") } }); await recordGithubProductUsage(env, "finding_explained_denied", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "denied", metadata: { reason: authorization.reason, actorKind: authorization.actorKind, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "explain") } }); @@ -12252,7 +12253,7 @@ async function maybeProcessGenerateTestsCommand(env: Env, deliveryId: string, pa await recordGenerateTestsSkip(env, deliveryId, req.repoFullName, targetKey, req.actor, "cached_pr_missing"); return true; } - const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "generate-tests" as LoopOverMentionCommandName, settings, pr }); + const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "generate-tests" as LoopOverMentionCommandName, settings, pr, needsMinerDetection: true }); if (!authorization.authorized) { await recordAuditEvent(env, { eventType: "github_app.e2e_tests_generation_denied", actor: req.actor, targetKey, outcome: "denied", detail: authorization.reason, metadata: { deliveryId, repoFullName: req.repoFullName, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "generate-tests") } }); await recordGithubProductUsage(env, "e2e_tests_generation_denied", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "denied", metadata: { reason: authorization.reason, actorKind: authorization.actorKind, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "generate-tests") } }); @@ -12973,7 +12974,7 @@ async function maybeProcessPrPanelGenerateTests( commandName: "generate-tests" as LoopOverMentionCommandName, settings, pr, - needsMinerDetection: false, + needsMinerDetection: true, }); if (!authorization.authorized) { await recordAuditEvent(env, { diff --git a/test/unit/queue-3.test.ts b/test/unit/queue-3.test.ts index 759c4760b5..4a6c9b8c70 100644 --- a/test/unit/queue-3.test.ts +++ b/test/unit/queue-3.test.ts @@ -823,6 +823,38 @@ describe("queue processors", () => { expect(paused).toBeFalsy(); }); + // REGRESSION (#8685): pause is a maintainer/collaborator-only default, but a repo may widen it to + // confirmed_miner via commandAuthorization. That widening was dead because the pause handler omitted + // authorizePrActionActor's needsMinerDetection: true, so the miner status was never computed and a + // confirmed miner's own-PR pause always fell through to miner_detection_unavailable. With the flag now + // passed, the documented widening actually authorizes the confirmed miner. + it("pause (#8685): a confirmed Gittensor miner is authorized to pause their OWN PR once the repo widens commandAuthorization for pause", async () => { + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); + await seedPausePr(env); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandAuthorization: { commands: { pause: ["confirmed_miner"] } } }); + await upsertOfficialMinerDetection(env, "reporter", { status: "confirmed", snapshot: queueMinerSnapshot("reporter") }, 60_000); + let postedBody: string | undefined; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // No repo permission at all — the ONLY route to authorization is the confirmed_miner role on their own PR. + if (url.includes("/collaborators/") && url.includes("/permission")) return new Response("not found", { status: 404 }); + if (url.includes("/issues/77/comments")) { + postedBody = init?.body ? JSON.parse(init.body.toString()).body : undefined; + return Response.json({ id: 5 }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + await processJob(env, plannerWebhook("@loopover pause CI is flaky today", "reporter", pauseIssue)); + expect(postedBody).toContain("Auto-review paused by @reporter"); + const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.autoreview_paused").first<{ outcome: string; detail: string }>(); + expect(audit?.outcome).toBe("completed"); + expect(audit?.detail).toBe("CI is flaky today"); + // Provably NOT the pre-fix miner_detection_unavailable denial: no denied marker was recorded. + const denied = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.autoreview_paused_denied").first(); + expect(denied).toBeFalsy(); + }); + it("pause: a pause on a PR with no cached record is recorded as a cached_pr_missing skip, never posted", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); await setupPlannerRepo(env); // repo + installation, but deliberately NO cached PR record diff --git a/test/unit/queue-5.test.ts b/test/unit/queue-5.test.ts index 47e29bfe8c..fa829f9e34 100644 --- a/test/unit/queue-5.test.ts +++ b/test/unit/queue-5.test.ts @@ -4228,6 +4228,51 @@ describe("queue processors", () => { expect(JSON.parse(audited?.metadata_json ?? "{}")).toMatchObject({ status: "ok", byok: false }); }); + // REGRESSION (#8685): generate-tests is a maintainer-only default, but a repo may widen it to + // confirmed_miner via commandAuthorization (the safety clamp keeps confirmed_miner, only dropping the + // spoofable bare pr_author role). That widening was dead because this handler omitted + // authorizePrActionActor's needsMinerDetection: true -- miner status was never computed, so a confirmed + // miner's own-PR invocation always fell through to miner_detection_unavailable. With the flag now passed, + // a confirmed miner on their OWN PR is authorized once the repo widens the command. (Deliberately a + // materially different code path from the pause command's regression: a real generation + comment post.) + it("dispatches generation for a confirmed Gittensor miner on their OWN PR once the repo widens commandAuthorization for generate-tests (#8685)", async () => { + const repoFullName = "JSONbored/gen-tests-8685-miner"; + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + AI: { run: async () => ({ response: "```typescript\n" + VALID_TEST_SOURCE + "\n```" }) } as unknown as Ai, + LOOPOVER_REVIEW_E2E_TESTS: "true", + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + }); + await seedGenerateTestsPr(env, repoFullName, 8685, "gen-tests-8685-miner", "miner-author"); + // Widen generate-tests to confirmed_miner; re-upsert restates seedGenerateTestsPr's own settings since + // upsertRepositorySettings is a full replace, not a merge. + await upsertRepositorySettings(env, { repoFullName, autoLabelEnabled: false, requireLinkedIssue: false, commandAuthorization: { default: ["maintainer"], commands: { "generate-tests": ["confirmed_miner"] } } }); + await upsertOfficialMinerDetection(env, "miner-author", { status: "confirmed", snapshot: queueMinerSnapshot("miner-author") }, 60_000); + let posted = 0; + let postedBody = ""; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + // No repo permission at all -- the ONLY route to authorization is the confirmed_miner role on their own PR. + if (url.includes("/collaborators/miner-author/permission")) return new Response("not found", { status: 404 }); + if (url.includes("/issues/8685/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/8685/comments") && method === "POST") { posted += 1; postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); return Response.json({ id: 86850 }); } + return new Response("not found", { status: 404 }); + }); + + await processJob(env, generateTestsWebhook(repoFullName, 8685, "miner-author", { association: "NONE", commenterIsAuthor: true })); + + expect(posted).toBe(1); + expect(postedBody).toContain("AI-generated Playwright test for @miner-author"); + const audited = await env.DB.prepare("select outcome from audit_events where event_type = ?").bind("github_app.e2e_tests_generation").first<{ outcome: string }>(); + expect(audited?.outcome).toBe("completed"); + // Provably NOT the pre-fix miner_detection_unavailable denial. + const denied = await env.DB.prepare("select 1 from audit_events where event_type = ?").bind("github_app.e2e_tests_generation_denied").first(); + expect(denied).toBeFalsy(); + }); + it("denies a collaborator-tier actor (write permission, not the PR author) — narrower than every other command", async () => { const repoFullName = "JSONbored/gen-tests-4195-collab"; const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), LOOPOVER_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" }); From 5579bf43deea8e6ae6e848d89a00c80e3098eabd Mon Sep 17 00:00:00 2001 From: bitfathers94 <237535319+bitfathers94@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:27:55 +0000 Subject: [PATCH 2/2] fix(test): add required default field to queue-3 pause commandAuthorization fixture The RepositoryCommandAuthorizationPolicy type requires `default`; the #8685 pause widening fixture omitted it, breaking tsc --noEmit. --- test/unit/queue-3.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/queue-3.test.ts b/test/unit/queue-3.test.ts index 4a6c9b8c70..bf3340e094 100644 --- a/test/unit/queue-3.test.ts +++ b/test/unit/queue-3.test.ts @@ -831,7 +831,7 @@ describe("queue processors", () => { it("pause (#8685): a confirmed Gittensor miner is authorized to pause their OWN PR once the repo widens commandAuthorization for pause", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() }); await seedPausePr(env); - await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandAuthorization: { commands: { pause: ["confirmed_miner"] } } }); + await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", commandAuthorization: { default: ["maintainer", "collaborator"], commands: { pause: ["confirmed_miner"] } } }); await upsertOfficialMinerDetection(env, "reporter", { status: "confirmed", snapshot: queueMinerSnapshot("reporter") }, 60_000); let postedBody: string | undefined; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {