| 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 | a2aEndpoint: 'https://research.example/a2a',
|
| 39 | });
|
| 40 |
|
| 41 | expect(request.status).toBe('pending');
|
| 42 | expect(request).toMatchObject({
|
| 43 | autoPublish: true,
|
| 44 | autoAcceptPolicy: 'safe_matching',
|
| 45 | autoAcceptTaskCategories: ['research'],
|
| 46 | });
|
| 47 | await expect(resolveInstallation(store, 'owner-a', 'codex-installation-a')).resolves.toEqual({
|
| 48 | status: 'unbound',
|
| 49 | });
|
| 50 |
|
| 51 | const approved = await approveAgentSetup(store, 'owner-a', request.requestId);
|
| 52 | expect(approved).toMatchObject({
|
| 53 | status: 'connected',
|
| 54 | agent: {
|
| 55 | name: 'Research agent',
|
| 56 | capabilities: ['research'],
|
| 57 | agentVersion: '2.0.0',
|
| 58 | a2aEndpoint: 'https://research.example/a2a',
|
| 59 | },
|
| 60 | project: { name: 'Market research' },
|
| 61 | });
|
| 62 | await expect(
|
| 63 | resolveInstallation(store, 'owner-a', 'codex-installation-a'),
|
| 64 | ).resolves.toMatchObject({ status: 'connected', agent: { name: 'Research agent' } });
|
| 65 | await expect(resolveInstallation(store, 'owner-b', 'codex-installation-a')).resolves.toEqual({
|
| 66 | status: 'unbound',
|
| 67 | });
|
| 68 | });
|
| 69 |
|
| 70 | it('supports unrelated agents and confirmed installation switching', async () => {
|
| 71 | const store = new MemoryOnboardingStore();
|
| 72 | const first = await requestAgentSetup(store, 'owner', {
|
| 73 | clientId: 'client-a',
|
| 74 | agentName: 'Coding agent',
|
| 75 | projectName: 'Product A',
|
| 76 | });
|
| 77 | const second = await requestAgentSetup(store, 'owner', {
|
| 78 | clientId: 'client-b',
|
| 79 | agentName: 'Support agent',
|
| 80 | projectName: 'Product B',
|
| 81 | });
|
| 82 | const firstBinding = await approveAgentSetup(store, 'owner', first.requestId);
|
| 83 | const secondBinding = await approveAgentSetup(store, 'owner', second.requestId);
|
| 84 | if (firstBinding.status !== 'connected' || secondBinding.status !== 'connected')
|
| 85 | throw new Error('Expected connected agents');
|
| 86 |
|
| 87 | const switchRequest = await requestAgentSetup(store, 'owner', {
|
| 88 | clientId: 'client-a',
|
| 89 | action: 'switch',
|
| 90 | existingAgentId: secondBinding.agent.agentId,
|
| 91 | });
|
| 92 | const beforeSwitch = await resolveInstallation(store, 'owner', 'client-a');
|
| 93 | expect(beforeSwitch.status).toBe('connected');
|
| 94 | if (beforeSwitch.status !== 'connected') throw new Error('Expected connected installation');
|
| 95 | expect(beforeSwitch.agent.agentId).toBe(firstBinding.agent.agentId);
|
| 96 | await approveAgentSetup(store, 'owner', switchRequest.requestId);
|
| 97 | const afterSwitch = await resolveInstallation(store, 'owner', 'client-a');
|
| 98 | expect(afterSwitch.status).toBe('connected');
|
| 99 | if (afterSwitch.status !== 'connected') throw new Error('Expected connected installation');
|
| 100 | expect(afterSwitch.agent.agentId).toBe(secondBinding.agent.agentId);
|
| 101 | const state = await getOnboardingState(store, 'owner');
|
| 102 | expect(state.projects).toHaveLength(2);
|
| 103 | expect(state.agentProfiles).toHaveLength(2);
|
| 104 | });
|
| 105 |
|
| 106 | it('updates only the profile bound to the authenticated installation', async () => {
|
| 107 | const store = new MemoryOnboardingStore();
|
| 108 | const request = await requestAgentSetup(store, 'owner', {
|
| 109 | clientId: 'client-a',
|
| 110 | agentName: 'Planner',
|
| 111 | projectName: 'Launch',
|
| 112 | });
|
| 113 | await approveAgentSetup(store, 'owner', request.requestId);
|
| 114 | const updated = await updateAgentProfile(store, 'owner', 'client-a', {
|
| 115 | capabilities: ['planning', 'coordination'],
|
| 116 | a2aEndpoint: 'https://planner.example/a2a',
|
| 117 | });
|
| 118 | expect(updated.capabilities).toEqual(['planning', 'coordination']);
|
| 119 | expect(updated.a2aEndpoint).toBe('https://planner.example/a2a');
|
| 120 | await expect(
|
| 121 | updateAgentProfile(store, 'owner', 'different-client', { name: 'Hijacked' }),
|
| 122 | ).rejects.toThrow('not connected');
|
| 123 | });
|
| 124 | });
|