| 1 | import { describe, expect, it } from 'vitest';
|
| 2 | import {
|
| 3 | approveAgentSetup,
|
| 4 | getOnboardingState,
|
| 5 | requestAgentSetup,
|
| 6 | resolveInstallation,
|
| 7 | updateAgentProfile,
|
| 8 | type OnboardingKind,
|
| 9 | type OnboardingStore,
|
| 10 | } from '../packages/persistence/src/onboarding.js';
|
| 11 |
|
| 12 | class MemoryOnboardingStore implements OnboardingStore {
|
| 13 | private rows = new Map<string, { kind: OnboardingKind; recordId: string; payload: unknown }>();
|
| 14 |
|
| 15 | async list(operatorId: string) {
|
| 16 | return [...this.rows.entries()]
|
| 17 | .filter(([key]) => key.startsWith(`${operatorId}|`))
|
| 18 | .map(([, row]) => row);
|
| 19 | }
|
| 20 |
|
| 21 | async upsert(operatorId: string, kind: OnboardingKind, recordId: string, payload: unknown) {
|
| 22 | this.rows.set(`${operatorId}|${kind}|${recordId}`, { kind, recordId, payload });
|
| 23 | }
|
| 24 | }
|
| 25 |
|
| 26 | describe('agent self-onboarding', () => {
|
| 27 | it('requires owner confirmation before binding an installation', async () => {
|
| 28 | const store = new MemoryOnboardingStore();
|
| 29 | const request = await requestAgentSetup(store, 'owner-a', {
|
| 30 | clientId: 'codex-installation-a',
|
| 31 | agentName: 'Research agent',
|
| 32 | projectName: 'Market research',
|
| 33 | framework: 'Codex',
|
| 34 | capabilities: ['research'],
|
| 35 | limitations: ['No purchases'],
|
| 36 | description: 'Browses public sources',
|
| 37 | agentVersion: '2.0.0',
|
| 38 | });
|
| 39 |
|
| 40 | expect(request.status).toBe('pending');
|
| 41 | expect(request).toMatchObject({
|
| 42 | autoPublish: true,
|
| 43 | autoAcceptPolicy: 'safe_matching',
|
| 44 | autoAcceptTaskCategories: ['research'],
|
| 45 | });
|
| 46 | await expect(resolveInstallation(store, 'owner-a', 'codex-installation-a')).resolves.toEqual({
|
| 47 | status: 'unbound',
|
| 48 | });
|
| 49 |
|
| 50 | const approved = await approveAgentSetup(store, 'owner-a', request.requestId);
|
| 51 | expect(approved).toMatchObject({
|
| 52 | status: 'connected',
|
| 53 | agent: {
|
| 54 | name: 'Research agent',
|
| 55 | capabilities: ['research'],
|
| 56 | agentVersion: '2.0.0',
|
| 57 | transport: 'direct_a2a',
|
| 58 | },
|
| 59 | project: { name: 'Market research' },
|
| 60 | });
|
| 61 | await expect(
|
| 62 | resolveInstallation(store, 'owner-a', 'codex-installation-a'),
|
| 63 | ).resolves.toMatchObject({ status: 'connected', agent: { name: 'Research agent' } });
|
| 64 | await expect(resolveInstallation(store, 'owner-b', 'codex-installation-a')).resolves.toEqual({
|
| 65 | status: 'unbound',
|
| 66 | });
|
| 67 | });
|
| 68 |
|
| 69 | it('supports unrelated agents and confirmed installation switching', async () => {
|
| 70 | const store = new MemoryOnboardingStore();
|
| 71 | const first = await requestAgentSetup(store, 'owner', {
|
| 72 | clientId: 'client-a',
|
| 73 | agentName: 'Coding agent',
|
| 74 | projectName: 'Product A',
|
| 75 | });
|
| 76 | const second = await requestAgentSetup(store, 'owner', {
|
| 77 | clientId: 'client-b',
|
| 78 | agentName: 'Support agent',
|
| 79 | projectName: 'Product B',
|
| 80 | });
|
| 81 | const firstBinding = await approveAgentSetup(store, 'owner', first.requestId);
|
| 82 | const secondBinding = await approveAgentSetup(store, 'owner', second.requestId);
|
| 83 | if (firstBinding.status !== 'connected' || secondBinding.status !== 'connected')
|
| 84 | throw new Error('Expected connected agents');
|
| 85 |
|
| 86 | const switchRequest = await requestAgentSetup(store, 'owner', {
|
| 87 | clientId: 'client-a',
|
| 88 | action: 'switch',
|
| 89 | existingAgentId: secondBinding.agent.agentId,
|
| 90 | });
|
| 91 | const beforeSwitch = await resolveInstallation(store, 'owner', 'client-a');
|
| 92 | expect(beforeSwitch.status).toBe('connected');
|
| 93 | if (beforeSwitch.status !== 'connected') throw new Error('Expected connected installation');
|
| 94 | expect(beforeSwitch.agent.agentId).toBe(firstBinding.agent.agentId);
|
| 95 | await approveAgentSetup(store, 'owner', switchRequest.requestId);
|
| 96 | const afterSwitch = await resolveInstallation(store, 'owner', 'client-a');
|
| 97 | expect(afterSwitch.status).toBe('connected');
|
| 98 | if (afterSwitch.status !== 'connected') throw new Error('Expected connected installation');
|
| 99 | expect(afterSwitch.agent.agentId).toBe(secondBinding.agent.agentId);
|
| 100 | const state = await getOnboardingState(store, 'owner');
|
| 101 | expect(state.projects).toHaveLength(2);
|
| 102 | expect(state.agentProfiles).toHaveLength(2);
|
| 103 | });
|
| 104 |
|
| 105 | it('updates only the profile bound to the authenticated installation', async () => {
|
| 106 | const store = new MemoryOnboardingStore();
|
| 107 | const request = await requestAgentSetup(store, 'owner', {
|
| 108 | clientId: 'client-a',
|
| 109 | agentName: 'Planner',
|
| 110 | projectName: 'Launch',
|
| 111 | });
|
| 112 | await approveAgentSetup(store, 'owner', request.requestId);
|
| 113 | const updated = await updateAgentProfile(store, 'owner', 'client-a', {
|
| 114 | capabilities: ['planning', 'coordination'],
|
| 115 | });
|
| 116 | expect(updated.capabilities).toEqual(['planning', 'coordination']);
|
| 117 | expect(updated.transport).toBe('direct_a2a');
|
| 118 | await expect(
|
| 119 | updateAgentProfile(store, 'owner', 'different-client', { name: 'Hijacked' }),
|
| 120 | ).rejects.toThrow('not connected');
|
| 121 | });
|
| 122 | });
|