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