Skip to content

Commit 3a47f91

Browse files
committed
feat(review): allow UI rereview via ai-review team request
1 parent be1e4e7 commit 3a47f91

3 files changed

Lines changed: 76 additions & 5 deletions

File tree

docs/runbooks/review-requested-debug.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ For the same `deliveryId`, check review handler gate logs.
5353

5454
Expected outcomes:
5555
- Accepted path: `Accepted review_requested event for kodiai reviewer`
56+
- Accepted path (team-based rereview): `Accepted review_requested event for rereview team` (team `ai-review`)
5657
- Skip path with reason:
5758
- `non-kodiai-reviewer`
5859
- `team-only-request`

src/handlers/review.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,55 @@ describe("createReviewHandler review_requested gating", () => {
229229

230230
await handlers.get("pull_request.review_requested")!(
231231
buildReviewRequestedEvent({
232-
requested_team: { name: "backend" },
232+
requested_team: { name: "backend", slug: "backend" },
233233
}),
234234
);
235235

236236
expect(enqueueCount).toBe(0);
237237
});
238238

239+
test("accepts team-based rereview requests for ai-review", async () => {
240+
const handlers = new Map<string, (event: WebhookEvent) => Promise<void>>();
241+
const enqueued: Array<{ installationId: number }> = [];
242+
243+
const eventRouter: EventRouter = {
244+
register: (eventKey, handler) => {
245+
handlers.set(eventKey, handler);
246+
},
247+
dispatch: async () => undefined,
248+
};
249+
250+
const jobQueue: JobQueue = {
251+
enqueue: async <T>(installationId: number) => {
252+
enqueued.push({ installationId });
253+
return undefined as T;
254+
},
255+
getQueueSize: () => 0,
256+
getPendingCount: () => 0,
257+
};
258+
259+
createReviewHandler({
260+
eventRouter,
261+
jobQueue,
262+
workspaceManager: {} as WorkspaceManager,
263+
githubApp: { getAppSlug: () => "kodiai" } as GitHubApp,
264+
executor: {} as never,
265+
logger: createNoopLogger(),
266+
});
267+
268+
const handler = handlers.get("pull_request.review_requested");
269+
expect(handler).toBeDefined();
270+
271+
await handler!(
272+
buildReviewRequestedEvent({
273+
requested_team: { name: "ai-review", slug: "ai-review" },
274+
}),
275+
);
276+
277+
expect(enqueued).toHaveLength(1);
278+
expect(enqueued[0]?.installationId).toBe(42);
279+
});
280+
239281
test("skips malformed reviewer payloads without throwing", async () => {
240282
const handlers = new Map<string, (event: WebhookEvent) => Promise<void>>();
241283
let enqueueCount = 0;

src/handlers/review.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ function normalizeReviewerLogin(login: string): string {
4444
*
4545
* Trigger model: initial review events plus explicit re-request only.
4646
* Re-requested reviews run only when kodiai itself is the requested reviewer.
47+
* Additionally, a team-based re-request is supported for the special team slug/name "ai-review"
48+
* to enable UI-only re-review without a comment.
4749
* Clones the repo, builds a review prompt, runs Claude via the executor,
4850
* and optionally submits a silent approval if no issues were found.
4951
*/
@@ -57,6 +59,8 @@ export function createReviewHandler(deps: {
5759
}): void {
5860
const { eventRouter, jobQueue, workspaceManager, githubApp, executor, logger } = deps;
5961

62+
const rereviewTeamSlugs = new Set(["ai-review"]);
63+
6064
async function handleReview(event: WebhookEvent): Promise<void> {
6165
const payload = event.payload as unknown as
6266
| PullRequestOpenedEvent
@@ -110,6 +114,10 @@ export function createReviewHandler(deps: {
110114
typeof requestedTeam?.name === "string"
111115
? requestedTeam.name
112116
: undefined;
117+
const requestedTeamSlug =
118+
typeof (requestedTeam as { slug?: unknown } | undefined)?.slug === "string"
119+
? (requestedTeam as { slug: string }).slug
120+
: undefined;
113121
const appSlug = githubApp.getAppSlug();
114122
const normalizedAppSlug = normalizeReviewerLogin(appSlug);
115123

@@ -144,18 +152,38 @@ export function createReviewHandler(deps: {
144152
"Accepted review_requested event for kodiai reviewer",
145153
);
146154
} else if (requestedTeamName) {
155+
const normalizedTeamName = requestedTeamName.trim().toLowerCase();
156+
const normalizedTeamSlug = (requestedTeamSlug ?? "").trim().toLowerCase();
157+
const matchedTeam = rereviewTeamSlugs.has(normalizedTeamSlug) || rereviewTeamSlugs.has(normalizedTeamName);
158+
159+
if (!matchedTeam) {
160+
logger.info(
161+
{
162+
...baseLog,
163+
gate: "review_requested_reviewer",
164+
gateResult: "skipped",
165+
skipReason: "team-only-request",
166+
requestedReviewer: null,
167+
requestedTeam: requestedTeamName,
168+
requestedTeamSlug: requestedTeamSlug ?? null,
169+
},
170+
"Skipping review_requested event because only a non-rereview team was requested",
171+
);
172+
return;
173+
}
174+
147175
logger.info(
148176
{
149177
...baseLog,
150178
gate: "review_requested_reviewer",
151-
gateResult: "skipped",
152-
skipReason: "team-only-request",
179+
gateResult: "accepted",
153180
requestedReviewer: null,
154181
requestedTeam: requestedTeamName,
182+
requestedTeamSlug: requestedTeamSlug ?? null,
183+
rereviewTeam: true,
155184
},
156-
"Skipping review_requested event because only a team was requested",
185+
"Accepted review_requested event for rereview team",
157186
);
158-
return;
159187
} else {
160188
logger.warn(
161189
{

0 commit comments

Comments
 (0)