Add STEP_DONE guards, verdict storage, and stale delivery suppression

This commit is contained in:
ejclaw
2026-04-23 15:27:49 +09:00
parent afd9c7a538
commit 9ef79e3828
18 changed files with 767 additions and 64 deletions

View File

@@ -143,6 +143,10 @@ function buildPairedTask(overrides: Partial<PairedTask> = {}): PairedTask {
review_requested_at: null,
round_trip_count: 0,
owner_failure_count: 0,
owner_step_done_streak: 0,
finalize_step_done_count: 0,
task_done_then_user_reopen_count: 0,
empty_step_done_streak: 0,
status: 'active',
arbiter_verdict: null,
arbiter_requested_at: null,
@@ -293,6 +297,62 @@ describe('paired execution context', () => {
expect(db.insertPairedTurnOutput).not.toHaveBeenCalled();
});
it('records a quick reopen when a new owner task starts shortly after TASK_DONE completion', () => {
const previousTask = buildPairedTask({
id: 'task-completed',
status: 'completed',
completion_reason: 'done',
updated_at: new Date(Date.now() - 60_000).toISOString(),
task_done_then_user_reopen_count: 0,
});
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue(undefined);
vi.mocked(db.getLatestPairedTaskForChat).mockReturnValue(previousTask);
const result = resolveOwnerTaskForHumanMessage({
group,
chatJid: 'dc:test',
roomRoleContext: ownerContext,
existingTask: null,
});
expect(result.task).not.toBeNull();
expect(db.updatePairedTask).toHaveBeenCalledWith(
'task-completed',
expect.objectContaining({
task_done_then_user_reopen_count: 1,
}),
);
});
it('resets active STEP_DONE loop counters when a new human message continues an active task', () => {
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue(
buildPairedTask({
status: 'active',
owner_failure_count: 1,
owner_step_done_streak: 2,
empty_step_done_streak: 2,
}),
);
preparePairedExecutionContext({
group,
chatJid: 'dc:test',
runId: 'run-human-reset-step-done-loop',
roomRoleContext: ownerContext,
hasHumanMessage: true,
});
expect(db.updatePairedTask).toHaveBeenCalledWith(
'task-1',
expect.objectContaining({
round_trip_count: 0,
owner_failure_count: 0,
owner_step_done_streak: 0,
empty_step_done_streak: 0,
}),
);
});
it('uses room role context agent overrides when creating a paired task', () => {
preparePairedExecutionContext({
group,
@@ -761,6 +821,7 @@ describe('paired execution context', () => {
'task-1',
expect.objectContaining({
owner_failure_count: 0,
owner_step_done_streak: 1,
}),
);
expect(
@@ -774,6 +835,43 @@ describe('paired execution context', () => {
);
});
it('requests arbiter when active STEP_DONE repeats without code changes', () => {
vi.spyOn(config, 'isArbiterEnabled').mockReturnValue(true);
const repoDir = createCanonicalRepoWithCommit('active step done loop');
const sourceRef = resolveTreeRef(repoDir);
vi.mocked(db.getPairedTaskById).mockReturnValue(
buildPairedTask({
status: 'active',
source_ref: sourceRef,
owner_step_done_streak: 2,
empty_step_done_streak: 2,
}),
);
vi.mocked(db.getPairedWorkspace).mockImplementation((_taskId, role) =>
role === 'owner' ? buildWorkspace('owner', repoDir) : undefined,
);
completePairedExecutionContext({
taskId: 'task-1',
role: 'owner',
status: 'succeeded',
summary: 'STEP_DONE\n요약만 반복되고 코드 변경은 없음',
});
expect(db.updatePairedTask).toHaveBeenCalledWith(
'task-1',
expect.objectContaining({
status: 'arbiter_requested',
owner_step_done_streak: 3,
empty_step_done_streak: 3,
}),
);
expect(
pairedWorkspaceManager.markPairedTaskReviewReady,
).not.toHaveBeenCalled();
});
it('returns merge_ready owner finalize output to active when the owner reports STEP_DONE', () => {
vi.mocked(db.getPairedTaskById).mockReturnValue(
buildPairedTask({
@@ -794,6 +892,44 @@ describe('paired execution context', () => {
expect.objectContaining({
status: 'active',
owner_failure_count: 0,
finalize_step_done_count: 1,
}),
);
expect(
pairedWorkspaceManager.markPairedTaskReviewReady,
).not.toHaveBeenCalled();
});
it('requests arbiter when finalize STEP_DONE repeats without code changes', () => {
vi.spyOn(config, 'isArbiterEnabled').mockReturnValue(true);
const repoDir = createCanonicalRepoWithCommit('reviewed');
const approvedSourceRef = resolveTreeRef(repoDir);
vi.mocked(db.getPairedTaskById).mockReturnValue(
buildPairedTask({
status: 'merge_ready',
source_ref: approvedSourceRef,
empty_step_done_streak: 1,
finalize_step_done_count: 1,
}),
);
vi.mocked(db.getPairedWorkspace).mockImplementation((_taskId, role) =>
role === 'owner' ? buildWorkspace('owner', repoDir) : undefined,
);
completePairedExecutionContext({
taskId: 'task-1',
role: 'owner',
status: 'succeeded',
summary: 'STEP_DONE\n아직 남았지만 코드 변경은 없음',
});
expect(db.updatePairedTask).toHaveBeenCalledWith(
'task-1',
expect.objectContaining({
status: 'arbiter_requested',
empty_step_done_streak: 2,
finalize_step_done_count: 2,
}),
);
expect(