| 1 | import { describe, expect, it } from 'vitest';
|
| 2 | import { buildApi } from '../apps/api/src/app.js';
|
| 3 | import type { PublicAgentCard } from '../packages/protocol/src/index.js';
|
| 4 |
|
| 5 | describe('connector-first Agent Card onboarding API', () => {
|
| 6 | it('accepts an agent profile, requires owner approval, and blocks publication until runtime verification', async () => {
|
| 7 | const rows: { kind: string; recordId: string; payload: any }[] = [];
|
| 8 | const cards = new Map<string, PublicAgentCard>();
|
| 9 | const claim = {
|
| 10 | claimId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
|
| 11 | status: 'pending' as const,
|
| 12 | runtimeEndpoint: 'https://agent.example/openclasp',
|
| 13 | expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
| 14 | createdAt: new Date().toISOString(),
|
| 15 | profile: {
|
| 16 | description: 'Compares approved suppliers and prepares purchases',
|
| 17 | framework: 'LangGraph',
|
| 18 | agentVersion: '2.1.0',
|
| 19 | capabilities: ['compare supplier quotes'],
|
| 20 | limitations: ['requires human approval before payment'],
|
| 21 | },
|
| 22 | };
|
| 23 | let runtimeVerified = false;
|
| 24 | const repository = {
|
| 25 | list: async () => rows,
|
| 26 | upsert: async (_operatorId: string, kind: string, recordId: string, payload: any) => {
|
| 27 | const current = rows.find((row) => row.kind === kind && row.recordId === recordId);
|
| 28 | if (current) current.payload = payload;
|
| 29 | else rows.push({ kind, recordId, payload });
|
| 30 | },
|
| 31 | createConnectorClaim: async () => ({ ...claim, claimSecret: 'oc_cc_secret' }),
|
| 32 | getConnectorClaim: async () => claim,
|
| 33 | approveConnectorClaim: async (_owner: string, _claimId: string, name: string) => {
|
| 34 | const agent = {
|
| 35 | agentId: 'agent-connected',
|
| 36 | projectId: 'project-connected',
|
| 37 | ...claim.profile,
|
| 38 | name,
|
| 39 | nameProvenance: 'operator_attested',
|
| 40 | profileProvenance: 'self_declared',
|
| 41 | identityMode: 'connector_claim',
|
| 42 | status: 'active',
|
| 43 | autoPublish: false,
|
| 44 | autoAcceptPolicy: 'off',
|
| 45 | autoAcceptTaskCategories: [],
|
| 46 | createdAt: new Date().toISOString(),
|
| 47 | updatedAt: new Date().toISOString(),
|
| 48 | };
|
| 49 | rows.push({ kind: 'agent_profile', recordId: agent.agentId, payload: agent });
|
| 50 | return { ...claim, status: 'approved', agentId: agent.agentId, agent };
|
| 51 | },
|
| 52 | rejectConnectorClaim: async () => ({ ...claim, status: 'rejected' }),
|
| 53 | listAgentRuntimes: async () =>
|
| 54 | runtimeVerified
|
| 55 | ? [{ agentId: 'agent-connected', status: 'verified', endpoint: claim.runtimeEndpoint }]
|
| 56 | : [],
|
| 57 | publishAgent: async (_operatorId: string, card: PublicAgentCard) => {
|
| 58 | cards.set(card.agentId, card);
|
| 59 | return card;
|
| 60 | },
|
| 61 | unpublishAgent: async (_operatorId: string, agentId: string) => cards.delete(agentId),
|
| 62 | getPublishedAgent: async (agentId: string) => cards.get(agentId),
|
| 63 | } as any;
|
| 64 | const app = buildApi(undefined, undefined, repository);
|
| 65 | await app.ready();
|
| 66 | const headers = { 'x-openclasp-operator': 'owner-a' };
|
| 67 |
|
| 68 | const start = await app.inject({
|
| 69 | method: 'POST',
|
| 70 | url: '/v0.1/connector-claims',
|
| 71 | payload: {
|
| 72 | runtimeEndpoint: claim.runtimeEndpoint,
|
| 73 | credentialPublicKey: 'public-key',
|
| 74 | profile: claim.profile,
|
| 75 | },
|
| 76 | });
|
| 77 | expect(start.statusCode).toBe(200);
|
| 78 | expect(start.json()).toMatchObject({
|
| 79 | claimId: claim.claimId,
|
| 80 | claimSecret: 'oc_cc_secret',
|
| 81 | confirmationUrl: expect.stringContaining(`/connect?claim=${claim.claimId}`),
|
| 82 | });
|
| 83 |
|
| 84 | const missingName = await app.inject({
|
| 85 | method: 'POST',
|
| 86 | url: `/v0.1/connector-claims/${claim.claimId}/approve`,
|
| 87 | headers,
|
| 88 | payload: {},
|
| 89 | });
|
| 90 | expect(missingName.statusCode).toBe(400);
|
| 91 |
|
| 92 | const approved = await app.inject({
|
| 93 | method: 'POST',
|
| 94 | url: `/v0.1/connector-claims/${claim.claimId}/approve`,
|
| 95 | headers,
|
| 96 | payload: { name: 'Purchasing agent' },
|
| 97 | });
|
| 98 | expect(approved.statusCode).toBe(200);
|
| 99 | expect(approved.json()).toMatchObject({
|
| 100 | status: 'approved',
|
| 101 | agent: {
|
| 102 | name: 'Purchasing agent',
|
| 103 | identityMode: 'connector_claim',
|
| 104 | nameProvenance: 'operator_attested',
|
| 105 | profileProvenance: 'self_declared',
|
| 106 | },
|
| 107 | });
|
| 108 |
|
| 109 | expect(
|
| 110 | (
|
| 111 | await app.inject({
|
| 112 | method: 'POST',
|
| 113 | url: '/v0.1/agents/agent-connected/publication',
|
| 114 | headers,
|
| 115 | payload: { published: true },
|
| 116 | })
|
| 117 | ).json(),
|
| 118 | ).toEqual({ error: 'Verify the agent runtime before publishing' });
|
| 119 |
|
| 120 | runtimeVerified = true;
|
| 121 | const publication = await app.inject({
|
| 122 | method: 'POST',
|
| 123 | url: '/v0.1/agents/agent-connected/publication',
|
| 124 | headers,
|
| 125 | payload: { published: true },
|
| 126 | });
|
| 127 | expect(publication.statusCode).toBe(200);
|
| 128 | expect(publication.json()).toMatchObject({ published: true, card: { transports: [] } });
|
| 129 |
|
| 130 | expect(
|
| 131 | (
|
| 132 | await app.inject({
|
| 133 | method: 'POST',
|
| 134 | url: '/v0.1/quickstart/agent',
|
| 135 | headers,
|
| 136 | payload: {},
|
| 137 | })
|
| 138 | ).statusCode,
|
| 139 | ).toBe(404);
|
| 140 | await app.close();
|
| 141 | });
|
| 142 | });
|