feat: add paired execution freshness guard

This commit is contained in:
Eyejoker
2026-03-29 04:37:42 +09:00
parent 03ff69fdd0
commit bb30330c66
5 changed files with 500 additions and 14 deletions

View File

@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('./db.js', () => ({
applyPairedEvent: vi.fn(),
cancelSupersededPairedExecutions: vi.fn(() => 0),
createPairedArtifact: vi.fn(),
createPairedExecution: vi.fn(),
createPairedTask: vi.fn(),
@@ -11,6 +12,7 @@ vi.mock('./db.js', () => ({
getPairedWorkspace: vi.fn(),
listPairedArtifactsForTask: vi.fn(),
listPairedEventsForTask: vi.fn(),
listPairedExecutionsForTask: vi.fn(),
updatePairedExecution: vi.fn(),
updatePairedTask: vi.fn(),
upsertPairedProject: vi.fn(),
@@ -79,6 +81,7 @@ async function importExecutionContextForService(serviceId: string) {
const dbModule = {
applyPairedEvent: vi.fn(),
cancelSupersededPairedExecutions: vi.fn(() => 0),
createPairedArtifact: vi.fn(),
createPairedExecution: vi.fn(),
createPairedTask: vi.fn(),
@@ -88,6 +91,7 @@ async function importExecutionContextForService(serviceId: string) {
getPairedWorkspace: vi.fn(),
listPairedArtifactsForTask: vi.fn(),
listPairedEventsForTask: vi.fn(),
listPairedExecutionsForTask: vi.fn(),
updatePairedExecution: vi.fn(),
updatePairedTask: vi.fn(),
upsertPairedProject: vi.fn(),
@@ -103,6 +107,7 @@ async function importExecutionContextForService(serviceId: string) {
dbModule.getPairedWorkspace.mockReturnValue(undefined);
dbModule.listPairedArtifactsForTask.mockReturnValue([]);
dbModule.listPairedEventsForTask.mockReturnValue([]);
dbModule.listPairedExecutionsForTask.mockReturnValue([]);
const pairedWorkspaceManagerModule = {
PLAN_REVIEW_REQUIRED_BLOCK_MESSAGE:
@@ -175,12 +180,14 @@ describe('paired execution context', () => {
event: { id: 1, ...event },
result: onApply ? onApply() : null,
}));
vi.mocked(db.cancelSupersededPairedExecutions).mockReturnValue(0);
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue(undefined);
vi.mocked(db.getPairedExecutionById).mockReturnValue(undefined);
vi.mocked(db.getPairedTaskById).mockReturnValue(undefined);
vi.mocked(db.getPairedWorkspace).mockReturnValue(undefined);
vi.mocked(db.listPairedArtifactsForTask).mockReturnValue([]);
vi.mocked(db.listPairedEventsForTask).mockReturnValue([]);
vi.mocked(db.listPairedExecutionsForTask).mockReturnValue([]);
vi.mocked(
pairedWorkspaceManager.provisionOwnerWorkspaceForPairedTask,
).mockReturnValue({
@@ -240,13 +247,63 @@ describe('paired execution context', () => {
});
});
it('auto-requests review when a low-risk owner execution completes', () => {
it('does not self-cancel when the same execution resumes', () => {
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue({
id: 'task-1',
chat_jid: 'dc:test',
group_folder: group.folder,
owner_service_id: 'codex-main',
reviewer_service_id: 'codex-review',
title: null,
source_ref: 'HEAD',
task_policy: 'autonomous',
risk_level: 'low',
plan_status: 'not_requested',
review_requested_at: null,
status: 'active',
created_at: '2026-03-28T00:00:00.000Z',
updated_at: '2026-03-28T00:00:00.000Z',
});
vi.mocked(db.getPairedExecutionById).mockReturnValue({
id: 'run-1:codex-main',
task_id: 'task-1',
service_id: 'codex-main',
role: 'owner',
workspace_id: 'task-1:owner',
checkpoint_fingerprint: 'fingerprint-1',
status: 'running',
summary: null,
created_at: '2026-03-28T00:00:00.000Z',
started_at: '2026-03-28T00:00:00.000Z',
completed_at: null,
});
preparePairedExecutionContext({
group,
chatJid: 'dc:test',
runId: 'run-1',
roomRoleContext: ownerContext,
});
expect(db.cancelSupersededPairedExecutions).toHaveBeenCalledWith({
taskId: 'task-1',
role: 'owner',
exceptExecutionId: 'run-1:codex-main',
note: 'Superseded by a newer execution for the same task and role.',
});
});
it('auto-requests review when a low-risk owner execution completes', () => {
vi.mocked(pairedWorkspaceManager.resolvePairedTaskSourceFingerprint).mockReturnValue(
'fingerprint-2',
);
vi.mocked(db.getPairedExecutionById).mockReturnValue({
id: 'run-1:codex-main',
task_id: 'task-1',
service_id: 'codex-main',
role: 'owner',
workspace_id: 'task-1:owner',
checkpoint_fingerprint: 'fingerprint-1',
status: 'running',
summary: null,
created_at: '2026-03-28T00:00:00.000Z',
@@ -331,8 +388,8 @@ describe('paired execution context', () => {
event: expect.objectContaining({
task_id: 'task-1',
event_type: 'request_review',
source_fingerprint: 'fingerprint-1',
dedupe_key: 'auto-request-review:fingerprint-1',
source_fingerprint: 'fingerprint-2',
dedupe_key: 'auto-request-review:fingerprint-2',
}),
}),
);
@@ -553,6 +610,157 @@ describe('paired execution context', () => {
expect(pairedWorkspaceManager.markPairedTaskReviewReady).not.toHaveBeenCalled();
});
it('does not reopen review from a cancelled stale owner execution', () => {
vi.mocked(db.getPairedExecutionById).mockReturnValue({
id: 'run-1:codex-main',
task_id: 'task-1',
service_id: 'codex-main',
role: 'owner',
workspace_id: 'task-1:owner',
checkpoint_fingerprint: 'fingerprint-1',
status: 'cancelled',
summary: null,
created_at: '2026-03-28T00:00:00.000Z',
started_at: '2026-03-28T00:00:00.000Z',
completed_at: null,
});
completePairedExecutionContext({
executionId: 'run-1:codex-main',
status: 'succeeded',
summary: 'stale',
});
expect(db.updatePairedExecution).toHaveBeenCalledWith(
'run-1:codex-main',
expect.objectContaining({
status: 'cancelled',
}),
);
expect(db.applyPairedEvent).not.toHaveBeenCalled();
expect(pairedWorkspaceManager.markPairedTaskReviewReady).not.toHaveBeenCalled();
});
it('does not reopen review from an older owner execution when a newer owner execution already failed', () => {
vi.mocked(db.getPairedExecutionById).mockReturnValue({
id: 'run-1:codex-main',
task_id: 'task-1',
service_id: 'codex-main',
role: 'owner',
workspace_id: 'task-1:owner',
checkpoint_fingerprint: 'fingerprint-1',
status: 'running',
summary: null,
created_at: '2026-03-28T00:00:00.000Z',
started_at: '2026-03-28T00:00:00.000Z',
completed_at: null,
});
vi.mocked(db.listPairedExecutionsForTask).mockReturnValue([
{
id: 'run-1:codex-main',
task_id: 'task-1',
service_id: 'codex-main',
role: 'owner',
workspace_id: 'task-1:owner',
checkpoint_fingerprint: 'fingerprint-1',
status: 'running',
summary: null,
created_at: '2026-03-28T00:00:00.000Z',
started_at: '2026-03-28T00:00:00.000Z',
completed_at: null,
},
{
id: 'run-2:codex-main',
task_id: 'task-1',
service_id: 'codex-main',
role: 'owner',
workspace_id: 'task-1:owner',
checkpoint_fingerprint: 'fingerprint-2',
status: 'failed',
summary: 'newer',
created_at: '2026-03-28T00:01:00.000Z',
started_at: '2026-03-28T00:01:00.000Z',
completed_at: '2026-03-28T00:02:00.000Z',
},
]);
completePairedExecutionContext({
executionId: 'run-1:codex-main',
status: 'succeeded',
summary: 'stale-after-failed',
});
expect(db.updatePairedExecution).toHaveBeenCalledWith(
'run-1:codex-main',
expect.objectContaining({
status: 'succeeded',
summary: 'stale-after-failed',
}),
);
expect(db.applyPairedEvent).not.toHaveBeenCalled();
expect(pairedWorkspaceManager.markPairedTaskReviewReady).not.toHaveBeenCalled();
});
it('does not reopen review from an older owner execution when a newer owner execution already succeeded', () => {
vi.mocked(db.getPairedExecutionById).mockReturnValue({
id: 'run-1:codex-main',
task_id: 'task-1',
service_id: 'codex-main',
role: 'owner',
workspace_id: 'task-1:owner',
checkpoint_fingerprint: 'fingerprint-1',
status: 'running',
summary: null,
created_at: '2026-03-28T00:00:00.000Z',
started_at: '2026-03-28T00:00:00.000Z',
completed_at: null,
});
vi.mocked(db.listPairedExecutionsForTask).mockReturnValue([
{
id: 'run-1:codex-main',
task_id: 'task-1',
service_id: 'codex-main',
role: 'owner',
workspace_id: 'task-1:owner',
checkpoint_fingerprint: 'fingerprint-1',
status: 'running',
summary: null,
created_at: '2026-03-28T00:00:00.000Z',
started_at: '2026-03-28T00:00:00.000Z',
completed_at: null,
},
{
id: 'run-2:codex-main',
task_id: 'task-1',
service_id: 'codex-main',
role: 'owner',
workspace_id: 'task-1:owner',
checkpoint_fingerprint: 'fingerprint-2',
status: 'succeeded',
summary: 'newer',
created_at: '2026-03-28T00:01:00.000Z',
started_at: '2026-03-28T00:01:00.000Z',
completed_at: '2026-03-28T00:02:00.000Z',
},
]);
completePairedExecutionContext({
executionId: 'run-1:codex-main',
status: 'succeeded',
summary: 'stale-after-succeeded',
});
expect(db.updatePairedExecution).toHaveBeenCalledWith(
'run-1:codex-main',
expect.objectContaining({
status: 'succeeded',
summary: 'stale-after-succeeded',
}),
);
expect(db.applyPairedEvent).not.toHaveBeenCalled();
expect(pairedWorkspaceManager.markPairedTaskReviewReady).not.toHaveBeenCalled();
});
it('records a new checkpoint and returns the task to review_pending when owner changes arrive during in_review', () => {
vi.mocked(db.getPairedExecutionById).mockReturnValue({
id: 'run-1:codex-main',
@@ -621,6 +829,11 @@ describe('paired execution context', () => {
review_requested_at: expect.any(String),
}),
);
expect(db.cancelSupersededPairedExecutions).toHaveBeenCalledWith({
taskId: 'task-1',
role: 'reviewer',
note: 'Superseded by a newer review checkpoint.',
});
expect(pairedWorkspaceManager.markPairedTaskReviewReady).not.toHaveBeenCalled();
});