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
13 changes: 7 additions & 6 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11757,6 +11757,7 @@ async function maybeProcessGateOverrideCommand(
commandName: "gate-override" as LoopOverMentionCommandName,
settings,
pr,
needsMinerDetection: true,
});
if (!authorization.authorized) {
await recordAuditEvent(env, {
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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") } });
Expand Down Expand Up @@ -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") } });
Expand Down Expand Up @@ -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") } });
Expand Down Expand Up @@ -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") } });
Expand Down Expand Up @@ -12973,7 +12974,7 @@ async function maybeProcessPrPanelGenerateTests(
commandName: "generate-tests" as LoopOverMentionCommandName,
settings,
pr,
needsMinerDetection: false,
needsMinerDetection: true,
});
if (!authorization.authorized) {
await recordAuditEvent(env, {
Expand Down
32 changes: 32 additions & 0 deletions test/unit/queue-3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: { 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) => {
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
Expand Down
45 changes: 45 additions & 0 deletions test/unit/queue-5.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand Down