diff --git a/server/src/__tests__/issue-execution-policy.test.ts b/server/src/__tests__/issue-execution-policy.test.ts index 7c89dba3ba1..37a64dc2784 100644 --- a/server/src/__tests__/issue-execution-policy.test.ts +++ b/server/src/__tests__/issue-execution-policy.test.ts @@ -203,6 +203,43 @@ describe("issue execution policy transitions", () => { }); }); + it("clears loose review instructions with explicit null during a stage transition", () => { + const reviewStageId = policy.stages[0].id; + const result = applyIssueExecutionPolicyTransition({ + issue: { + status: "in_progress", + assigneeAgentId: coderAgentId, + assigneeUserId: null, + executionPolicy: policy, + executionState: { + status: "pending", + currentStageId: reviewStageId, + currentStageIndex: 0, + currentStageType: "review", + currentParticipant: { type: "agent", agentId: qaAgentId }, + returnAssignee: { type: "agent", agentId: coderAgentId }, + reviewRequest: { instructions: "Old review request" }, + completedStageIds: [], + lastDecisionId: null, + lastDecisionOutcome: null, + }, + }, + policy, + requestedStatus: "in_review", + requestedAssigneePatch: {}, + actor: { agentId: coderAgentId }, + commentBody: "Ready for review", + reviewRequest: null, + }); + + expect(result.patch.executionState).toMatchObject({ + status: "pending", + currentStageType: "review", + currentParticipant: { type: "agent", agentId: qaAgentId }, + reviewRequest: null, + }); + }); + it("reviewer approves → advances to approval stage", () => { const reviewStageId = policy.stages[0].id; const result = applyIssueExecutionPolicyTransition({ diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 4889a7fadf5..7e94b1bdb06 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -1892,7 +1892,7 @@ export function issueRoutes( userId: actor.actorType === "user" ? actor.actorId : null, }, commentBody, - reviewRequest: reviewRequest ?? undefined, + reviewRequest: reviewRequest === undefined ? undefined : reviewRequest, }); const decisionId = transition.decision ? randomUUID() : null; if (decisionId) { diff --git a/server/src/services/issue-execution-policy.ts b/server/src/services/issue-execution-policy.ts index c1037d83fb1..9a78512d184 100644 --- a/server/src/services/issue-execution-policy.ts +++ b/server/src/services/issue-execution-policy.ts @@ -304,6 +304,9 @@ export function applyIssueExecutionPolicyTransition(input: TransitionInput): Tra const currentStage = input.policy ? findStageById(input.policy, existingState?.currentStageId) : null; const requestedStatus = input.requestedStatus; const activeStage = currentStage && existingState?.status === PENDING_STATUS ? currentStage : null; + const effectiveReviewRequest = input.reviewRequest === undefined + ? existingState?.reviewRequest ?? null + : input.reviewRequest; if (!input.policy) { if (existingState) { @@ -368,7 +371,7 @@ export function applyIssueExecutionPolicyTransition(input: TransitionInput): Tra stage: activeStage, participant, returnAssignee: existingState?.returnAssignee ?? currentAssignee ?? actor, - reviewRequest: input.reviewRequest ?? existingState?.reviewRequest ?? null, + reviewRequest: effectiveReviewRequest, }); return { patch, @@ -472,7 +475,7 @@ export function applyIssueExecutionPolicyTransition(input: TransitionInput): Tra stage: activeStage, participant: currentParticipant, returnAssignee: existingState?.returnAssignee ?? currentAssignee ?? actor, - reviewRequest: input.reviewRequest ?? existingState?.reviewRequest ?? null, + reviewRequest: effectiveReviewRequest, }); return { patch, diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 5046ba09233..36e9ac97afe 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -1398,27 +1398,22 @@ export function issueService(db: Db) { ) `; if (filters?.descendantOf) { - const descendantIds: string[] = []; - const seen = new Set([filters.descendantOf]); - let frontier = [filters.descendantOf]; - - while (frontier.length > 0) { - const children = await db - .select({ id: issues.id }) - .from(issues) - .where(and(eq(issues.companyId, companyId), inArray(issues.parentId, frontier))); - - frontier = []; - for (const child of children) { - if (seen.has(child.id)) continue; - seen.add(child.id); - descendantIds.push(child.id); - frontier.push(child.id); - } - } - - if (descendantIds.length === 0) return []; - conditions.push(inArray(issues.id, descendantIds)); + conditions.push(sql` + ${issues.id} IN ( + WITH RECURSIVE descendants(id) AS ( + SELECT ${issues.id} + FROM ${issues} + WHERE ${issues.companyId} = ${companyId} + AND ${issues.parentId} = ${filters.descendantOf} + UNION + SELECT ${issues.id} + FROM ${issues} + JOIN descendants ON ${issues.parentId} = descendants.id + WHERE ${issues.companyId} = ${companyId} + ) + SELECT id FROM descendants + ) + `); } if (filters?.status) { const statuses = filters.status.split(",").map((s) => s.trim());