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
24 changes: 18 additions & 6 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12186,13 +12186,17 @@ async function closeReviewEvasionSelfCloseIfActive(
agentPaused: settings.agentPaused,
agentDryRun: settings.agentDryRun,
});
if (!isActingAutonomyLevel(resolveAutonomy(settings.autonomy, "close"))) {
const closeAutonomy = resolveAutonomy(settings.autonomy, "close");
if (closeAutonomy !== "auto") {
await recordAuditEvent(env, {
eventType: REVIEW_EVASION_CLOSED_EVENT_TYPE,
actor: "gittensory",
targetKey,
outcome: "denied",
detail: `autonomy for close is not acting -- review-evasion self-close not enforced for ${pr.authorLogin}`,
detail:
closeAutonomy === "auto_with_approval"
? `close autonomy requires approval -- review-evasion self-close not enforced for ${pr.authorLogin}`
: `autonomy for close is not acting -- review-evasion self-close not enforced for ${pr.authorLogin}`,
metadata: { deliveryId, repoFullName, headSha: pr.headSha },
}).catch(
/* v8 ignore next -- fail-safe: an audit write failure never blocks the handler. */
Expand Down Expand Up @@ -12434,13 +12438,17 @@ async function closeReviewEvasionDraftConversionIfActive(
agentPaused: settings.agentPaused,
agentDryRun: settings.agentDryRun,
});
if (!isActingAutonomyLevel(resolveAutonomy(settings.autonomy, "close"))) {
const closeAutonomy = resolveAutonomy(settings.autonomy, "close");
if (closeAutonomy !== "auto") {
await recordAuditEvent(env, {
eventType: REVIEW_EVASION_CLOSED_EVENT_TYPE,
actor: "gittensory",
targetKey,
outcome: "denied",
detail: `autonomy for close is not acting -- review-evasion draft-conversion not enforced for ${pr.authorLogin}`,
detail:
closeAutonomy === "auto_with_approval"
? `close autonomy requires approval -- review-evasion draft-conversion not enforced for ${pr.authorLogin}`
: `autonomy for close is not acting -- review-evasion draft-conversion not enforced for ${pr.authorLogin}`,
metadata: { deliveryId, repoFullName, headSha: pr.headSha },
}).catch(
/* v8 ignore next -- fail-safe: an audit write failure never blocks the handler. */
Expand Down Expand Up @@ -12648,13 +12656,17 @@ async function closeRepeatedDraftCyclingIfDetected(
agentPaused: settings.agentPaused,
agentDryRun: settings.agentDryRun,
});
if (!isActingAutonomyLevel(resolveAutonomy(settings.autonomy, "close"))) {
const closeAutonomy = resolveAutonomy(settings.autonomy, "close");
if (closeAutonomy !== "auto") {
await recordAuditEvent(env, {
eventType: REVIEW_EVASION_CLOSED_EVENT_TYPE,
actor: "gittensory",
targetKey,
outcome: "denied",
detail: `autonomy for close is not acting -- repeated draft-cycling not enforced for ${pr.authorLogin} (conversion #${draftConversionCount})`,
detail:
closeAutonomy === "auto_with_approval"
? `close autonomy requires approval -- repeated draft-cycling not enforced for ${pr.authorLogin} (conversion #${draftConversionCount})`
: `autonomy for close is not acting -- repeated draft-cycling not enforced for ${pr.authorLogin} (conversion #${draftConversionCount})`,
metadata: { deliveryId, repoFullName, headSha: pr.headSha, draftConversionCount },
}).catch(
/* v8 ignore next -- fail-safe: an audit write failure never blocks the handler. */
Expand Down
47 changes: 47 additions & 0 deletions test/unit/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26375,6 +26375,21 @@ describe("review-evasion protection (#review-evasion-protection)", () => {
expect(audit?.detail).toContain("autonomy for close is not acting");
});

it("REGRESSION: denies live self-close enforcement when close autonomy requires approval", async () => {
const calls: Array<{ url: string; method: string }> = [];
stubEvasionFetch(calls);
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" });
await setupEvasionRepo(env, { autonomy: { close: "auto_with_approval" } });
await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" });

await processJob(env, { type: "github-webhook", deliveryId: "self-close-approval-required", eventName: "pull_request", payload: closedPayload("contributor") });

expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false);
const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>();
expect(audit?.outcome).toBe("denied");
expect(audit?.detail).toContain("requires approval");
});

it("denies enforcement when pull_requests: write is not granted", async () => {
const calls: Array<{ url: string; method: string }> = [];
stubEvasionFetch(calls);
Expand Down Expand Up @@ -26908,6 +26923,21 @@ describe("review-evasion protection (#review-evasion-protection)", () => {
expect(audit?.detail).toContain("autonomy for close is not acting");
});

it("REGRESSION: denies live draft-conversion enforcement when close autonomy requires approval", async () => {
const calls: Array<{ url: string; method: string }> = [];
stubEvasionFetch(calls);
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" });
await setupEvasionRepo(env, { autonomy: { close: "auto_with_approval" } });
await repositoriesModule.startActiveReviewTracking(env, { repoFullName: "JSONbored/gittensory", pullNumber: 42, headSha: "abc123", deliveryId: "review-start-1" });

await processJob(env, { type: "github-webhook", deliveryId: "draft-evasion-approval-required", eventName: "pull_request", payload: draftEvasionPayload("contributor") });

expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false);
const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ?").bind("github_app.review_evasion_closed").first<{ outcome: string; detail: string }>();
expect(audit?.outcome).toBe("denied");
expect(audit?.detail).toContain("requires approval");
});

it("denies enforcement when pull_requests: write is not granted", async () => {
const calls: Array<{ url: string; method: string }> = [];
stubEvasionFetch(calls);
Expand Down Expand Up @@ -27306,6 +27336,23 @@ describe("review-evasion protection (#review-evasion-protection)", () => {
expect(audit?.detail).toContain("autonomy for close is not acting");
});

it("REGRESSION: denies live repeated draft-cycling enforcement when close autonomy requires approval", async () => {
const calls: Array<{ url: string; method: string }> = [];
stubEvasionFetch(calls);
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "gittensory" });
await setupEvasionRepo(env, { autonomy: { close: "auto_with_approval" } });

await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-approval-required-1", eventName: "pull_request", payload: draftEvasionPayload("contributor") });
await processJob(env, { type: "github-webhook", deliveryId: "draft-cycle-approval-required-2", eventName: "pull_request", payload: draftEvasionPayload("contributor") });

expect(calls.some((c) => c.method === "PATCH" && c.url.endsWith("/pulls/42"))).toBe(false);
const audit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? order by rowid desc limit 1")
.bind("github_app.review_evasion_closed")
.first<{ outcome: string; detail: string }>();
expect(audit?.outcome).toBe("denied");
expect(audit?.detail).toContain("requires approval");
});

it("dry-run: audits the would-be enforcement without mutating GitHub or recording a live strike", async () => {
const calls: Array<{ url: string; method: string }> = [];
stubEvasionFetch(calls);
Expand Down