forked from farhoodlabs/paperclip
Fix signoff stage access and comment wake retries
This commit is contained in:
@@ -413,45 +413,33 @@ describe("issue execution policy transitions", () => {
|
||||
const policy = twoStagePolicy();
|
||||
const reviewStageId = policy.stages[0].id;
|
||||
|
||||
it("non-participant stage updates are coerced back to the active stage", () => {
|
||||
const result = applyIssueExecutionPolicyTransition({
|
||||
issue: {
|
||||
status: "in_review",
|
||||
assigneeAgentId: qaAgentId,
|
||||
assigneeUserId: null,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: reviewStageId,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: qaAgentId },
|
||||
returnAssignee: { type: "agent", agentId: coderAgentId },
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
it("non-participant cannot advance the active stage", () => {
|
||||
expect(() =>
|
||||
applyIssueExecutionPolicyTransition({
|
||||
issue: {
|
||||
status: "in_review",
|
||||
assigneeAgentId: qaAgentId,
|
||||
assigneeUserId: null,
|
||||
executionPolicy: policy,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: reviewStageId,
|
||||
currentStageIndex: 0,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: qaAgentId },
|
||||
returnAssignee: { type: "agent", agentId: coderAgentId },
|
||||
completedStageIds: [],
|
||||
lastDecisionId: null,
|
||||
lastDecisionOutcome: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
policy,
|
||||
requestedStatus: "done",
|
||||
requestedAssigneePatch: { assigneeUserId: boardUserId },
|
||||
actor: { agentId: coderAgentId },
|
||||
commentBody: "Trying to bypass review",
|
||||
});
|
||||
|
||||
expect(result.patch).toMatchObject({
|
||||
status: "in_review",
|
||||
assigneeAgentId: qaAgentId,
|
||||
assigneeUserId: null,
|
||||
executionState: {
|
||||
status: "pending",
|
||||
currentStageId: reviewStageId,
|
||||
currentStageType: "review",
|
||||
currentParticipant: { type: "agent", agentId: qaAgentId },
|
||||
returnAssignee: { type: "agent", agentId: coderAgentId },
|
||||
},
|
||||
});
|
||||
expect(result.decision).toBeUndefined();
|
||||
policy,
|
||||
requestedStatus: "done",
|
||||
requestedAssigneePatch: { assigneeUserId: boardUserId },
|
||||
actor: { agentId: coderAgentId },
|
||||
commentBody: "Trying to bypass review",
|
||||
}),
|
||||
).toThrow("Only the active reviewer or approver can advance");
|
||||
});
|
||||
|
||||
it("non-participant can still post non-advancing updates", () => {
|
||||
|
||||
@@ -707,6 +707,18 @@ export function shouldResetTaskSessionForWake(
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldRequireIssueCommentForWake(
|
||||
contextSnapshot: Record<string, unknown> | null | undefined,
|
||||
) {
|
||||
const wakeReason = readNonEmptyString(contextSnapshot?.wakeReason);
|
||||
return (
|
||||
wakeReason === "issue_assigned" ||
|
||||
wakeReason === "execution_review_requested" ||
|
||||
wakeReason === "execution_approval_requested" ||
|
||||
wakeReason === "execution_changes_requested"
|
||||
);
|
||||
}
|
||||
|
||||
export function formatRuntimeWorkspaceWarningLog(warning: string) {
|
||||
return {
|
||||
stream: "stdout" as const,
|
||||
@@ -2035,6 +2047,17 @@ export function heartbeatService(db: Db) {
|
||||
return { outcome: "retry_exhausted" as const, queuedRun: null };
|
||||
}
|
||||
|
||||
if (!shouldRequireIssueCommentForWake(contextSnapshot)) {
|
||||
if (run.issueCommentStatus !== "not_applicable") {
|
||||
await patchRunIssueCommentStatus(run.id, {
|
||||
issueCommentStatus: "not_applicable",
|
||||
issueCommentSatisfiedByCommentId: null,
|
||||
issueCommentRetryQueuedAt: null,
|
||||
});
|
||||
}
|
||||
return { outcome: "not_applicable" as const, queuedRun: null };
|
||||
}
|
||||
|
||||
const queuedRun = await enqueueMissingIssueCommentRetry(run, agent, issueId);
|
||||
if (queuedRun) {
|
||||
await appendRunEvent(run, await nextRunEventSeq(run.id), {
|
||||
|
||||
@@ -393,13 +393,19 @@ export function applyIssueExecutionPolicyTransition(input: TransitionInput): Tra
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
const attemptedStageAdvance =
|
||||
(requestedStatus !== undefined && requestedStatus !== "in_review") ||
|
||||
(requestedAssigneePatchProvided && !principalsEqual(explicitAssignee, currentParticipant));
|
||||
const stageStateDrifted =
|
||||
input.issue.status !== "in_review" ||
|
||||
!principalsEqual(currentAssignee, currentParticipant) ||
|
||||
!principalsEqual(existingState?.currentParticipant ?? null, currentParticipant) ||
|
||||
(requestedStatus !== undefined && requestedStatus !== "in_review") ||
|
||||
(requestedAssigneePatchProvided && !principalsEqual(explicitAssignee, currentParticipant))
|
||||
) {
|
||||
!principalsEqual(existingState?.currentParticipant ?? null, currentParticipant);
|
||||
|
||||
if (attemptedStageAdvance && !stageStateDrifted) {
|
||||
throw unprocessable("Only the active reviewer or approver can advance the current execution stage");
|
||||
}
|
||||
|
||||
if (stageStateDrifted) {
|
||||
buildPendingStagePatch({
|
||||
patch,
|
||||
previous: existingState,
|
||||
|
||||
Reference in New Issue
Block a user