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
8 changes: 5 additions & 3 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12171,7 +12171,7 @@ async function maybeProcessExplainCommand(env: Env, deliveryId: string, payload:
const { authorization } = await authorizePrActionActor({ env, deliveryId, installationId: req.installationId, repoFullName: req.repoFullName, issue: payload.issue!, actor: req.actor, commandName: "explain" as LoopOverMentionCommandName, settings, pr });
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 } });
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") } });
return true;
}
const findingRef = normalizeResolveFindingRef(command.argument);
Expand Down Expand Up @@ -12252,7 +12252,7 @@ async function maybeProcessGenerateTestsCommand(env: Env, deliveryId: string, pa
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 });
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 } });
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") } });
return true;
}
const manifest = await loadRepoFocusManifest(env, req.repoFullName).catch(() => null);
Expand Down Expand Up @@ -12490,6 +12490,7 @@ async function maybeProcessConfigurationCommand(
detail: `Effective configuration posted for ${targetKey}.`,
metadata: { deliveryId, repoFullName: req.repoFullName, mode },
});
await recordGithubProductUsage(env, "configuration_posted", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "completed", metadata: { mode } });
return true;
}

Expand All @@ -12509,6 +12510,7 @@ async function recordConfigurationSkip(
detail: reason,
metadata: { deliveryId, repoFullName, reason },
});
await recordGithubProductUsage(env, "configuration_skipped", { actor, repoFullName, targetKey, outcome: "skipped", metadata: { reason } });
}

/**
Expand Down Expand Up @@ -12989,7 +12991,7 @@ async function maybeProcessPrPanelGenerateTests(
repoFullName,
targetKey: `${repoFullName}#${pr.number}`,
outcome: "denied",
metadata: { reason: authorization.reason, actorKind: authorization.actorKind },
metadata: { reason: authorization.reason, actorKind: authorization.actorKind, allowedRoles: commandAuthorizationAllowedRoles(settings.commandAuthorization, "generate-tests") },
});
return true;
}
Expand Down
18 changes: 18 additions & 0 deletions test/unit/queue-3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,24 @@ describe("queue processors", () => {
expect(audit?.outcome).toBe("completed");
});

it("configuration (#8688): a posted @loopover configuration is recorded in product-usage telemetry", async () => {
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() });
await setupPlannerRepo(env);
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" });
if (url.includes("/collaborators/") && url.includes("/permission")) return Response.json({ permission: "admin" }); // maintainer
if (url.includes("/issues/77/comments") && method === "GET") return Response.json([]);
if (url.includes("/issues/77/comments") && method === "POST") return Response.json({ id: 5 }, { status: 201 });
return new Response("not found", { status: 404 });
});
await processJob(env, plannerWebhook("@loopover configuration", "maintainer1"));
const usage = await env.DB.prepare("select outcome, json_extract(metadata_json, '$.mode') as mode from product_usage_events where event_name = ?").bind("configuration_posted").first<{ outcome: string; mode: string }>();
expect(usage?.outcome).toBe("completed");
expect(usage?.mode).toBe("live");
});

it.each([
["env pause", async (env: Env) => { (env as Env & { AGENT_ACTIONS_PAUSED: string }).AGENT_ACTIONS_PAUSED = "true"; }, "paused"],
["repo pause", async (env: Env) => { await upsertRepositorySettings(env, { repoFullName: "JSONbored/gittensory", agentPaused: true }); }, "paused"],
Expand Down
36 changes: 36 additions & 0 deletions test/unit/queue-5.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4076,6 +4076,24 @@ describe("queue processors", () => {
expect(denied).toMatchObject({ outcome: "denied" });
});

it("records the explain denial in product-usage telemetry WITH the command's allowedRoles (#8688)", async () => {
const repoFullName = "JSONbored/explain-8688-deny-usage";
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() });
await seedExplainPr(env, repoFullName, 8688, "explain-8688-deny-usage");
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
const url = input.toString();
if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" });
if (url.includes("/collaborators/org-member/permission")) return Response.json({ permission: "read" });
return new Response("not found", { status: 404 });
});

await processJob(env, explainWebhook(repoFullName, 8688, "@loopover explain ai_review_split", "org-member", { association: "MEMBER" }));

const usage = await env.DB.prepare("select outcome, json_extract(metadata_json, '$.allowedRoles') as roles from product_usage_events where event_name = ?").bind("finding_explained_denied").first<{ outcome: string; roles: string | null }>();
expect(usage?.outcome).toBe("denied");
expect(JSON.parse(usage?.roles ?? "null")).toEqual(["maintainer", "collaborator"]);
});

it("records a classifier skip for a bot-authored explain command, never acting on it", async () => {
const repoFullName = "JSONbored/explain-2169-bot";
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() });
Expand Down Expand Up @@ -4232,6 +4250,24 @@ describe("queue processors", () => {
expect(denied?.outcome).toBe("denied");
});

it("records the generate-tests denial in product-usage telemetry WITH the command's allowedRoles (#8688)", async () => {
const repoFullName = "JSONbored/gen-tests-8688-deny-usage";
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), LOOPOVER_REVIEW_E2E_TESTS: "true", AI_SUMMARIES_ENABLED: "true", AI_PUBLIC_COMMENTS_ENABLED: "true" });
await seedGenerateTestsPr(env, repoFullName, 8690, "gen-tests-8688-deny-usage");
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
const url = input.toString();
if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" });
if (url.includes("/collaborators/writer/permission")) return Response.json({ permission: "write" });
return new Response("not found", { status: 404 });
});

await processJob(env, generateTestsWebhook(repoFullName, 8690, "writer", { association: "COLLABORATOR" }));

const usage = await env.DB.prepare("select outcome, json_extract(metadata_json, '$.allowedRoles') as roles from product_usage_events where event_name = ?").bind("e2e_tests_generation_denied").first<{ outcome: string; roles: string | null }>();
expect(usage?.outcome).toBe("denied");
expect(JSON.parse(usage?.roles ?? "null")).toEqual(["maintainer"]);
});

it("denies the PR's own author even though they authored it — the exact loophole a click-to-generate button must not open", async () => {
const repoFullName = "JSONbored/gen-tests-4195-author";
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