diff --git a/src/paired-execution-context.test.ts b/src/paired-execution-context.test.ts index 4a7b77b..c441299 100644 --- a/src/paired-execution-context.test.ts +++ b/src/paired-execution-context.test.ts @@ -583,4 +583,73 @@ describe('paired execution context', () => { }), ); }); + + it('keeps reviewer tasks review_ready when reviewer execution fails without a terminal verdict', () => { + vi.mocked(db.getPairedTaskById).mockReturnValue( + buildPairedTask({ + status: 'in_review', + }), + ); + + completePairedExecutionContext({ + taskId: 'task-1', + role: 'reviewer', + status: 'failed', + summary: 'runtime exploded before verdict', + }); + + expect(db.updatePairedTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ + status: 'review_ready', + updated_at: expect.any(String), + }), + ); + }); + + it('keeps arbiter tasks arbiter_requested when arbiter execution fails without a terminal verdict', () => { + vi.mocked(db.getPairedTaskById).mockReturnValue( + buildPairedTask({ + status: 'in_arbitration', + }), + ); + + completePairedExecutionContext({ + taskId: 'task-1', + role: 'arbiter', + status: 'failed', + summary: 'runtime exploded before verdict', + }); + + expect(db.updatePairedTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ + status: 'arbiter_requested', + updated_at: expect.any(String), + }), + ); + }); + + it('still resets owner tasks to active after failed execution', () => { + vi.mocked(db.getPairedTaskById).mockReturnValue( + buildPairedTask({ + status: 'merge_ready', + }), + ); + + completePairedExecutionContext({ + taskId: 'task-1', + role: 'owner', + status: 'failed', + summary: 'push failed', + }); + + expect(db.updatePairedTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ + status: 'active', + updated_at: expect.any(String), + }), + ); + }); }); diff --git a/src/paired-execution-context.ts b/src/paired-execution-context.ts index 251b2cf..86dd418 100644 --- a/src/paired-execution-context.ts +++ b/src/paired-execution-context.ts @@ -466,8 +466,46 @@ export function completePairedExecutionContext(args: { return; } } + const now = new Date().toISOString(); + if (role === 'reviewer') { + const fallbackStatus = + task.status === 'in_review' || task.status === 'review_ready' + ? 'review_ready' + : task.status; + if (fallbackStatus !== task.status) { + updatePairedTask(taskId, { status: fallbackStatus, updated_at: now }); + logger.warn( + { + taskId, + role, + previousStatus: task.status, + nextStatus: fallbackStatus, + }, + 'Preserved reviewer task in review-ready state after failed execution', + ); + } + return; + } + if (role === 'arbiter') { + const fallbackStatus = + task.status === 'in_arbitration' || task.status === 'arbiter_requested' + ? 'arbiter_requested' + : task.status; + if (fallbackStatus !== task.status) { + updatePairedTask(taskId, { status: fallbackStatus, updated_at: now }); + logger.warn( + { + taskId, + role, + previousStatus: task.status, + nextStatus: fallbackStatus, + }, + 'Preserved arbiter task in arbitration-requested state after failed execution', + ); + } + return; + } if (task.status !== 'active') { - const now = new Date().toISOString(); updatePairedTask(taskId, { status: 'active', updated_at: now }); logger.info( { taskId, role, previousStatus: task.status },