The reviewer step_done verdict mapped to request_owner_changes unconditionally, bypassing the deadlock guard that every sibling verdict respects. When the owner kept replying task_done and the reviewer kept replying step_done, the room oscillated forever, spamming duplicate reviewer messages into the channel. Apply the same `roundTripCount >= deadlockThreshold` guard used by the done_with_concerns/continue/default branch so step_done oscillation escalates to the arbiter (or completes via escalation when no arbiter) instead of looping. Reuses existing round_trip_count and deadlockThreshold; no migration needed. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
212 lines
6.4 KiB
TypeScript
212 lines
6.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
parseVisibleVerdict,
|
|
isUserInputWaitVerdict,
|
|
parseReviewerVisibleVerdict,
|
|
resolveOwnerCompletionSignal,
|
|
resolveReviewerCompletionSignal,
|
|
resolveReviewerFailureSignal,
|
|
} from './paired-execution-context-shared.js';
|
|
|
|
describe('paired execution context shared verdict helpers', () => {
|
|
it('parses visible verdicts from the first summary line only', () => {
|
|
expect(parseVisibleVerdict('STEP_DONE\nmore to do')).toBe('step_done');
|
|
expect(parseVisibleVerdict('TASK_DONE\nall done')).toBe('task_done');
|
|
expect(parseVisibleVerdict('APPROVE\nlooks good')).toBe('task_done');
|
|
expect(parseVisibleVerdict('APPROVED\nlooks good')).toBe('task_done');
|
|
expect(
|
|
parseVisibleVerdict(
|
|
'DONE_WITH_CONCERNS\n\nfollow-up detail that should not affect parsing',
|
|
),
|
|
).toBe('done_with_concerns');
|
|
expect(parseVisibleVerdict('BLOCKED\nextra detail')).toBe('blocked');
|
|
expect(parseVisibleVerdict('random prose')).toBe('continue');
|
|
expect(parseVisibleVerdict('PROCEED — arbiter-style text')).toBe(
|
|
'continue',
|
|
);
|
|
expect(
|
|
parseReviewerVisibleVerdict('PROCEED — review approved, no work left'),
|
|
).toBe('task_done');
|
|
});
|
|
|
|
it('detects STEP_DONE summaries that are only waiting for user input', () => {
|
|
expect(
|
|
isUserInputWaitVerdict(
|
|
'STEP_DONE — 대기 유지. whoami 결과를 보내주면 바로 SSH 로그인 테스트합니다.',
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
isUserInputWaitVerdict(
|
|
'STEP_DONE\n키 등록 끝나면 알려주세요. 그 전까지 더 안 보냅니다.',
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
isUserInputWaitVerdict('STEP_DONE\n1단계 완료, 다음 단계 진행합니다.'),
|
|
).toBe(false);
|
|
expect(
|
|
isUserInputWaitVerdict('TASK_DONE\n사용자 입력 대기 없이 완료'),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('maps normal owner completion verdicts to reviewer or arbiter signals', () => {
|
|
expect(
|
|
resolveOwnerCompletionSignal({
|
|
phase: 'normal',
|
|
visibleVerdict: 'step_done',
|
|
}),
|
|
).toEqual({
|
|
kind: 'request_reviewer',
|
|
resetStatusToActive: false,
|
|
});
|
|
expect(
|
|
resolveOwnerCompletionSignal({
|
|
phase: 'normal',
|
|
visibleVerdict: 'done',
|
|
}),
|
|
).toEqual({
|
|
kind: 'request_reviewer',
|
|
resetStatusToActive: false,
|
|
});
|
|
expect(
|
|
resolveOwnerCompletionSignal({
|
|
phase: 'normal',
|
|
visibleVerdict: 'blocked',
|
|
}),
|
|
).toEqual({ kind: 'request_arbiter' });
|
|
});
|
|
|
|
it('maps finalize owner outcomes to complete, re-review, or arbiter', () => {
|
|
expect(
|
|
resolveOwnerCompletionSignal({
|
|
phase: 'finalize',
|
|
visibleVerdict: 'step_done',
|
|
hasChangesSinceApproval: false,
|
|
roundTripCount: 0,
|
|
deadlockThreshold: 2,
|
|
}),
|
|
).toEqual({
|
|
kind: 'request_reviewer',
|
|
resetStatusToActive: true,
|
|
});
|
|
expect(
|
|
resolveOwnerCompletionSignal({
|
|
phase: 'finalize',
|
|
visibleVerdict: 'done',
|
|
hasChangesSinceApproval: false,
|
|
roundTripCount: 0,
|
|
deadlockThreshold: 2,
|
|
}),
|
|
).toEqual({ kind: 'complete', completionReason: 'done' });
|
|
expect(
|
|
resolveOwnerCompletionSignal({
|
|
phase: 'finalize',
|
|
visibleVerdict: 'done_with_concerns',
|
|
hasChangesSinceApproval: false,
|
|
roundTripCount: 1,
|
|
deadlockThreshold: 3,
|
|
}),
|
|
).toEqual({
|
|
kind: 'request_reviewer',
|
|
resetStatusToActive: true,
|
|
});
|
|
expect(
|
|
resolveOwnerCompletionSignal({
|
|
phase: 'finalize',
|
|
visibleVerdict: 'done',
|
|
hasChangesSinceApproval: true,
|
|
roundTripCount: 3,
|
|
deadlockThreshold: 3,
|
|
}),
|
|
).toEqual({ kind: 'request_arbiter' });
|
|
});
|
|
|
|
it('maps reviewer completion verdicts to finalize, owner changes, or arbiter', () => {
|
|
expect(
|
|
resolveReviewerCompletionSignal({
|
|
visibleVerdict: parseVisibleVerdict('APPROVE\nverified'),
|
|
roundTripCount: 2,
|
|
deadlockThreshold: 2,
|
|
}),
|
|
).toEqual({ kind: 'request_owner_finalize' });
|
|
expect(
|
|
resolveReviewerCompletionSignal({
|
|
visibleVerdict: 'task_done',
|
|
roundTripCount: 0,
|
|
deadlockThreshold: 3,
|
|
}),
|
|
).toEqual({ kind: 'request_owner_finalize' });
|
|
expect(
|
|
resolveReviewerCompletionSignal({
|
|
visibleVerdict: 'step_done',
|
|
roundTripCount: 0,
|
|
deadlockThreshold: 3,
|
|
}),
|
|
).toEqual({ kind: 'request_owner_changes' });
|
|
expect(
|
|
resolveReviewerCompletionSignal({
|
|
visibleVerdict: 'done',
|
|
roundTripCount: 0,
|
|
deadlockThreshold: 3,
|
|
}),
|
|
).toEqual({ kind: 'request_owner_finalize' });
|
|
expect(
|
|
resolveReviewerCompletionSignal({
|
|
visibleVerdict: 'continue',
|
|
roundTripCount: 1,
|
|
deadlockThreshold: 3,
|
|
}),
|
|
).toEqual({ kind: 'request_owner_changes' });
|
|
expect(
|
|
resolveReviewerCompletionSignal({
|
|
visibleVerdict: 'done_with_concerns',
|
|
roundTripCount: 3,
|
|
deadlockThreshold: 3,
|
|
}),
|
|
).toEqual({ kind: 'request_arbiter' });
|
|
});
|
|
|
|
it('routes reviewer step_done to arbiter once round trips hit the deadlock threshold', () => {
|
|
// Below threshold: keep iterating with the owner.
|
|
expect(
|
|
resolveReviewerCompletionSignal({
|
|
visibleVerdict: 'step_done',
|
|
roundTripCount: 1,
|
|
deadlockThreshold: 2,
|
|
}),
|
|
).toEqual({ kind: 'request_owner_changes' });
|
|
// At/above threshold: break the owner⇄reviewer step_done oscillation by
|
|
// escalating to the arbiter instead of looping forever.
|
|
expect(
|
|
resolveReviewerCompletionSignal({
|
|
visibleVerdict: 'step_done',
|
|
roundTripCount: 2,
|
|
deadlockThreshold: 2,
|
|
}),
|
|
).toEqual({ kind: 'request_arbiter' });
|
|
});
|
|
|
|
it('maps reviewer failure verdicts to explicit failure signals', () => {
|
|
expect(
|
|
resolveReviewerFailureSignal({
|
|
visibleVerdict: 'task_done',
|
|
}),
|
|
).toEqual({ kind: 'request_owner_finalize' });
|
|
expect(
|
|
resolveReviewerFailureSignal({
|
|
visibleVerdict: 'done',
|
|
}),
|
|
).toEqual({ kind: 'request_owner_finalize' });
|
|
expect(
|
|
resolveReviewerFailureSignal({
|
|
visibleVerdict: 'needs_context',
|
|
}),
|
|
).toEqual({ kind: 'complete', completionReason: 'escalated' });
|
|
expect(
|
|
resolveReviewerFailureSignal({
|
|
visibleVerdict: 'continue',
|
|
}),
|
|
).toEqual({ kind: 'preserve_review_ready' });
|
|
});
|
|
});
|