Route STEP_DONE through reviewer
This commit is contained in:
@@ -83,7 +83,7 @@ describe('message-runtime-follow-up', () => {
|
|||||||
expect(enqueue).toHaveBeenCalledTimes(1);
|
expect(enqueue).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the fallback STEP_DONE verdict to schedule owner follow-ups when the owner keeps the task active', () => {
|
it('does not use fallback owner STEP_DONE verdict to schedule owner follow-ups while active', () => {
|
||||||
const enqueue = vi.fn();
|
const enqueue = vi.fn();
|
||||||
const result = dispatchPairedFollowUpForEvent({
|
const result = dispatchPairedFollowUpForEvent({
|
||||||
chatJid: 'group@test',
|
chatJid: 'group@test',
|
||||||
@@ -102,14 +102,12 @@ describe('message-runtime-follow-up', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
kind: 'paired-follow-up',
|
kind: 'none',
|
||||||
intentKind: 'owner-follow-up',
|
|
||||||
scheduled: true,
|
|
||||||
taskStatus: 'active',
|
taskStatus: 'active',
|
||||||
lastTurnOutputRole: 'owner',
|
lastTurnOutputRole: 'owner',
|
||||||
lastTurnOutputVerdict: 'step_done',
|
lastTurnOutputVerdict: 'step_done',
|
||||||
});
|
});
|
||||||
expect(enqueue).toHaveBeenCalledTimes(1);
|
expect(enqueue).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prefers the latest persisted turn output over the fallback delivery role when both are present', () => {
|
it('prefers the latest persisted turn output over the fallback delivery role when both are present', () => {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ describe('message-runtime-rules', () => {
|
|||||||
lastTurnOutputVerdict: 'step_done',
|
lastTurnOutputVerdict: 'step_done',
|
||||||
intentKind: 'owner-follow-up',
|
intentKind: 'owner-follow-up',
|
||||||
}),
|
}),
|
||||||
).toBe(true);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps active tasks with reviewer output to an owner follow-up', () => {
|
it('maps active tasks with reviewer output to an owner follow-up', () => {
|
||||||
@@ -132,14 +132,14 @@ describe('message-runtime-rules', () => {
|
|||||||
).toEqual({ kind: 'owner-follow-up' });
|
).toEqual({ kind: 'owner-follow-up' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps active tasks with owner STEP_DONE output to an owner follow-up', () => {
|
it('does not map active owner STEP_DONE output to an owner follow-up', () => {
|
||||||
expect(
|
expect(
|
||||||
resolveNextTurnAction({
|
resolveNextTurnAction({
|
||||||
taskStatus: 'active',
|
taskStatus: 'active',
|
||||||
lastTurnOutputRole: 'owner',
|
lastTurnOutputRole: 'owner',
|
||||||
lastTurnOutputVerdict: 'step_done',
|
lastTurnOutputVerdict: 'step_done',
|
||||||
}),
|
}),
|
||||||
).toEqual({ kind: 'owner-follow-up' });
|
).toEqual({ kind: 'none' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns none when an active task has no reviewer or arbiter handoff output', () => {
|
it('returns none when an active task has no reviewer or arbiter handoff output', () => {
|
||||||
@@ -329,7 +329,7 @@ describe('message-runtime-rules', () => {
|
|||||||
lastTurnOutputRole: 'owner',
|
lastTurnOutputRole: 'owner',
|
||||||
lastTurnOutputVerdict: 'step_done',
|
lastTurnOutputVerdict: 'step_done',
|
||||||
}),
|
}),
|
||||||
).toBe('owner');
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves reviewer execution target from review_ready task status', () => {
|
it('resolves reviewer execution target from review_ready task status', () => {
|
||||||
|
|||||||
@@ -146,8 +146,7 @@ export function resolveNextTurnAction(args: {
|
|||||||
? { kind: 'none' }
|
? { kind: 'none' }
|
||||||
: { kind: 'finalize-owner-turn' };
|
: { kind: 'finalize-owner-turn' };
|
||||||
case 'active':
|
case 'active':
|
||||||
return args.lastTurnOutputVerdict === 'step_done' ||
|
return args.lastTurnOutputRole === 'reviewer' ||
|
||||||
args.lastTurnOutputRole === 'reviewer' ||
|
|
||||||
args.lastTurnOutputRole === 'arbiter'
|
args.lastTurnOutputRole === 'arbiter'
|
||||||
? { kind: 'owner-follow-up' }
|
? { kind: 'owner-follow-up' }
|
||||||
: { kind: 'none' };
|
: { kind: 'none' };
|
||||||
|
|||||||
@@ -2126,7 +2126,7 @@ If your first line is DONE_WITH_CONCERNS, the system will reopen review instead
|
|||||||
expect(pending?.prompt).toContain('이전 reviewer 피드백');
|
expect(pending?.prompt).toContain('이전 reviewer 피드백');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('builds an owner follow-up pending turn when the latest owner output is STEP_DONE on an active task', () => {
|
it('does not build an owner follow-up pending turn from owner STEP_DONE on an active task', () => {
|
||||||
const chatJid = 'group@test';
|
const chatJid = 'group@test';
|
||||||
const group = makeGroup('claude-code');
|
const group = makeGroup('claude-code');
|
||||||
const task = {
|
const task = {
|
||||||
@@ -2170,12 +2170,7 @@ If your first line is DONE_WITH_CONCERNS, the system will reopen review instead
|
|||||||
resolveChannel: () => makeChannel(chatJid),
|
resolveChannel: () => makeChannel(chatJid),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(pending).not.toBeNull();
|
expect(pending).toBeNull();
|
||||||
expect(pending).toMatchObject({
|
|
||||||
role: 'owner',
|
|
||||||
intentKind: 'owner-follow-up',
|
|
||||||
taskId: task.id,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('builds a reviewer pending turn when a review_ready task follows owner TASK_DONE output', () => {
|
it('builds a reviewer pending turn when a review_ready task follows owner TASK_DONE output', () => {
|
||||||
@@ -2335,7 +2330,7 @@ If your first line is DONE_WITH_CONCERNS, the system will reopen review instead
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('re-enqueues owner follow-up after a successful owner STEP_DONE delivery keeps the task active', async () => {
|
it('does not re-enqueue owner follow-up after a successful owner STEP_DONE delivery keeps the task active', async () => {
|
||||||
const chatJid = 'group@test';
|
const chatJid = 'group@test';
|
||||||
const group = makeGroup('codex');
|
const group = makeGroup('codex');
|
||||||
const ownerChannel: Channel = {
|
const ownerChannel: Channel = {
|
||||||
@@ -2445,17 +2440,12 @@ If your first line is DONE_WITH_CONCERNS, the system will reopen review instead
|
|||||||
chatJid,
|
chatJid,
|
||||||
'STEP_DONE\n리팩토링 1단계 완료, 다음 단계 진행',
|
'STEP_DONE\n리팩토링 1단계 완료, 다음 단계 진행',
|
||||||
);
|
);
|
||||||
expect(enqueueMessageCheck).toHaveBeenCalledWith(chatJid);
|
expect(enqueueMessageCheck).not.toHaveBeenCalled();
|
||||||
expect(pairedTask.status).toBe('active');
|
expect(pairedTask.status).toBe('active');
|
||||||
expect(pairedTask.round_trip_count).toBe(1);
|
expect(pairedTask.round_trip_count).toBe(1);
|
||||||
expect(pairedTask.owner_failure_count).toBe(0);
|
expect(pairedTask.owner_failure_count).toBe(0);
|
||||||
expect(logger.info).toHaveBeenCalledWith(
|
expect(logger.info).not.toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
chatJid,
|
|
||||||
runId: 'run-owner-step-done-follow-up',
|
|
||||||
completedRole: 'owner',
|
|
||||||
taskId: 'task-owner-step-done-follow-up',
|
|
||||||
taskStatus: 'active',
|
|
||||||
intentKind: 'owner-follow-up',
|
intentKind: 'owner-follow-up',
|
||||||
}),
|
}),
|
||||||
'Queued paired follow-up after successful owner delivery',
|
'Queued paired follow-up after successful owner delivery',
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ import {
|
|||||||
} from './paired-execution-context-shared.js';
|
} from './paired-execution-context-shared.js';
|
||||||
import type { PairedTask } from './types.js';
|
import type { PairedTask } from './types.js';
|
||||||
|
|
||||||
type OwnerFinalizeOutcome = 'stop' | 're_review' | 'continue_owner';
|
type OwnerFinalizeOutcome = 'stop' | 're_review';
|
||||||
const OWNER_FAILURE_ESCALATION_THRESHOLD = 2;
|
const OWNER_FAILURE_ESCALATION_THRESHOLD = 2;
|
||||||
const EMPTY_STEP_DONE_THRESHOLD = 2;
|
const EMPTY_STEP_DONE_THRESHOLD = 2;
|
||||||
const OWNER_STEP_DONE_LOOP_THRESHOLD = 3;
|
|
||||||
|
|
||||||
export function handleFailedOwnerExecution(args: {
|
export function handleFailedOwnerExecution(args: {
|
||||||
task: PairedTask;
|
task: PairedTask;
|
||||||
@@ -157,7 +156,7 @@ function handleOwnerFinalizeCompletion(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
signal.kind === 'request_owner_continue' &&
|
ownerVerdict === 'step_done' &&
|
||||||
hasNewChanges === false &&
|
hasNewChanges === false &&
|
||||||
nextEmptyStepDoneStreak >= EMPTY_STEP_DONE_THRESHOLD
|
nextEmptyStepDoneStreak >= EMPTY_STEP_DONE_THRESHOLD
|
||||||
) {
|
) {
|
||||||
@@ -200,7 +199,8 @@ function handleOwnerFinalizeCompletion(args: {
|
|||||||
owner_failure_count: 0,
|
owner_failure_count: 0,
|
||||||
owner_step_done_streak: 0,
|
owner_step_done_streak: 0,
|
||||||
finalize_step_done_count: nextFinalizeStepDoneCount,
|
finalize_step_done_count: nextFinalizeStepDoneCount,
|
||||||
empty_step_done_streak: 0,
|
empty_step_done_streak:
|
||||||
|
ownerVerdict === 'step_done' ? nextEmptyStepDoneStreak : 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -213,36 +213,27 @@ function handleOwnerFinalizeCompletion(args: {
|
|||||||
},
|
},
|
||||||
ownerVerdict === 'done_with_concerns'
|
ownerVerdict === 'done_with_concerns'
|
||||||
? 'Owner raised concerns during finalize — task set back to active'
|
? 'Owner raised concerns during finalize — task set back to active'
|
||||||
: 'Owner made changes after reviewer approval — task set back to active before re-review',
|
: ownerVerdict === 'step_done'
|
||||||
|
? 'Owner reported STEP_DONE during finalize — task set back to active before review'
|
||||||
|
: 'Owner made changes after reviewer approval — task set back to active before re-review',
|
||||||
);
|
);
|
||||||
return 're_review';
|
maybeAutoTriggerReviewerAfterOwnerCompletion({
|
||||||
}
|
task,
|
||||||
|
|
||||||
if (signal.kind === 'request_owner_continue') {
|
|
||||||
transitionPairedTaskStatus({
|
|
||||||
taskId,
|
taskId,
|
||||||
currentStatus: task.status,
|
now,
|
||||||
nextStatus: 'active',
|
logMessage:
|
||||||
expectedUpdatedAt: task.updated_at,
|
ownerVerdict === 'step_done'
|
||||||
updatedAt: now,
|
? 'Auto-triggered reviewer after owner finalize STEP_DONE'
|
||||||
patch: {
|
: 'Auto-triggered reviewer after owner finalize required re-review',
|
||||||
owner_failure_count: 0,
|
patch:
|
||||||
owner_step_done_streak: 0,
|
ownerVerdict === 'step_done'
|
||||||
finalize_step_done_count: nextFinalizeStepDoneCount,
|
? {
|
||||||
empty_step_done_streak: nextEmptyStepDoneStreak,
|
finalize_step_done_count: nextFinalizeStepDoneCount,
|
||||||
},
|
empty_step_done_streak: nextEmptyStepDoneStreak,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
logger.info(
|
return 'stop';
|
||||||
{
|
|
||||||
taskId,
|
|
||||||
ownerVerdict,
|
|
||||||
emptyStepDoneStreak: nextEmptyStepDoneStreak,
|
|
||||||
finalizeStepDoneCount: nextFinalizeStepDoneCount,
|
|
||||||
summary: summary?.slice(0, 100),
|
|
||||||
},
|
|
||||||
'Owner marked finalize output as an intermediate step — task returned to active without re-review',
|
|
||||||
);
|
|
||||||
return 'continue_owner';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transitionPairedTaskStatus({
|
transitionPairedTaskStatus({
|
||||||
@@ -271,6 +262,11 @@ function maybeAutoTriggerReviewerAfterOwnerCompletion(args: {
|
|||||||
taskId: string;
|
taskId: string;
|
||||||
now: string;
|
now: string;
|
||||||
logMessage: string;
|
logMessage: string;
|
||||||
|
patch?: {
|
||||||
|
owner_step_done_streak?: number;
|
||||||
|
finalize_step_done_count?: number;
|
||||||
|
empty_step_done_streak?: number;
|
||||||
|
};
|
||||||
}): void {
|
}): void {
|
||||||
const { task, taskId, now, logMessage } = args;
|
const { task, taskId, now, logMessage } = args;
|
||||||
if (task.round_trip_count >= PAIRED_MAX_ROUND_TRIPS) {
|
if (task.round_trip_count >= PAIRED_MAX_ROUND_TRIPS) {
|
||||||
@@ -300,6 +296,7 @@ function maybeAutoTriggerReviewerAfterOwnerCompletion(args: {
|
|||||||
owner_failure_count: 0,
|
owner_failure_count: 0,
|
||||||
owner_step_done_streak: 0,
|
owner_step_done_streak: 0,
|
||||||
empty_step_done_streak: 0,
|
empty_step_done_streak: 0,
|
||||||
|
...args.patch,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (hasActiveCiWatcherForChat(task.chat_jid)) {
|
if (hasActiveCiWatcherForChat(task.chat_jid)) {
|
||||||
@@ -345,16 +342,8 @@ export function handleOwnerCompletion(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ownerVerdict = parseVisibleVerdict(summary);
|
const ownerVerdict = parseVisibleVerdict(summary);
|
||||||
const workspace = getPairedWorkspace(task.id, 'owner');
|
|
||||||
const hasNewChanges = workspace?.workspace_dir
|
|
||||||
? hasCodeChangesSinceRef(workspace.workspace_dir, task.source_ref)
|
|
||||||
: null;
|
|
||||||
const nextOwnerStepDoneStreak =
|
const nextOwnerStepDoneStreak =
|
||||||
ownerVerdict === 'step_done' ? (task.owner_step_done_streak ?? 0) + 1 : 0;
|
ownerVerdict === 'step_done' ? (task.owner_step_done_streak ?? 0) + 1 : 0;
|
||||||
const nextEmptyStepDoneStreak =
|
|
||||||
ownerVerdict === 'step_done' && hasNewChanges === false
|
|
||||||
? (task.empty_step_done_streak ?? 0) + 1
|
|
||||||
: 0;
|
|
||||||
const signal = resolveOwnerCompletionSignal({
|
const signal = resolveOwnerCompletionSignal({
|
||||||
phase: 'normal',
|
phase: 'normal',
|
||||||
visibleVerdict: ownerVerdict,
|
visibleVerdict: ownerVerdict,
|
||||||
@@ -372,86 +361,23 @@ export function handleOwnerCompletion(args: {
|
|||||||
taskId,
|
taskId,
|
||||||
ownerVerdict,
|
ownerVerdict,
|
||||||
ownerStepDoneStreak: nextOwnerStepDoneStreak,
|
ownerStepDoneStreak: nextOwnerStepDoneStreak,
|
||||||
emptyStepDoneStreak: nextEmptyStepDoneStreak,
|
|
||||||
summary: summary?.slice(0, 100),
|
summary: summary?.slice(0, 100),
|
||||||
},
|
},
|
||||||
patch: {
|
patch: {
|
||||||
owner_failure_count: 0,
|
owner_failure_count: 0,
|
||||||
owner_step_done_streak: nextOwnerStepDoneStreak,
|
owner_step_done_streak: nextOwnerStepDoneStreak,
|
||||||
empty_step_done_streak: nextEmptyStepDoneStreak,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (nextOwnerStepDoneStreak !== (task.owner_step_done_streak ?? 0)) {
|
||||||
signal.kind === 'request_owner_continue' &&
|
|
||||||
hasNewChanges === false &&
|
|
||||||
nextOwnerStepDoneStreak >= OWNER_STEP_DONE_LOOP_THRESHOLD &&
|
|
||||||
nextEmptyStepDoneStreak >= OWNER_STEP_DONE_LOOP_THRESHOLD
|
|
||||||
) {
|
|
||||||
requestArbiterOrEscalate({
|
|
||||||
taskId,
|
|
||||||
currentStatus: task.status,
|
|
||||||
expectedUpdatedAt: task.updated_at,
|
|
||||||
now,
|
|
||||||
arbiterLogMessage:
|
|
||||||
'Owner repeated STEP_DONE in active mode without code changes — requesting arbiter',
|
|
||||||
escalateLogMessage:
|
|
||||||
'Owner repeated STEP_DONE in active mode without code changes — escalating to user',
|
|
||||||
logContext: {
|
|
||||||
taskId,
|
|
||||||
ownerVerdict,
|
|
||||||
hasNewChanges,
|
|
||||||
ownerStepDoneStreak: nextOwnerStepDoneStreak,
|
|
||||||
emptyStepDoneStreak: nextEmptyStepDoneStreak,
|
|
||||||
summary: summary?.slice(0, 100),
|
|
||||||
},
|
|
||||||
patch: {
|
|
||||||
owner_failure_count: 0,
|
|
||||||
owner_step_done_streak: nextOwnerStepDoneStreak,
|
|
||||||
empty_step_done_streak: nextEmptyStepDoneStreak,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signal.kind === 'request_owner_continue') {
|
|
||||||
applyPairedTaskPatch({
|
|
||||||
taskId,
|
|
||||||
expectedUpdatedAt: task.updated_at,
|
|
||||||
updatedAt: now,
|
|
||||||
patch: {
|
|
||||||
owner_failure_count: 0,
|
|
||||||
owner_step_done_streak: nextOwnerStepDoneStreak,
|
|
||||||
empty_step_done_streak: nextEmptyStepDoneStreak,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
logger.info(
|
|
||||||
{
|
|
||||||
taskId,
|
|
||||||
ownerVerdict,
|
|
||||||
hasNewChanges,
|
|
||||||
ownerStepDoneStreak: nextOwnerStepDoneStreak,
|
|
||||||
emptyStepDoneStreak: nextEmptyStepDoneStreak,
|
|
||||||
summary: summary?.slice(0, 100),
|
|
||||||
},
|
|
||||||
'Owner marked the current output as an intermediate completed step — keeping task active for follow-up',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
nextOwnerStepDoneStreak !== (task.owner_step_done_streak ?? 0) ||
|
|
||||||
nextEmptyStepDoneStreak !== (task.empty_step_done_streak ?? 0)
|
|
||||||
) {
|
|
||||||
applyPairedTaskPatch({
|
applyPairedTaskPatch({
|
||||||
taskId,
|
taskId,
|
||||||
expectedUpdatedAt: task.updated_at,
|
expectedUpdatedAt: task.updated_at,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
patch: {
|
patch: {
|
||||||
owner_step_done_streak: nextOwnerStepDoneStreak,
|
owner_step_done_streak: nextOwnerStepDoneStreak,
|
||||||
empty_step_done_streak: nextEmptyStepDoneStreak,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ describe('paired execution context shared verdict helpers', () => {
|
|||||||
visibleVerdict: 'step_done',
|
visibleVerdict: 'step_done',
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
kind: 'request_owner_continue',
|
kind: 'request_reviewer',
|
||||||
resetStatusToActive: false,
|
resetStatusToActive: false,
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
@@ -57,7 +57,7 @@ describe('paired execution context shared verdict helpers', () => {
|
|||||||
deadlockThreshold: 2,
|
deadlockThreshold: 2,
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
kind: 'request_owner_continue',
|
kind: 'request_reviewer',
|
||||||
resetStatusToActive: true,
|
resetStatusToActive: true,
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type { PairedTaskStatus } from './types.js';
|
|||||||
export type CompletionSignal =
|
export type CompletionSignal =
|
||||||
| { kind: 'request_reviewer'; resetStatusToActive: boolean }
|
| { kind: 'request_reviewer'; resetStatusToActive: boolean }
|
||||||
| { kind: 'request_owner_finalize' }
|
| { kind: 'request_owner_finalize' }
|
||||||
| { kind: 'request_owner_continue'; resetStatusToActive: boolean }
|
|
||||||
| { kind: 'request_owner_changes' }
|
| { kind: 'request_owner_changes' }
|
||||||
| { kind: 'request_arbiter' }
|
| { kind: 'request_arbiter' }
|
||||||
| { kind: 'complete'; completionReason: 'done' | 'escalated' }
|
| { kind: 'complete'; completionReason: 'done' | 'escalated' }
|
||||||
@@ -38,12 +37,6 @@ export function resolveOwnerCompletionSignal(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (phase === 'normal') {
|
if (phase === 'normal') {
|
||||||
if (visibleVerdict === 'step_done') {
|
|
||||||
return {
|
|
||||||
kind: 'request_owner_continue',
|
|
||||||
resetStatusToActive: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
kind: 'request_reviewer',
|
kind: 'request_reviewer',
|
||||||
resetStatusToActive: false,
|
resetStatusToActive: false,
|
||||||
@@ -52,7 +45,7 @@ export function resolveOwnerCompletionSignal(args: {
|
|||||||
|
|
||||||
if (visibleVerdict === 'step_done') {
|
if (visibleVerdict === 'step_done') {
|
||||||
return {
|
return {
|
||||||
kind: 'request_owner_continue',
|
kind: 'request_reviewer',
|
||||||
resetStatusToActive: true,
|
resetStatusToActive: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -802,13 +802,22 @@ describe('paired execution context', () => {
|
|||||||
).not.toHaveBeenCalled();
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps an active task in owner follow-up mode when the owner reports STEP_DONE', () => {
|
it('requests review when the owner reports STEP_DONE in active mode', () => {
|
||||||
vi.mocked(db.getPairedTaskById).mockReturnValue(
|
vi.mocked(db.getPairedTaskById).mockReturnValue(
|
||||||
buildPairedTask({
|
buildPairedTask({
|
||||||
status: 'active',
|
status: 'active',
|
||||||
round_trip_count: 1,
|
round_trip_count: 1,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
vi.mocked(pairedWorkspaceManager.markPairedTaskReviewReady).mockReturnValue(
|
||||||
|
{
|
||||||
|
ownerWorkspace: buildWorkspace('owner', '/tmp/paired/task-1/owner'),
|
||||||
|
reviewerWorkspace: buildWorkspace(
|
||||||
|
'reviewer',
|
||||||
|
'/tmp/paired/task-1/reviewer',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
completePairedExecutionContext({
|
completePairedExecutionContext({
|
||||||
taskId: 'task-1',
|
taskId: 'task-1',
|
||||||
@@ -820,22 +829,17 @@ describe('paired execution context', () => {
|
|||||||
expect(db.updatePairedTask).toHaveBeenCalledWith(
|
expect(db.updatePairedTask).toHaveBeenCalledWith(
|
||||||
'task-1',
|
'task-1',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
owner_failure_count: 0,
|
round_trip_count: 2,
|
||||||
owner_step_done_streak: 1,
|
owner_step_done_streak: 0,
|
||||||
|
empty_step_done_streak: 0,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
pairedWorkspaceManager.markPairedTaskReviewReady,
|
pairedWorkspaceManager.markPairedTaskReviewReady,
|
||||||
).not.toHaveBeenCalled();
|
).toHaveBeenCalledWith('task-1');
|
||||||
expect(db.updatePairedTask).not.toHaveBeenCalledWith(
|
|
||||||
'task-1',
|
|
||||||
expect.objectContaining({
|
|
||||||
status: 'active',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('requests arbiter when active STEP_DONE repeats without code changes', () => {
|
it('requests review instead of arbiter when active STEP_DONE repeats without code changes', () => {
|
||||||
vi.spyOn(config, 'isArbiterEnabled').mockReturnValue(true);
|
vi.spyOn(config, 'isArbiterEnabled').mockReturnValue(true);
|
||||||
|
|
||||||
const repoDir = createCanonicalRepoWithCommit('active step done loop');
|
const repoDir = createCanonicalRepoWithCommit('active step done loop');
|
||||||
@@ -851,6 +855,15 @@ describe('paired execution context', () => {
|
|||||||
vi.mocked(db.getPairedWorkspace).mockImplementation((_taskId, role) =>
|
vi.mocked(db.getPairedWorkspace).mockImplementation((_taskId, role) =>
|
||||||
role === 'owner' ? buildWorkspace('owner', repoDir) : undefined,
|
role === 'owner' ? buildWorkspace('owner', repoDir) : undefined,
|
||||||
);
|
);
|
||||||
|
vi.mocked(pairedWorkspaceManager.markPairedTaskReviewReady).mockReturnValue(
|
||||||
|
{
|
||||||
|
ownerWorkspace: buildWorkspace('owner', repoDir),
|
||||||
|
reviewerWorkspace: buildWorkspace(
|
||||||
|
'reviewer',
|
||||||
|
'/tmp/paired/task-1/reviewer',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
completePairedExecutionContext({
|
completePairedExecutionContext({
|
||||||
taskId: 'task-1',
|
taskId: 'task-1',
|
||||||
@@ -862,23 +875,44 @@ describe('paired execution context', () => {
|
|||||||
expect(db.updatePairedTask).toHaveBeenCalledWith(
|
expect(db.updatePairedTask).toHaveBeenCalledWith(
|
||||||
'task-1',
|
'task-1',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
status: 'arbiter_requested',
|
round_trip_count: 1,
|
||||||
owner_step_done_streak: 3,
|
owner_step_done_streak: 0,
|
||||||
empty_step_done_streak: 3,
|
empty_step_done_streak: 0,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
pairedWorkspaceManager.markPairedTaskReviewReady,
|
pairedWorkspaceManager.markPairedTaskReviewReady,
|
||||||
).not.toHaveBeenCalled();
|
).toHaveBeenCalledWith('task-1');
|
||||||
|
expect(db.updatePairedTask).not.toHaveBeenCalledWith(
|
||||||
|
'task-1',
|
||||||
|
expect.objectContaining({
|
||||||
|
status: 'arbiter_requested',
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns merge_ready owner finalize output to active when the owner reports STEP_DONE', () => {
|
it('re-triggers review when the owner reports STEP_DONE during finalize', () => {
|
||||||
|
const repoDir = createCanonicalRepoWithCommit('reviewed');
|
||||||
|
const approvedSourceRef = resolveTreeRef(repoDir);
|
||||||
vi.mocked(db.getPairedTaskById).mockReturnValue(
|
vi.mocked(db.getPairedTaskById).mockReturnValue(
|
||||||
buildPairedTask({
|
buildPairedTask({
|
||||||
status: 'merge_ready',
|
status: 'merge_ready',
|
||||||
|
source_ref: approvedSourceRef,
|
||||||
round_trip_count: 1,
|
round_trip_count: 1,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
vi.mocked(db.getPairedWorkspace).mockImplementation((_taskId, role) =>
|
||||||
|
role === 'owner' ? buildWorkspace('owner', repoDir) : undefined,
|
||||||
|
);
|
||||||
|
vi.mocked(pairedWorkspaceManager.markPairedTaskReviewReady).mockReturnValue(
|
||||||
|
{
|
||||||
|
ownerWorkspace: buildWorkspace('owner', repoDir),
|
||||||
|
reviewerWorkspace: buildWorkspace(
|
||||||
|
'reviewer',
|
||||||
|
'/tmp/paired/task-1/reviewer',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
completePairedExecutionContext({
|
completePairedExecutionContext({
|
||||||
taskId: 'task-1',
|
taskId: 'task-1',
|
||||||
@@ -893,11 +927,20 @@ describe('paired execution context', () => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
owner_failure_count: 0,
|
owner_failure_count: 0,
|
||||||
finalize_step_done_count: 1,
|
finalize_step_done_count: 1,
|
||||||
|
empty_step_done_streak: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(db.updatePairedTask).toHaveBeenCalledWith(
|
||||||
|
'task-1',
|
||||||
|
expect.objectContaining({
|
||||||
|
round_trip_count: 2,
|
||||||
|
finalize_step_done_count: 1,
|
||||||
|
empty_step_done_streak: 1,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
pairedWorkspaceManager.markPairedTaskReviewReady,
|
pairedWorkspaceManager.markPairedTaskReviewReady,
|
||||||
).not.toHaveBeenCalled();
|
).toHaveBeenCalledWith('task-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('requests arbiter when finalize STEP_DONE repeats without code changes', () => {
|
it('requests arbiter when finalize STEP_DONE repeats without code changes', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user