mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
feat: add approval checkpoints backend
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import {
|
||||
approvalPolicyRequiresCheckpoint,
|
||||
normalizeApprovalPolicy,
|
||||
normalizePendingApproval,
|
||||
} from '@shared/domain/approval';
|
||||
|
||||
describe('approval helpers', () => {
|
||||
test('normalizes duplicate checkpoint rules into stable agent-scoped policy entries', () => {
|
||||
expect(normalizeApprovalPolicy({
|
||||
rules: [
|
||||
{ kind: 'tool-call', agentIds: ['agent-1', ' agent-1 ', 'agent-2'] },
|
||||
{ kind: 'tool-call', agentIds: ['agent-2', 'agent-3'] },
|
||||
{ kind: 'final-response', agentIds: [] },
|
||||
],
|
||||
})).toEqual({
|
||||
rules: [
|
||||
{ kind: 'tool-call', agentIds: ['agent-1', 'agent-2', 'agent-3'] },
|
||||
{ kind: 'final-response' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('matches approval requirements for all-agent and agent-specific rules', () => {
|
||||
const policy = normalizeApprovalPolicy({
|
||||
rules: [
|
||||
{ kind: 'tool-call', agentIds: ['agent-1'] },
|
||||
{ kind: 'final-response', agentIds: [] },
|
||||
],
|
||||
});
|
||||
|
||||
expect(approvalPolicyRequiresCheckpoint(policy, 'tool-call', 'agent-1')).toBe(true);
|
||||
expect(approvalPolicyRequiresCheckpoint(policy, 'tool-call', 'agent-2')).toBe(false);
|
||||
expect(approvalPolicyRequiresCheckpoint(policy, 'final-response', 'agent-2')).toBe(true);
|
||||
});
|
||||
|
||||
test('normalizes pending approvals with optional message previews', () => {
|
||||
expect(normalizePendingApproval({
|
||||
id: 'approval-1',
|
||||
kind: 'final-response',
|
||||
status: 'pending',
|
||||
requestedAt: '2026-03-24T10:00:00.000Z',
|
||||
title: 'Approve final response',
|
||||
messages: [
|
||||
{
|
||||
id: 'msg-1',
|
||||
authorName: 'Primary Agent',
|
||||
content: 'Draft answer',
|
||||
},
|
||||
],
|
||||
})).toEqual({
|
||||
id: 'approval-1',
|
||||
kind: 'final-response',
|
||||
status: 'pending',
|
||||
requestedAt: '2026-03-24T10:00:00.000Z',
|
||||
title: 'Approve final response',
|
||||
messages: [
|
||||
{
|
||||
id: 'msg-1',
|
||||
authorName: 'Primary Agent',
|
||||
content: 'Draft answer',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -101,4 +101,28 @@ describe('pattern validation', () => {
|
||||
expect(groupChat?.agents[1].instructions).toContain('specific improvements');
|
||||
expect(groupChat?.agents[1].instructions).toContain('instead of restarting the conversation');
|
||||
});
|
||||
|
||||
test('approval policy rejects unknown agent references', () => {
|
||||
const singlePattern = createBuiltinPatterns(BUILTIN_TIMESTAMP).find(
|
||||
(pattern) => pattern.mode === 'single',
|
||||
);
|
||||
|
||||
expect(singlePattern).toBeDefined();
|
||||
|
||||
const issues = validatePatternDefinition({
|
||||
...singlePattern!,
|
||||
approvalPolicy: {
|
||||
rules: [
|
||||
{
|
||||
kind: 'tool-call',
|
||||
agentIds: ['agent-missing'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(issues.find((issue) => issue.field === 'approvalPolicy')?.message).toBe(
|
||||
'Approval checkpoint "tool-call" references unknown agent "agent-missing".',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
completeSessionRunRecord,
|
||||
createSessionRunRecord,
|
||||
normalizeSessionRunRecords,
|
||||
upsertRunApprovalEvent,
|
||||
upsertRunMessageEvent,
|
||||
} from '@shared/domain/runTimeline';
|
||||
import type { ProjectRecord } from '@shared/domain/project';
|
||||
import type { PendingApprovalRecord } from '@shared/domain/approval';
|
||||
|
||||
function createPattern(): PatternDefinition {
|
||||
return {
|
||||
@@ -168,4 +170,44 @@ describe('run timeline helpers', () => {
|
||||
test('normalizes missing run collections to an empty array', () => {
|
||||
expect(normalizeSessionRunRecords(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
test('tracks approval checkpoints as a single timeline event that can be resolved later', () => {
|
||||
const baseRun = createSessionRunRecord({
|
||||
requestId: 'turn-1',
|
||||
project: createProject(),
|
||||
workspaceKind: 'project',
|
||||
pattern: createPattern(),
|
||||
triggerMessageId: 'msg-user-1',
|
||||
startedAt: '2026-03-23T00:00:01.000Z',
|
||||
});
|
||||
|
||||
const pendingApproval: PendingApprovalRecord = {
|
||||
id: 'approval-1',
|
||||
kind: 'tool-call',
|
||||
status: 'pending',
|
||||
requestedAt: '2026-03-23T00:00:02.000Z',
|
||||
agentId: 'agent-writer',
|
||||
agentName: 'Writer',
|
||||
title: 'Approve tool access',
|
||||
permissionKind: 'tool access',
|
||||
};
|
||||
|
||||
const pendingRun = upsertRunApprovalEvent(baseRun, pendingApproval);
|
||||
const resolvedRun = upsertRunApprovalEvent(pendingRun, {
|
||||
...pendingApproval,
|
||||
status: 'approved',
|
||||
resolvedAt: '2026-03-23T00:00:03.000Z',
|
||||
});
|
||||
|
||||
const approvalEvent = resolvedRun.events.find((event) => event.kind === 'approval');
|
||||
expect(approvalEvent).toMatchObject({
|
||||
approvalId: 'approval-1',
|
||||
approvalKind: 'tool-call',
|
||||
approvalTitle: 'Approve tool access',
|
||||
permissionKind: 'tool access',
|
||||
status: 'completed',
|
||||
decision: 'approved',
|
||||
updatedAt: '2026-03-23T00:00:03.000Z',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,6 +122,13 @@ describe('session library helpers', () => {
|
||||
isPinned: true,
|
||||
isArchived: true,
|
||||
lastError: 'sidecar crashed',
|
||||
pendingApproval: {
|
||||
id: 'approval-1',
|
||||
kind: 'tool-call',
|
||||
status: 'pending',
|
||||
requestedAt: '2026-03-23T00:01:00.000Z',
|
||||
title: 'Approve tool access',
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
id: 'msg-1',
|
||||
@@ -149,6 +156,7 @@ describe('session library helpers', () => {
|
||||
updatedAt: '2026-03-23T00:07:00.000Z',
|
||||
});
|
||||
expect(session.messages[0]?.pending).toBe(false);
|
||||
expect(session.pendingApproval).toBeUndefined();
|
||||
expect(session.runs).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user