136 lines
5.1 KiB
TypeScript
136 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
decideOpenCodePromptDeliveryRepair,
|
|
type OpenCodePromptDeliveryRepairInput,
|
|
} from '@main/services/team/opencode/delivery/OpenCodePromptDeliveryRepairPolicy';
|
|
|
|
function base(overrides: Partial<OpenCodePromptDeliveryRepairInput> = {}) {
|
|
return {
|
|
teamName: 'team-a',
|
|
memberName: 'alice',
|
|
inboxMessageId: 'msg-1',
|
|
replyRecipient: 'user',
|
|
messageKind: 'default',
|
|
actionMode: 'ask',
|
|
taskRefs: [],
|
|
status: 'responded',
|
|
responseState: 'empty_assistant_turn',
|
|
attempts: 1,
|
|
maxAttempts: 3,
|
|
pendingReason: 'empty_assistant_turn',
|
|
readAllowed: false,
|
|
inboxReadCommitted: false,
|
|
visibleReplyFound: false,
|
|
hasKnownProgressProof: false,
|
|
toolCallNames: [],
|
|
acceptanceUnknown: false,
|
|
hardFailureKind: 'none',
|
|
...overrides,
|
|
} satisfies OpenCodePromptDeliveryRepairInput;
|
|
}
|
|
|
|
describe('OpenCodePromptDeliveryRepairPolicy', () => {
|
|
it('adds no-assistant response repair without treating it as success', () => {
|
|
const decision = decideOpenCodePromptDeliveryRepair(base());
|
|
|
|
expect(decision.kind).toBe('no_assistant_response');
|
|
expect(decision.retryable).toBe(true);
|
|
expect(decision.controlText).toContain('You must not end this turn empty.');
|
|
expect(decision.controlText).toContain('relayOfMessageId="msg-1"');
|
|
});
|
|
|
|
it('requires member work sync status and report for work-sync nudges', () => {
|
|
const decision = decideOpenCodePromptDeliveryRepair(
|
|
base({
|
|
messageKind: 'member_work_sync_nudge',
|
|
actionMode: 'do',
|
|
taskRefs: [{ taskId: 'task-1', displayId: '#1', teamName: 'team-a' }],
|
|
responseState: 'responded_plain_text',
|
|
pendingReason: 'plain_text_ack_only_still_requires_answer',
|
|
controlUrl: 'http://127.0.0.1:43123',
|
|
})
|
|
);
|
|
|
|
expect(decision.kind).toBe('work_sync_report_required');
|
|
expect(decision.controlText).toContain('member_work_sync_status');
|
|
expect(decision.controlText).toContain('member_work_sync_report');
|
|
expect(decision.controlText).toContain('controlUrl="http://127.0.0.1:43123"');
|
|
expect(decision.controlText).toContain('"task-1"');
|
|
expect(decision.controlText).not.toContain('reportToken=');
|
|
});
|
|
|
|
it('uses review pickup repair wording for review pickup work-sync nudges', () => {
|
|
const decision = decideOpenCodePromptDeliveryRepair(
|
|
base({
|
|
messageKind: 'member_work_sync_nudge',
|
|
workSyncIntent: 'review_pickup',
|
|
actionMode: 'do',
|
|
taskRefs: [{ taskId: 'task-1', displayId: '#1', teamName: 'team-a' }],
|
|
responseState: 'responded_plain_text',
|
|
pendingReason: 'plain_text_ack_only_still_requires_answer',
|
|
})
|
|
);
|
|
|
|
expect(decision.kind).toBe('work_sync_report_required');
|
|
expect(decision.controlText).toContain('review pickup control message');
|
|
expect(decision.controlText).toContain('start or continue the review');
|
|
expect(decision.controlText).toContain('"task-1"');
|
|
expect(decision.controlText).not.toContain('Then call agent-teams_member_work_sync_report');
|
|
});
|
|
|
|
it('repairs visible replies that missed required taskRefs with exact metadata', () => {
|
|
const taskRef = { taskId: 'task-refs-1', displayId: 'refs-1', teamName: 'team-a' };
|
|
const decision = decideOpenCodePromptDeliveryRepair(
|
|
base({
|
|
taskRefs: [taskRef],
|
|
responseState: 'responded_visible_message',
|
|
pendingReason: 'visible_reply_missing_task_refs',
|
|
visibleReplyFound: true,
|
|
})
|
|
);
|
|
|
|
expect(decision.kind).toBe('missing_visible_reply_correlation');
|
|
expect(decision.retryable).toBe(true);
|
|
expect(decision.controlText).toContain('relayOfMessageId="msg-1"');
|
|
expect(decision.controlText).toContain(
|
|
`Include taskRefs exactly as this JSON array: ${JSON.stringify([taskRef])}.`
|
|
);
|
|
});
|
|
|
|
it('does not repair terminal, permission, or session failures', () => {
|
|
expect(
|
|
decideOpenCodePromptDeliveryRepair(
|
|
base({ status: 'failed_terminal', responseState: 'empty_assistant_turn' })
|
|
)
|
|
).toMatchObject({ kind: 'none', retryable: false });
|
|
|
|
expect(
|
|
decideOpenCodePromptDeliveryRepair(
|
|
base({ responseState: 'permission_blocked', hardFailureKind: 'permission' })
|
|
)
|
|
).toMatchObject({ kind: 'none', retryable: false });
|
|
|
|
expect(
|
|
decideOpenCodePromptDeliveryRepair(
|
|
base({ responseState: 'session_error', hardFailureKind: 'session' })
|
|
)
|
|
).toMatchObject({ kind: 'none', retryable: false });
|
|
});
|
|
|
|
it('does not ask to repeat side-effect tools after tool_error', () => {
|
|
const decision = decideOpenCodePromptDeliveryRepair(
|
|
base({
|
|
responseState: 'tool_error',
|
|
pendingReason: 'tool_error_without_required_delivery_proof',
|
|
toolCallNames: ['bash'],
|
|
actionMode: 'do',
|
|
taskRefs: [{ taskId: 'task-2', displayId: '#2', teamName: 'team-a' }],
|
|
})
|
|
);
|
|
|
|
expect(decision.kind).toBe('progress_proof_required');
|
|
expect(decision.controlText).toContain('Do not repeat side-effectful commands');
|
|
expect(decision.controlText).toContain('"task-2"');
|
|
});
|
|
});
|