The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 18b70a0f11a5Commit detailsView latest

tests/api.test.ts 685 lines · 23.1 KB · typescript
1
import { describe, expect, it } from 'vitest';
2
import { buildApi } from '../apps/api/src/app.js';
3
4
describe('HTTP API', () => {
5
  it('serves health, readiness, and OpenAPI', async () => {
6
    const app = buildApi();
7
    await app.ready();
8
    expect((await app.inject({ method: 'GET', url: '/health' })).statusCode).toBe(200);
9
    expect((await app.inject({ method: 'GET', url: '/ready' })).json()).toEqual({
10
      status: 'ready',
11
    });
12
    expect(
13
      (await app.inject({ method: 'GET', url: '/extensions/trust/v0.1' })).json(),
14
    ).toMatchObject({ version: '0.1', transportsMessages: false });
15
    const specification = (await app.inject({ method: 'GET', url: '/openapi.json' })).json();
16
    expect(specification.info.title).toBe('OpenClasp API');
17
    expect(specification.paths).toHaveProperty('/v0.1/risk/assess');
18
    await app.close();
19
  });
20
21
  it('isolates hosted dashboard and settings by authenticated operator', async () => {
22
    const calls: string[] = [];
23
    const repository = {
24
      dashboard: async (operatorId: string) => {
25
        calls.push(`dashboard:${operatorId}`);
26
        return {
27
          agents: [],
28
          projects: [],
29
          installations: [],
30
          setupRequests: [],
31
          publications: [],
32
          interactions: [],
33
          federatedInteractions: [],
34
          liveSessions: [],
35
          hostedThreads: [],
36
          events: [],
37
          conflicts: [],
38
          receipts: [],
39
          profiles: [],
40
          counterpartyBriefs: [],
41
          completionReports: [],
42
          feedbackRequests: [],
43
          interactionFeedback: [],
44
          interactionConclusions: [],
45
          learningEligibility: [],
46
          profileDeltas: [],
47
          runtimes: [],
48
          accessTokens: [],
49
        };
50
      },
51
      getSettings: async (operatorId: string) => {
52
        calls.push(`settings:${operatorId}`);
53
        return {
54
          displayName: '',
55
          contributionEnabled: false,
56
          retentionDays: 30,
57
          evidenceSharing: 'ask' as const,
58
          rawConversationsStored: false as const,
59
        };
60
      },
61
      saveSettings: async (operatorId: string, value: any) => {
62
        calls.push(`save:${operatorId}`);
63
        return { ...value, rawConversationsStored: false as const };
64
      },
65
      upsert: async () => undefined,
66
      list: async () => [],
67
      publishAgent: async (_operatorId: string, card: any) => card,
68
      unpublishAgent: async () => true,
69
      getPublishedAgent: async () => undefined,
70
      searchPublishedAgents: async () => [],
71
      registerAgentRuntime: async (operatorId: string, agentId: string, endpoint: string) => {
72
        calls.push(`runtime:${operatorId}:${agentId}:${endpoint}`);
73
        return {
74
          agentId,
75
          endpoint,
76
          a2aEndpoint: endpoint,
77
          status: 'verified' as const,
78
          verifiedAt: new Date().toISOString(),
79
          verificationKey: 'public-key',
80
        };
81
      },
82
      touchAgentPresence: async (operatorId: string, agentId: string) => {
83
        calls.push(`heartbeat:${operatorId}:${agentId}`);
84
        return {
85
          status: 'online' as const,
86
          checkedAt: '2026-01-01T00:00:00.000Z',
87
        };
88
      },
89
      disableAgentRuntime: async (operatorId: string, agentId: string) => {
90
        calls.push(`disable-runtime:${operatorId}:${agentId}`);
91
        return { agentId, status: 'disabled' as const };
92
      },
93
      deleteAgent: async (operatorId: string, agentId: string) => {
94
        calls.push(`delete-agent:${operatorId}:${agentId}`);
95
        return { agentId, deleted: true as const, historyRetained: true as const };
96
      },
97
      listAgentAccessTokens: async (operatorId: string, agentId?: string) => {
98
        calls.push(`list-tokens:${operatorId}:${agentId ?? '*'}`);
99
        return [];
100
      },
101
      issueAgentAccessToken: async (
102
        operatorId: string,
103
        agentId: string,
104
        value: { name: string; expiresInDays: number },
105
      ) => {
106
        calls.push(`issue-token:${operatorId}:${agentId}:${value.name}:${value.expiresInDays}`);
107
        return {
108
          tokenId: 'abcdefghijklmnop',
109
          token: 'oc_at_abcdefghijklmnop.abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG',
110
          agentId,
111
          name: value.name,
112
          scopes: ['mcp:access', 'runtime:connect'],
113
          createdAt: '2026-01-01T00:00:00.000Z',
114
          expiresAt: '2027-01-01T00:00:00.000Z',
115
        };
116
      },
117
      revokeAgentAccessToken: async (operatorId: string, agentId: string, tokenId: string) => {
118
        calls.push(`revoke-token:${operatorId}:${agentId}:${tokenId}`);
119
        return { tokenId, agentId, revokedAt: '2026-01-02T00:00:00.000Z' };
120
      },
121
      getCounterpartyBrief: async (operatorId: string, interactionId: string, agentId: string) => {
122
        calls.push(`brief:${operatorId}:${interactionId}:${agentId}`);
123
        return {
124
          briefId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
125
          interactionId,
126
          contractHash: 'contract-hash',
127
          recipientAgentId: agentId,
128
          subjectAgentId: 'agent-b',
129
          taskCategory: 'recruiting',
130
          decision: 'CHALLENGE' as const,
131
          requirements: [],
132
          insights: [],
133
          relevantSampleSize: 0,
134
          historyConfidence: 0,
135
          subjectAgentVersion: '1.0.0',
136
          recommendedContractChanges: [],
137
          generatedAt: '2026-08-29T00:00:00.000Z',
138
          expiresAt: '2026-08-29T01:00:00.000Z',
139
        };
140
      },
141
      submitCompletionReport: async (
142
        operatorId: string,
143
        agentId: string,
144
        report: any,
145
        submissionMethod: 'oauth_installation' | 'agent_access_token' | 'runtime_session',
146
      ) => {
147
        calls.push(
148
          `completion:${operatorId}:${agentId}:${report.interactionId}:${submissionMethod}`,
149
        );
150
        return report;
151
      },
152
      recordSessionCompletionReport: async (token: string, report: any) => {
153
        calls.push(`session-completion:${token}:${report.interactionId}`);
154
        return report;
155
      },
156
      listFeedbackRequests: async (operatorId: string, agentId: string) => {
157
        calls.push(`feedback-requests:${operatorId}:${agentId}`);
158
        return [];
159
      },
160
      submitInteractionFeedback: async (
161
        operatorId: string,
162
        agentId: string,
163
        feedback: any,
164
        submissionMethod: 'oauth_installation' | 'agent_access_token' | 'runtime_session',
165
      ) => {
166
        calls.push(
167
          `feedback:${operatorId}:${agentId}:${feedback.interactionId}:${submissionMethod}`,
168
        );
169
        return { feedbackId: feedback.feedbackId, status: 'submitted' as const, revealed: false };
170
      },
171
      recordSessionFeedback: async (token: string, feedback: any) => {
172
        calls.push(`session-feedback:${token}:${feedback.interactionId}`);
173
        return { feedbackId: feedback.feedbackId, status: 'submitted' as const, revealed: false };
174
      },
175
      receiveTemporaryMessage: async (
176
        token: string,
177
        agentId: string,
178
        requestKey: string,
179
        content: string,
180
      ) => {
181
        calls.push(`temporary:${token}:${agentId}:${requestKey}:${content}`);
182
        return {
183
          message: {
184
            messageId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
185
            threadId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
186
            interactionId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
187
            senderAgentId: 'agent-peer',
188
            recipientAgentId: agentId,
189
            contentType: 'text/plain' as const,
190
            content,
191
            contentHash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
192
            delivery: 'delivered' as const,
193
            createdAt: new Date().toISOString(),
194
          },
195
          deduplicated: false,
196
        };
197
      },
198
    };
199
    const app = buildApi(undefined, undefined, repository);
200
    await app.ready();
201
    expect((await app.inject({ method: 'GET', url: '/v0.1/dashboard' })).statusCode).toBe(401);
202
    expect(
203
      (
204
        await app.inject({
205
          method: 'GET',
206
          url: '/v0.1/dashboard',
207
          headers: { 'x-openclasp-operator': 'user-a' },
208
        })
209
      ).statusCode,
210
    ).toBe(200);
211
    expect(
212
      (
213
        await app.inject({
214
          method: 'GET',
215
          url: '/v0.1/settings',
216
          headers: { 'x-openclasp-operator': 'user-b' },
217
        })
218
      ).statusCode,
219
    ).toBe(200);
220
    expect(
221
      (
222
        await app.inject({
223
          method: 'PUT',
224
          url: '/v0.1/agents/agent-a/runtime',
225
          headers: { 'x-openclasp-operator': 'user-a' },
226
          payload: { endpoint: 'https://agent.example/openclasp' },
227
        })
228
      ).statusCode,
229
    ).toBe(200);
230
    expect(calls).toEqual([
231
      'dashboard:user-a',
232
      'settings:user-b',
233
      'runtime:user-a:agent-a:https://agent.example/openclasp',
234
    ]);
235
    expect(
236
      (
237
        await app.inject({
238
          method: 'GET',
239
          url: '/v0.1/runtime/bootstrap',
240
          headers: {
241
            'x-openclasp-operator': 'user-a',
242
            'x-openclasp-bound-agent': 'agent-a',
243
            host: 'openclasp.example',
244
            'x-forwarded-proto': 'https',
245
          },
246
        })
247
      ).json(),
248
    ).toMatchObject({
249
      agentId: 'agent-a',
250
      openClaspUrl: 'https://openclasp.example',
251
      protocol: 'A2A/1.0',
252
    });
253
    expect(
254
      (
255
        await app.inject({
256
          method: 'PUT',
257
          url: '/v0.1/runtime',
258
          headers: {
259
            'x-openclasp-operator': 'user-a',
260
            'x-openclasp-bound-agent': 'agent-a',
261
          },
262
          payload: { endpoint: 'https://runtime.example/a2a' },
263
        })
264
      ).statusCode,
265
    ).toBe(200);
266
    expect(calls.at(-1)).toBe('runtime:user-a:agent-a:https://runtime.example/a2a');
267
    expect(
268
      (
269
        await app.inject({
270
          method: 'DELETE',
271
          url: '/v0.1/runtime',
272
          headers: {
273
            'x-openclasp-operator': 'user-a',
274
            'x-openclasp-bound-agent': 'agent-a',
275
          },
276
        })
277
      ).statusCode,
278
    ).toBe(200);
279
    expect(calls.at(-1)).toBe('disable-runtime:user-a:agent-a');
280
    expect(
281
      (
282
        await app.inject({
283
          method: 'POST',
284
          url: '/v0.1/runtime/heartbeat',
285
          headers: {
286
            'x-openclasp-operator': 'user-a',
287
            'x-openclasp-bound-agent': 'agent-a',
288
          },
289
        })
290
      ).statusCode,
291
    ).toBe(200);
292
    expect(calls.at(-1)).toBe('heartbeat:user-a:agent-a');
293
    expect(
294
      (
295
        await app.inject({
296
          method: 'DELETE',
297
          url: '/v0.1/agents/agent-a',
298
          headers: { 'x-openclasp-operator': 'user-a' },
299
        })
300
      ).json(),
301
    ).toMatchObject({ agentId: 'agent-a', deleted: true, historyRetained: true });
302
    expect(calls.at(-1)).toBe('delete-agent:user-a:agent-a');
303
    const issuedToken = await app.inject({
304
      method: 'POST',
305
      url: '/v0.1/agents/agent-a/access-tokens',
306
      headers: { 'x-openclasp-operator': 'user-a' },
307
      payload: { name: 'Botpress', expiresInDays: 365 },
308
    });
309
    expect(issuedToken.statusCode).toBe(200);
310
    expect(issuedToken.json()).toMatchObject({
311
      agentId: 'agent-a',
312
      name: 'Botpress',
313
      scopes: ['mcp:access', 'runtime:connect'],
314
    });
315
    expect(calls.at(-1)).toBe('issue-token:user-a:agent-a:Botpress:365');
316
    expect(
317
      (
318
        await app.inject({
319
          method: 'DELETE',
320
          url: '/v0.1/agents/agent-a/access-tokens/abcdefghijklmnop',
321
          headers: { 'x-openclasp-operator': 'user-a' },
322
        })
323
      ).statusCode,
324
    ).toBe(200);
325
    expect(calls.at(-1)).toBe('revoke-token:user-a:agent-a:abcdefghijklmnop');
326
    const interactionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
327
    expect(
328
      (
329
        await app.inject({
330
          method: 'GET',
331
          url: `/v0.1/federated-interactions/${interactionId}/brief?agentId=agent-a`,
332
          headers: { 'x-openclasp-operator': 'user-a' },
333
        })
334
      ).json(),
335
    ).toMatchObject({ recipientAgentId: 'agent-a', decision: 'CHALLENGE' });
336
    expect(
337
      (
338
        await app.inject({
339
          method: 'GET',
340
          url: `/v0.1/federated-interactions/${interactionId}/brief?agentId=agent-b`,
341
          headers: {
342
            'x-openclasp-operator': 'user-a',
343
            'x-openclasp-bound-agent': 'agent-a',
344
          },
345
        })
346
      ).statusCode,
347
    ).toBe(403);
348
    const completionReport = {
349
      reportId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
350
      interactionId,
351
      contractHash: 'contract-hash',
352
      reportingAgentId: 'agent-a',
353
      counterpartyAgentId: 'agent-b',
354
      agentVersion: '1.0.0',
355
      outcome: 'partial',
356
      summary: 'The counterparty answered but no matching role was available.',
357
      requestedOutcome: 'Find a backend role',
358
      criteria: [],
359
      completedAt: '2026-08-29T00:00:00.000Z',
360
      confidence: 0.9,
361
      dataSharingMode: 'structured_only',
362
    };
363
    expect(
364
      (
365
        await app.inject({
366
          method: 'POST',
367
          url: `/v0.1/federated-interactions/${interactionId}/completion-reports`,
368
          headers: {
369
            'x-openclasp-operator': 'user-a',
370
            'x-openclasp-bound-agent': 'agent-a',
371
          },
372
          payload: completionReport,
373
        })
374
      ).statusCode,
375
    ).toBe(200);
376
    expect(calls.at(-1)).toBe(`completion:user-a:agent-a:${interactionId}:oauth_installation`);
377
    expect(
378
      (
379
        await app.inject({
380
          method: 'POST',
381
          url: `/v0.1/federated-interactions/${interactionId}/completion-reports`,
382
          headers: {
383
            'x-openclasp-operator': 'user-a',
384
            'x-openclasp-bound-agent': 'agent-a',
385
          },
386
          payload: { ...completionReport, rawTranscript: 'must never be accepted' },
387
        })
388
      ).statusCode,
389
    ).toBe(400);
390
    expect(
391
      (
392
        await app.inject({
393
          method: 'POST',
394
          url: `/sessions/${interactionId}/completion-reports`,
395
          payload: completionReport,
396
        })
397
      ).statusCode,
398
    ).toBe(401);
399
    expect(
400
      (
401
        await app.inject({
402
          method: 'POST',
403
          url: `/sessions/${interactionId}/completion-reports`,
404
          headers: { authorization: 'Bearer scoped-session-token' },
405
          payload: completionReport,
406
        })
407
      ).statusCode,
408
    ).toBe(200);
409
    expect(calls.at(-1)).toBe(`session-completion:scoped-session-token:${interactionId}`);
410
    expect(
411
      (
412
        await app.inject({
413
          method: 'GET',
414
          url: '/v0.1/feedback-requests?agentId=agent-a',
415
          headers: {
416
            'x-openclasp-operator': 'user-a',
417
            'x-openclasp-bound-agent': 'agent-a',
418
          },
419
        })
420
      ).statusCode,
421
    ).toBe(200);
422
    const interactionFeedback = {
423
      feedbackId: 'dddddddd-dddd-4ddd-8ddd-dddddddddddd',
424
      requestId: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
425
      interactionId,
426
      reviewerAgentId: 'agent-a',
427
      subjectAgentId: 'agent-b',
428
      reviewerAgentVersion: '1.0.0',
429
      ratings: {
430
        overall_satisfaction: 0.7,
431
        outcome_satisfaction: 0.6,
432
        communication: 0.9,
433
        timeliness: 0.8,
434
        scope_adherence: 0.6,
435
        evidence_quality: 0.7,
436
        correction_handling: 0.8,
437
        reliability: 0.75,
438
      },
439
      wouldWorkAgain: 'yes',
440
      confidence: 0.9,
441
      submittedAt: '2026-08-29T00:10:00.000Z',
442
    };
443
    expect(
444
      (
445
        await app.inject({
446
          method: 'POST',
447
          url: `/v0.1/federated-interactions/${interactionId}/feedback`,
448
          headers: {
449
            'x-openclasp-operator': 'user-a',
450
            'x-openclasp-bound-agent': 'agent-a',
451
          },
452
          payload: interactionFeedback,
453
        })
454
      ).statusCode,
455
    ).toBe(200);
456
    expect(calls.at(-1)).toBe(`feedback:user-a:agent-a:${interactionId}:oauth_installation`);
457
    expect(
458
      (
459
        await app.inject({
460
          method: 'POST',
461
          url: `/sessions/${interactionId}/feedback`,
462
          headers: { authorization: 'Bearer scoped-session-token' },
463
          payload: interactionFeedback,
464
        })
465
      ).statusCode,
466
    ).toBe(200);
467
    expect(calls.at(-1)).toBe(`session-feedback:scoped-session-token:${interactionId}`);
468
    const providerConnection = await app.inject({
469
      method: 'POST',
470
      url: '/v0.1/provider-connections',
471
      headers: { 'x-openclasp-operator': 'user-a' },
472
      payload: {
473
        provider: 'botpress',
474
        agentName: 'Recruiting agent',
475
        projectName: 'Recruiting',
476
        capabilities: ['candidate matching'],
477
        limitations: ['no final hiring decisions'],
478
        expiresInDays: 365,
479
      },
480
    });
481
    expect(providerConnection.statusCode).toBe(200);
482
    expect(providerConnection.json()).toMatchObject({
483
      agent: {
484
        name: 'Recruiting agent',
485
        framework: 'Botpress',
486
        identityMode: 'owner_managed',
487
      },
488
      accessToken: { name: 'Botpress', scopes: ['mcp:access', 'runtime:connect'] },
489
    });
490
    expect(calls.at(-1)).toMatch(/^issue-token:user-a:agent_.*:Botpress:365$/);
491
    expect(
492
      (
493
        await app.inject({
494
          method: 'POST',
495
          url: '/a2a/temporary/agent-a',
496
          payload: {
497
            jsonrpc: '2.0',
498
            id: 'request-1',
499
            method: 'message/send',
500
            params: { message: { parts: [{ kind: 'text', text: 'Hello engineer' }] } },
501
          },
502
        })
503
      ).statusCode,
504
    ).toBe(401);
505
    const temporaryDelivery = await app.inject({
506
      method: 'POST',
507
      url: '/a2a/temporary/agent-a',
508
      headers: { authorization: 'Bearer scoped-token' },
509
      payload: {
510
        jsonrpc: '2.0',
511
        id: 'request-1',
512
        method: 'message/send',
513
        params: { message: { parts: [{ kind: 'text', text: 'Hello engineer' }] } },
514
      },
515
    });
516
    expect(temporaryDelivery.statusCode).toBe(200);
517
    expect(temporaryDelivery.json()).toMatchObject({
518
      result: {
519
        task: { id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', state: 'submitted' },
520
        privacyMode: 'openclasp_hosted_temporary',
521
      },
522
    });
523
    expect(calls.at(-1)).toBe('temporary:scoped-token:agent-a:request-1:Hello engineer');
524
    await app.close();
525
  });
526
527
  it('publishes only an owned agent public card', async () => {
528
    const published: any[] = [];
529
    const repository = {
530
      dashboard: async () => ({}) as any,
531
      getSettings: async () => ({}) as any,
532
      saveSettings: async () => ({}) as any,
533
      upsert: async () => undefined,
534
      list: async () => [
535
        {
536
          kind: 'agent_profile' as const,
537
          recordId: 'agent-one',
538
          payload: {
539
            agentId: 'agent-one',
540
            projectId: 'secret-project',
541
            name: 'Research agent',
542
            description: 'Finds primary sources',
543
            framework: 'Codex',
544
            agentVersion: '1.0.0',
545
            a2aEndpoint: 'https://agent.example/a2a',
546
            capabilities: ['research'],
547
            limitations: ['no purchases'],
548
            identityMode: 'oauth_installation' as const,
549
            status: 'active' as const,
550
            createdAt: '2026-01-01T00:00:00.000Z',
551
            updatedAt: '2026-01-01T00:00:00.000Z',
552
          },
553
        },
554
      ],
555
      publishAgent: async (_operatorId: string, card: any) => {
556
        published.push(card);
557
        return card;
558
      },
559
      unpublishAgent: async () => true,
560
      getPublishedAgent: async () => published[0],
561
      searchPublishedAgents: async () => published,
562
    };
563
    const app = buildApi(undefined, undefined, repository);
564
    await app.ready();
565
    const response = await app.inject({
566
      method: 'POST',
567
      url: '/v0.1/agents/agent-one/publication',
568
      headers: { 'x-openclasp-operator': 'owner-one' },
569
      payload: { published: true },
570
    });
571
    expect(response.statusCode).toBe(200);
572
    expect(published[0]).toMatchObject({
573
      agentId: 'agent-one',
574
      capabilities: ['research'],
575
      assurance: 'oauth_authenticated',
576
    });
577
    expect(published[0]).not.toHaveProperty('projectId');
578
    expect(published[0]).not.toHaveProperty('operatorId');
579
    const publicCard = await app.inject({
580
      method: 'GET',
581
      url: '/agents/agent-one/card.json',
582
    });
583
    expect(publicCard.statusCode).toBe(200);
584
    expect(publicCard.json()).not.toHaveProperty('projectId');
585
    const a2aCard = await app.inject({
586
      method: 'GET',
587
      url: '/agents/agent-one/a2a-agent-card.json',
588
    });
589
    expect(a2aCard.statusCode).toBe(200);
590
    expect(a2aCard.json().supportedInterfaces[0]).toMatchObject({
591
      url: 'https://agent.example/a2a',
592
      protocolVersion: '1.0',
593
    });
594
    const automation = await app.inject({
595
      method: 'PUT',
596
      url: '/v0.1/agents/agent-one/automation',
597
      headers: { 'x-openclasp-operator': 'owner-one' },
598
      payload: {
599
        a2aEndpoint: 'https://agent.example/a2a',
600
        autoPublish: true,
601
        autoAcceptPolicy: 'safe_matching',
602
        autoAcceptTaskCategories: ['research'],
603
      },
604
    });
605
    expect(automation.statusCode).toBe(200);
606
    expect(automation.json()).toMatchObject({
607
      autoPublish: true,
608
      autoAcceptPolicy: 'safe_matching',
609
    });
610
    await app.close();
611
  });
612
613
  it('accepts only structured live-session events at the reporting endpoint', async () => {
614
    const events: any[] = [];
615
    const repository = {
616
      dashboard: async () => ({}) as any,
617
      getSettings: async () => ({}) as any,
618
      saveSettings: async () => ({}) as any,
619
      upsert: async () => undefined,
620
      list: async () => [],
621
      publishAgent: async (_operatorId: string, card: any) => card,
622
      unpublishAgent: async () => true,
623
      getPublishedAgent: async () => undefined,
624
      searchPublishedAgents: async () => [],
625
      recordLiveSessionEvent: async (token: string, value: any) => {
626
        events.push({ token, value });
627
        return {
628
          recorded: true,
629
          deduplicated: false,
630
          eventId: value.eventId,
631
          attestation: {
632
            algorithm: 'Ed25519' as const,
633
            keyId: 'openclasp:test',
634
            value: 'signature',
635
            digest: 'digest',
636
          },
637
        };
638
      },
639
    };
640
    const app = buildApi(undefined, undefined, repository);
641
    await app.ready();
642
    const interactionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
643
    expect(
644
      (await app.inject({ method: 'POST', url: `/sessions/${interactionId}/events` })).statusCode,
645
    ).toBe(401);
646
    const response = await app.inject({
647
      method: 'POST',
648
      url: `/sessions/${interactionId}/events`,
649
      headers: { authorization: 'Bearer live-session-token' },
650
      payload: {
651
        eventId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
652
        interactionId,
653
        agentId: 'agent-a',
654
        sequence: 1,
655
        type: 'message_sent',
656
        occurredAt: new Date().toISOString(),
657
        messageHash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
658
        evidenceReferences: [],
659
        details: {},
660
      },
661
    });
662
    expect(response.statusCode).toBe(200);
663
    expect(events[0]).toMatchObject({
664
      token: 'live-session-token',
665
      value: { interactionId, agentId: 'agent-a', type: 'message_sent' },
666
    });
667
    const rawMessageAttempt = await app.inject({
668
      method: 'POST',
669
      url: `/sessions/${interactionId}/events`,
670
      headers: { authorization: 'Bearer live-session-token' },
671
      payload: {
672
        eventId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc',
673
        interactionId,
674
        agentId: 'agent-a',
675
        sequence: 2,
676
        type: 'message_sent',
677
        occurredAt: new Date().toISOString(),
678
        details: { message: 'raw conversation text' },
679
      },
680
    });
681
    expect(rawMessageAttempt.statusCode).toBe(400);
682
    expect(events).toHaveLength(1);
683
    await app.close();
684
  });
685
});