@fluxpointstudios/orynq-sdk-process-trace 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1012 +1,1012 @@
1
- /**
2
- * @summary Tests for trace building functionality in the process-trace package.
3
- */
4
-
5
- import { describe, it, expect, beforeEach } from 'vitest';
6
- import {
7
- createTrace,
8
- addSpan,
9
- addEvent,
10
- closeSpan,
11
- finalizeTrace,
12
- getSpan,
13
- getSpanEvents,
14
- isFinalized,
15
- getEventCount,
16
- getSpanCount,
17
- getRootSpans,
18
- getChildSpans,
19
- getEvent,
20
- getEventsByKind,
21
- DEFAULT_EVENT_VISIBILITY,
22
- } from '../index.js';
23
- import type {
24
- TraceRun,
25
- TraceSpan,
26
- TraceEvent,
27
- TraceBundle,
28
- CommandEvent,
29
- OutputEvent,
30
- DecisionEvent,
31
- } from '../index.js';
32
-
33
- // -----------------------------------------------------------------------------
34
- // createTrace Tests
35
- // -----------------------------------------------------------------------------
36
-
37
- describe('createTrace', () => {
38
- it('initializes correctly with required fields', async () => {
39
- const run = await createTrace({ agentId: 'test-agent' });
40
-
41
- expect(run).toHaveProperty('id');
42
- expect(run.id).toMatch(/^[0-9a-f-]{36}$/); // UUID format
43
- expect(run.schemaVersion).toBe('1.0');
44
- expect(run.agentId).toBe('test-agent');
45
- expect(run.status).toBe('running');
46
- expect(run.events).toEqual([]);
47
- expect(run.spans).toEqual([]);
48
- expect(run.nextSeq).toBe(0);
49
- expect(run.nextSpanSeq).toBe(0);
50
- });
51
-
52
- it('sets startedAt timestamp', async () => {
53
- const before = new Date().toISOString();
54
- const run = await createTrace({ agentId: 'test-agent' });
55
- const after = new Date().toISOString();
56
-
57
- expect(run.startedAt).toBeDefined();
58
- expect(run.startedAt >= before).toBe(true);
59
- expect(run.startedAt <= after).toBe(true);
60
- });
61
-
62
- it('initializes rolling hash to genesis', async () => {
63
- const run = await createTrace({ agentId: 'test-agent' });
64
-
65
- expect(run.rollingHash).toBeDefined();
66
- expect(run.rollingHash).toHaveLength(64);
67
- expect(run.rollingHash).toMatch(/^[a-f0-9]+$/);
68
- });
69
-
70
- it('accepts optional description', async () => {
71
- const run = await createTrace({
72
- agentId: 'test-agent',
73
- description: 'Test run description',
74
- });
75
-
76
- expect(run.metadata?.description).toBe('Test run description');
77
- });
78
-
79
- it('accepts optional metadata', async () => {
80
- const run = await createTrace({
81
- agentId: 'test-agent',
82
- metadata: {
83
- environment: 'test',
84
- version: '1.0.0',
85
- },
86
- });
87
-
88
- expect(run.metadata?.environment).toBe('test');
89
- expect(run.metadata?.version).toBe('1.0.0');
90
- });
91
-
92
- it('throws error for missing agentId', async () => {
93
- await expect(
94
- // @ts-expect-error - Testing runtime validation
95
- createTrace({})
96
- ).rejects.toThrow(/agentId/);
97
- });
98
-
99
- it('throws error for empty agentId', async () => {
100
- await expect(
101
- createTrace({ agentId: '' })
102
- ).rejects.toThrow(/agentId/);
103
- });
104
-
105
- it('generates unique run IDs', async () => {
106
- const run1 = await createTrace({ agentId: 'test-agent' });
107
- const run2 = await createTrace({ agentId: 'test-agent' });
108
-
109
- expect(run1.id).not.toBe(run2.id);
110
- });
111
- });
112
-
113
- // -----------------------------------------------------------------------------
114
- // addSpan Tests
115
- // -----------------------------------------------------------------------------
116
-
117
- describe('addSpan', () => {
118
- let run: TraceRun;
119
-
120
- beforeEach(async () => {
121
- run = await createTrace({ agentId: 'test-agent' });
122
- });
123
-
124
- it('creates span with correct fields', () => {
125
- const span = addSpan(run, { name: 'build' });
126
-
127
- expect(span.id).toMatch(/^[0-9a-f-]{36}$/);
128
- expect(span.spanSeq).toBe(0);
129
- expect(span.name).toBe('build');
130
- expect(span.status).toBe('running');
131
- expect(span.visibility).toBe('private'); // default
132
- expect(span.eventIds).toEqual([]);
133
- expect(span.childSpanIds).toEqual([]);
134
- expect(span.startedAt).toBeDefined();
135
- });
136
-
137
- it('increments spanSeq monotonically', () => {
138
- const span1 = addSpan(run, { name: 'span1' });
139
- const span2 = addSpan(run, { name: 'span2' });
140
- const span3 = addSpan(run, { name: 'span3' });
141
-
142
- expect(span1.spanSeq).toBe(0);
143
- expect(span2.spanSeq).toBe(1);
144
- expect(span3.spanSeq).toBe(2);
145
- });
146
-
147
- it('accepts custom visibility', () => {
148
- const publicSpan = addSpan(run, { name: 'public', visibility: 'public' });
149
- const privateSpan = addSpan(run, { name: 'private', visibility: 'private' });
150
- const secretSpan = addSpan(run, { name: 'secret', visibility: 'secret' });
151
-
152
- expect(publicSpan.visibility).toBe('public');
153
- expect(privateSpan.visibility).toBe('private');
154
- expect(secretSpan.visibility).toBe('secret');
155
- });
156
-
157
- it('accepts parent span ID', () => {
158
- const parentSpan = addSpan(run, { name: 'parent' });
159
- const childSpan = addSpan(run, { name: 'child', parentSpanId: parentSpan.id });
160
-
161
- expect(childSpan.parentSpanId).toBe(parentSpan.id);
162
- expect(parentSpan.childSpanIds).toContain(childSpan.id);
163
- });
164
-
165
- it('accepts metadata', () => {
166
- const span = addSpan(run, {
167
- name: 'build',
168
- metadata: { tool: 'npm', version: '10.0' },
169
- });
170
-
171
- expect(span.metadata?.tool).toBe('npm');
172
- expect(span.metadata?.version).toBe('10.0');
173
- });
174
-
175
- it('adds span to run.spans', () => {
176
- const span = addSpan(run, { name: 'build' });
177
-
178
- expect(run.spans).toContain(span);
179
- expect(run.spans).toHaveLength(1);
180
- });
181
-
182
- it('throws error for missing name', () => {
183
- expect(() =>
184
- // @ts-expect-error - Testing runtime validation
185
- addSpan(run, {})
186
- ).toThrow(/name/);
187
- });
188
-
189
- it('throws error for empty name', () => {
190
- expect(() =>
191
- addSpan(run, { name: '' })
192
- ).toThrow(/name/);
193
- });
194
-
195
- it('throws error for non-existent parent span', () => {
196
- expect(() =>
197
- addSpan(run, { name: 'child', parentSpanId: 'non-existent-id' })
198
- ).toThrow(/Parent span not found/);
199
- });
200
-
201
- it('throws error for closed parent span', async () => {
202
- const parentSpan = addSpan(run, { name: 'parent' });
203
- await closeSpan(run, parentSpan.id);
204
-
205
- expect(() =>
206
- addSpan(run, { name: 'child', parentSpanId: parentSpan.id })
207
- ).toThrow(/not running/);
208
- });
209
-
210
- it('throws error after finalization', async () => {
211
- const span = addSpan(run, { name: 'build' });
212
- await closeSpan(run, span.id);
213
- await finalizeTrace(run);
214
-
215
- expect(() =>
216
- addSpan(run, { name: 'new-span' })
217
- ).toThrow(/finalized/);
218
- });
219
- });
220
-
221
- // -----------------------------------------------------------------------------
222
- // addEvent Tests
223
- // -----------------------------------------------------------------------------
224
-
225
- describe('addEvent', () => {
226
- let run: TraceRun;
227
- let span: TraceSpan;
228
-
229
- beforeEach(async () => {
230
- run = await createTrace({ agentId: 'test-agent' });
231
- span = addSpan(run, { name: 'build' });
232
- });
233
-
234
- it('assigns seq number', async () => {
235
- const event = await addEvent(run, span.id, {
236
- kind: 'command',
237
- command: 'npm install',
238
- visibility: 'public',
239
- });
240
-
241
- expect(event.seq).toBe(0);
242
- });
243
-
244
- it('increments seq monotonically', async () => {
245
- const event1 = await addEvent(run, span.id, {
246
- kind: 'command',
247
- command: 'npm install',
248
- visibility: 'public',
249
- });
250
- const event2 = await addEvent(run, span.id, {
251
- kind: 'output',
252
- stream: 'stdout',
253
- content: 'done',
254
- visibility: 'private',
255
- });
256
- const event3 = await addEvent(run, span.id, {
257
- kind: 'command',
258
- command: 'npm build',
259
- visibility: 'public',
260
- });
261
-
262
- expect(event1.seq).toBe(0);
263
- expect(event2.seq).toBe(1);
264
- expect(event3.seq).toBe(2);
265
- });
266
-
267
- it('computes event hash', async () => {
268
- const event = await addEvent(run, span.id, {
269
- kind: 'command',
270
- command: 'npm install',
271
- visibility: 'public',
272
- });
273
-
274
- expect(event.hash).toBeDefined();
275
- expect(event.hash).toHaveLength(64);
276
- expect(event.hash).toMatch(/^[a-f0-9]+$/);
277
- });
278
-
279
- it('updates rolling hash', async () => {
280
- const initialHash = run.rollingHash;
281
-
282
- await addEvent(run, span.id, {
283
- kind: 'command',
284
- command: 'npm install',
285
- visibility: 'public',
286
- });
287
-
288
- expect(run.rollingHash).not.toBe(initialHash);
289
- expect(run.rollingHash).toHaveLength(64);
290
- });
291
-
292
- it('adds event ID to span.eventIds', async () => {
293
- const event = await addEvent(run, span.id, {
294
- kind: 'command',
295
- command: 'npm install',
296
- visibility: 'public',
297
- });
298
-
299
- expect(span.eventIds).toContain(event.id);
300
- });
301
-
302
- it('adds event to run.events', async () => {
303
- const event = await addEvent(run, span.id, {
304
- kind: 'command',
305
- command: 'npm install',
306
- visibility: 'public',
307
- });
308
-
309
- expect(run.events).toContain(event);
310
- });
311
-
312
- it('sets timestamp', async () => {
313
- const before = new Date().toISOString();
314
- const event = await addEvent(run, span.id, {
315
- kind: 'command',
316
- command: 'npm install',
317
- visibility: 'public',
318
- });
319
- const after = new Date().toISOString();
320
-
321
- expect(event.timestamp >= before).toBe(true);
322
- expect(event.timestamp <= after).toBe(true);
323
- });
324
-
325
- it('applies default visibility for command events', async () => {
326
- const event = await addEvent(run, span.id, {
327
- kind: 'command',
328
- command: 'npm install',
329
- });
330
-
331
- expect(event.visibility).toBe(DEFAULT_EVENT_VISIBILITY.command);
332
- expect(event.visibility).toBe('public');
333
- });
334
-
335
- it('applies default visibility for output events', async () => {
336
- const event = await addEvent(run, span.id, {
337
- kind: 'output',
338
- stream: 'stdout',
339
- content: 'output',
340
- });
341
-
342
- expect(event.visibility).toBe(DEFAULT_EVENT_VISIBILITY.output);
343
- expect(event.visibility).toBe('private');
344
- });
345
-
346
- it('applies default visibility for decision events', async () => {
347
- const event = await addEvent(run, span.id, {
348
- kind: 'decision',
349
- decision: 'use TypeScript',
350
- });
351
-
352
- expect(event.visibility).toBe(DEFAULT_EVENT_VISIBILITY.decision);
353
- expect(event.visibility).toBe('private');
354
- });
355
-
356
- it('allows overriding default visibility', async () => {
357
- const event = await addEvent(run, span.id, {
358
- kind: 'output',
359
- stream: 'stdout',
360
- content: 'public output',
361
- visibility: 'public',
362
- });
363
-
364
- expect(event.visibility).toBe('public');
365
- });
366
-
367
- it('throws error for non-existent span', async () => {
368
- await expect(
369
- addEvent(run, 'non-existent-span', {
370
- kind: 'command',
371
- command: 'npm install',
372
- visibility: 'public',
373
- })
374
- ).rejects.toThrow(/Span not found/);
375
- });
376
-
377
- it('throws error for closed span', async () => {
378
- await closeSpan(run, span.id);
379
-
380
- await expect(
381
- addEvent(run, span.id, {
382
- kind: 'command',
383
- command: 'npm install',
384
- visibility: 'public',
385
- })
386
- ).rejects.toThrow(/closed span/);
387
- });
388
-
389
- it('throws error after finalization', async () => {
390
- await closeSpan(run, span.id);
391
- await finalizeTrace(run);
392
-
393
- await expect(
394
- addEvent(run, span.id, {
395
- kind: 'command',
396
- command: 'npm install',
397
- visibility: 'public',
398
- })
399
- ).rejects.toThrow(/finalized/);
400
- });
401
-
402
- it('throws error for missing kind', async () => {
403
- await expect(
404
- addEvent(run, span.id, {
405
- // @ts-expect-error - Testing runtime validation
406
- command: 'npm install',
407
- })
408
- ).rejects.toThrow(/kind/);
409
- });
410
- });
411
-
412
- // -----------------------------------------------------------------------------
413
- // closeSpan Tests
414
- // -----------------------------------------------------------------------------
415
-
416
- describe('closeSpan', () => {
417
- let run: TraceRun;
418
- let span: TraceSpan;
419
-
420
- beforeEach(async () => {
421
- run = await createTrace({ agentId: 'test-agent' });
422
- span = addSpan(run, { name: 'build' });
423
- });
424
-
425
- it('sets endedAt timestamp', async () => {
426
- const before = new Date().toISOString();
427
- await closeSpan(run, span.id);
428
- const after = new Date().toISOString();
429
-
430
- expect(span.endedAt).toBeDefined();
431
- expect(span.endedAt! >= before).toBe(true);
432
- expect(span.endedAt! <= after).toBe(true);
433
- });
434
-
435
- it('sets durationMs', async () => {
436
- await closeSpan(run, span.id);
437
-
438
- expect(span.durationMs).toBeDefined();
439
- expect(span.durationMs).toBeGreaterThanOrEqual(0);
440
- });
441
-
442
- it('computes span hash', async () => {
443
- await addEvent(run, span.id, {
444
- kind: 'command',
445
- command: 'npm install',
446
- visibility: 'public',
447
- });
448
- await closeSpan(run, span.id);
449
-
450
- expect(span.hash).toBeDefined();
451
- expect(span.hash).toHaveLength(64);
452
- expect(span.hash).toMatch(/^[a-f0-9]+$/);
453
- });
454
-
455
- it('sets status to completed by default', async () => {
456
- await closeSpan(run, span.id);
457
-
458
- expect(span.status).toBe('completed');
459
- });
460
-
461
- it('accepts custom status', async () => {
462
- await closeSpan(run, span.id, 'failed');
463
-
464
- expect(span.status).toBe('failed');
465
- });
466
-
467
- it('throws error for non-existent span', async () => {
468
- await expect(
469
- closeSpan(run, 'non-existent-span')
470
- ).rejects.toThrow(/Span not found/);
471
- });
472
-
473
- it('throws error for already closed span', async () => {
474
- await closeSpan(run, span.id);
475
-
476
- await expect(
477
- closeSpan(run, span.id)
478
- ).rejects.toThrow(/already closed/);
479
- });
480
- });
481
-
482
- // -----------------------------------------------------------------------------
483
- // finalizeTrace Tests
484
- // -----------------------------------------------------------------------------
485
-
486
- describe('finalizeTrace', () => {
487
- let run: TraceRun;
488
-
489
- beforeEach(async () => {
490
- run = await createTrace({ agentId: 'test-agent' });
491
- });
492
-
493
- it('builds bundle with all hashes', async () => {
494
- const span = addSpan(run, { name: 'build', visibility: 'public' });
495
- await addEvent(run, span.id, {
496
- kind: 'command',
497
- command: 'npm install',
498
- visibility: 'public',
499
- });
500
- await closeSpan(run, span.id);
501
-
502
- const bundle = await finalizeTrace(run);
503
-
504
- expect(bundle).toHaveProperty('rootHash');
505
- expect(bundle).toHaveProperty('merkleRoot');
506
- expect(bundle).toHaveProperty('publicView');
507
- expect(bundle).toHaveProperty('privateRun');
508
- expect(bundle.rootHash).toHaveLength(64);
509
- expect(bundle.merkleRoot).toHaveLength(64);
510
- });
511
-
512
- it('sets run status to completed', async () => {
513
- const span = addSpan(run, { name: 'build' });
514
- await closeSpan(run, span.id);
515
-
516
- await finalizeTrace(run);
517
-
518
- expect(run.status).toBe('completed');
519
- });
520
-
521
- it('sets run endedAt and durationMs', async () => {
522
- const span = addSpan(run, { name: 'build' });
523
- await closeSpan(run, span.id);
524
-
525
- await finalizeTrace(run);
526
-
527
- expect(run.endedAt).toBeDefined();
528
- expect(run.durationMs).toBeDefined();
529
- expect(run.durationMs).toBeGreaterThanOrEqual(0);
530
- });
531
-
532
- it('closes open spans automatically', async () => {
533
- const span = addSpan(run, { name: 'build' });
534
- // Don't close the span
535
-
536
- await finalizeTrace(run);
537
-
538
- expect(span.status).toBe('completed');
539
- expect(span.hash).toBeDefined();
540
- });
541
-
542
- it('throws error if already finalized', async () => {
543
- const span = addSpan(run, { name: 'build' });
544
- await closeSpan(run, span.id);
545
- await finalizeTrace(run);
546
-
547
- await expect(
548
- finalizeTrace(run)
549
- ).rejects.toThrow(/already finalized/);
550
- });
551
-
552
- it('bundle contains public view with public spans', async () => {
553
- const publicSpan = addSpan(run, { name: 'public', visibility: 'public' });
554
- await addEvent(run, publicSpan.id, {
555
- kind: 'command',
556
- command: 'npm install',
557
- visibility: 'public',
558
- });
559
- await closeSpan(run, publicSpan.id);
560
-
561
- const privateSpan = addSpan(run, { name: 'private', visibility: 'private' });
562
- await addEvent(run, privateSpan.id, {
563
- kind: 'command',
564
- command: 'secret command',
565
- visibility: 'private',
566
- });
567
- await closeSpan(run, privateSpan.id);
568
-
569
- const bundle = await finalizeTrace(run);
570
-
571
- expect(bundle.publicView.publicSpans).toHaveLength(1);
572
- expect(bundle.publicView.publicSpans[0]?.name).toBe('public');
573
- expect(bundle.publicView.redactedSpanHashes).toHaveLength(1);
574
- });
575
-
576
- it('handles empty trace', async () => {
577
- const bundle = await finalizeTrace(run);
578
-
579
- expect(bundle.rootHash).toBeDefined();
580
- expect(bundle.publicView.publicSpans).toHaveLength(0);
581
- expect(bundle.publicView.totalSpans).toBe(0);
582
- expect(bundle.publicView.totalEvents).toBe(0);
583
- });
584
-
585
- it('formatVersion matches schema version', async () => {
586
- const span = addSpan(run, { name: 'build' });
587
- await closeSpan(run, span.id);
588
-
589
- const bundle = await finalizeTrace(run);
590
-
591
- expect(bundle.formatVersion).toBe('1.0');
592
- });
593
- });
594
-
595
- // -----------------------------------------------------------------------------
596
- // Query Function Tests
597
- // -----------------------------------------------------------------------------
598
-
599
- describe('getSpan', () => {
600
- let run: TraceRun;
601
-
602
- beforeEach(async () => {
603
- run = await createTrace({ agentId: 'test-agent' });
604
- });
605
-
606
- it('returns span by ID', () => {
607
- const span = addSpan(run, { name: 'build' });
608
-
609
- const found = getSpan(run, span.id);
610
-
611
- expect(found).toBe(span);
612
- });
613
-
614
- it('returns undefined for non-existent ID', () => {
615
- const found = getSpan(run, 'non-existent');
616
-
617
- expect(found).toBeUndefined();
618
- });
619
- });
620
-
621
- describe('getSpanEvents', () => {
622
- let run: TraceRun;
623
- let span: TraceSpan;
624
-
625
- beforeEach(async () => {
626
- run = await createTrace({ agentId: 'test-agent' });
627
- span = addSpan(run, { name: 'build' });
628
- });
629
-
630
- it('returns events for span sorted by seq', async () => {
631
- const event1 = await addEvent(run, span.id, {
632
- kind: 'command',
633
- command: 'first',
634
- visibility: 'public',
635
- });
636
- const event2 = await addEvent(run, span.id, {
637
- kind: 'output',
638
- stream: 'stdout',
639
- content: 'second',
640
- visibility: 'private',
641
- });
642
-
643
- const events = getSpanEvents(run, span.id);
644
-
645
- expect(events).toHaveLength(2);
646
- expect(events[0]).toBe(event1);
647
- expect(events[1]).toBe(event2);
648
- });
649
-
650
- it('returns empty array for span with no events', () => {
651
- const events = getSpanEvents(run, span.id);
652
-
653
- expect(events).toEqual([]);
654
- });
655
-
656
- it('returns empty array for non-existent span', () => {
657
- const events = getSpanEvents(run, 'non-existent');
658
-
659
- expect(events).toEqual([]);
660
- });
661
- });
662
-
663
- describe('isFinalized', () => {
664
- let run: TraceRun;
665
-
666
- beforeEach(async () => {
667
- run = await createTrace({ agentId: 'test-agent' });
668
- });
669
-
670
- it('returns false for new run', () => {
671
- expect(isFinalized(run)).toBe(false);
672
- });
673
-
674
- it('returns true after finalization', async () => {
675
- const span = addSpan(run, { name: 'build' });
676
- await closeSpan(run, span.id);
677
- await finalizeTrace(run);
678
-
679
- expect(isFinalized(run)).toBe(true);
680
- });
681
- });
682
-
683
- describe('getEventCount', () => {
684
- let run: TraceRun;
685
- let span: TraceSpan;
686
-
687
- beforeEach(async () => {
688
- run = await createTrace({ agentId: 'test-agent' });
689
- span = addSpan(run, { name: 'build' });
690
- });
691
-
692
- it('returns 0 for new run', () => {
693
- expect(getEventCount(run)).toBe(0);
694
- });
695
-
696
- it('returns correct count after adding events', async () => {
697
- await addEvent(run, span.id, {
698
- kind: 'command',
699
- command: 'first',
700
- visibility: 'public',
701
- });
702
- await addEvent(run, span.id, {
703
- kind: 'command',
704
- command: 'second',
705
- visibility: 'public',
706
- });
707
-
708
- expect(getEventCount(run)).toBe(2);
709
- });
710
- });
711
-
712
- describe('getSpanCount', () => {
713
- let run: TraceRun;
714
-
715
- beforeEach(async () => {
716
- run = await createTrace({ agentId: 'test-agent' });
717
- });
718
-
719
- it('returns 0 for new run', () => {
720
- expect(getSpanCount(run)).toBe(0);
721
- });
722
-
723
- it('returns correct count after adding spans', () => {
724
- addSpan(run, { name: 'span1' });
725
- addSpan(run, { name: 'span2' });
726
- addSpan(run, { name: 'span3' });
727
-
728
- expect(getSpanCount(run)).toBe(3);
729
- });
730
- });
731
-
732
- describe('getRootSpans', () => {
733
- let run: TraceRun;
734
-
735
- beforeEach(async () => {
736
- run = await createTrace({ agentId: 'test-agent' });
737
- });
738
-
739
- it('returns only spans without parents', () => {
740
- const root1 = addSpan(run, { name: 'root1' });
741
- const root2 = addSpan(run, { name: 'root2' });
742
- addSpan(run, { name: 'child1', parentSpanId: root1.id });
743
- addSpan(run, { name: 'child2', parentSpanId: root2.id });
744
-
745
- const rootSpans = getRootSpans(run);
746
-
747
- expect(rootSpans).toHaveLength(2);
748
- expect(rootSpans).toContain(root1);
749
- expect(rootSpans).toContain(root2);
750
- });
751
-
752
- it('returns empty array for no spans', () => {
753
- expect(getRootSpans(run)).toEqual([]);
754
- });
755
- });
756
-
757
- describe('getChildSpans', () => {
758
- let run: TraceRun;
759
-
760
- beforeEach(async () => {
761
- run = await createTrace({ agentId: 'test-agent' });
762
- });
763
-
764
- it('returns child spans for parent', () => {
765
- const parent = addSpan(run, { name: 'parent' });
766
- const child1 = addSpan(run, { name: 'child1', parentSpanId: parent.id });
767
- const child2 = addSpan(run, { name: 'child2', parentSpanId: parent.id });
768
-
769
- const children = getChildSpans(run, parent.id);
770
-
771
- expect(children).toHaveLength(2);
772
- expect(children).toContain(child1);
773
- expect(children).toContain(child2);
774
- });
775
-
776
- it('returns empty array for span with no children', () => {
777
- const span = addSpan(run, { name: 'lonely' });
778
-
779
- expect(getChildSpans(run, span.id)).toEqual([]);
780
- });
781
- });
782
-
783
- describe('getEvent', () => {
784
- let run: TraceRun;
785
- let span: TraceSpan;
786
-
787
- beforeEach(async () => {
788
- run = await createTrace({ agentId: 'test-agent' });
789
- span = addSpan(run, { name: 'build' });
790
- });
791
-
792
- it('returns event by ID', async () => {
793
- const event = await addEvent(run, span.id, {
794
- kind: 'command',
795
- command: 'npm install',
796
- visibility: 'public',
797
- });
798
-
799
- const found = getEvent(run, event.id);
800
-
801
- expect(found).toBe(event);
802
- });
803
-
804
- it('returns undefined for non-existent ID', () => {
805
- const found = getEvent(run, 'non-existent');
806
-
807
- expect(found).toBeUndefined();
808
- });
809
- });
810
-
811
- describe('getEventsByKind', () => {
812
- let run: TraceRun;
813
- let span: TraceSpan;
814
-
815
- beforeEach(async () => {
816
- run = await createTrace({ agentId: 'test-agent' });
817
- span = addSpan(run, { name: 'build' });
818
- });
819
-
820
- it('returns events of specified kind', async () => {
821
- const cmd1 = await addEvent(run, span.id, {
822
- kind: 'command',
823
- command: 'first',
824
- visibility: 'public',
825
- });
826
- await addEvent(run, span.id, {
827
- kind: 'output',
828
- stream: 'stdout',
829
- content: 'output',
830
- visibility: 'private',
831
- });
832
- const cmd2 = await addEvent(run, span.id, {
833
- kind: 'command',
834
- command: 'second',
835
- visibility: 'public',
836
- });
837
-
838
- const commands = getEventsByKind(run, 'command');
839
-
840
- expect(commands).toHaveLength(2);
841
- expect(commands).toContain(cmd1);
842
- expect(commands).toContain(cmd2);
843
- });
844
-
845
- it('returns empty array if no events of kind exist', async () => {
846
- await addEvent(run, span.id, {
847
- kind: 'command',
848
- command: 'test',
849
- visibility: 'public',
850
- });
851
-
852
- const decisions = getEventsByKind(run, 'decision');
853
-
854
- expect(decisions).toEqual([]);
855
- });
856
- });
857
-
858
- // -----------------------------------------------------------------------------
859
- // Event Ordering Tests
860
- // -----------------------------------------------------------------------------
861
-
862
- describe('event ordering', () => {
863
- let run: TraceRun;
864
-
865
- beforeEach(async () => {
866
- run = await createTrace({ agentId: 'test-agent' });
867
- });
868
-
869
- it('seq is monotonic across multiple spans', async () => {
870
- const span1 = addSpan(run, { name: 'span1' });
871
- const span2 = addSpan(run, { name: 'span2' });
872
-
873
- const e1 = await addEvent(run, span1.id, {
874
- kind: 'command',
875
- command: 'cmd1',
876
- visibility: 'public',
877
- });
878
- const e2 = await addEvent(run, span2.id, {
879
- kind: 'command',
880
- command: 'cmd2',
881
- visibility: 'public',
882
- });
883
- const e3 = await addEvent(run, span1.id, {
884
- kind: 'command',
885
- command: 'cmd3',
886
- visibility: 'public',
887
- });
888
-
889
- expect(e1.seq).toBe(0);
890
- expect(e2.seq).toBe(1);
891
- expect(e3.seq).toBe(2);
892
- });
893
-
894
- it('events are stored in insertion order', async () => {
895
- const span = addSpan(run, { name: 'span' });
896
-
897
- await addEvent(run, span.id, { kind: 'command', command: 'first', visibility: 'public' });
898
- await addEvent(run, span.id, { kind: 'command', command: 'second', visibility: 'public' });
899
- await addEvent(run, span.id, { kind: 'command', command: 'third', visibility: 'public' });
900
-
901
- expect(run.events[0]?.seq).toBe(0);
902
- expect(run.events[1]?.seq).toBe(1);
903
- expect(run.events[2]?.seq).toBe(2);
904
- });
905
- });
906
-
907
- // -----------------------------------------------------------------------------
908
- // Span Ordering Tests
909
- // -----------------------------------------------------------------------------
910
-
911
- describe('span ordering', () => {
912
- let run: TraceRun;
913
-
914
- beforeEach(async () => {
915
- run = await createTrace({ agentId: 'test-agent' });
916
- });
917
-
918
- it('spanSeq is monotonic', () => {
919
- const span1 = addSpan(run, { name: 'span1' });
920
- const span2 = addSpan(run, { name: 'span2' });
921
- const span3 = addSpan(run, { name: 'span3' });
922
-
923
- expect(span1.spanSeq).toBe(0);
924
- expect(span2.spanSeq).toBe(1);
925
- expect(span3.spanSeq).toBe(2);
926
- });
927
-
928
- it('child spans have higher spanSeq than parent', () => {
929
- const parent = addSpan(run, { name: 'parent' });
930
- const child = addSpan(run, { name: 'child', parentSpanId: parent.id });
931
- const grandchild = addSpan(run, { name: 'grandchild', parentSpanId: child.id });
932
-
933
- expect(child.spanSeq).toBeGreaterThan(parent.spanSeq);
934
- expect(grandchild.spanSeq).toBeGreaterThan(child.spanSeq);
935
- });
936
- });
937
-
938
- // -----------------------------------------------------------------------------
939
- // Visibility Defaults Tests
940
- // -----------------------------------------------------------------------------
941
-
942
- describe('visibility defaults', () => {
943
- let run: TraceRun;
944
- let span: TraceSpan;
945
-
946
- beforeEach(async () => {
947
- run = await createTrace({ agentId: 'test-agent' });
948
- span = addSpan(run, { name: 'build' });
949
- });
950
-
951
- it('command events are public by default', async () => {
952
- const event = await addEvent(run, span.id, {
953
- kind: 'command',
954
- command: 'npm install',
955
- });
956
-
957
- expect(event.visibility).toBe('public');
958
- });
959
-
960
- it('output events are private by default', async () => {
961
- const event = await addEvent(run, span.id, {
962
- kind: 'output',
963
- stream: 'stdout',
964
- content: 'output',
965
- });
966
-
967
- expect(event.visibility).toBe('private');
968
- });
969
-
970
- it('decision events are private by default', async () => {
971
- const event = await addEvent(run, span.id, {
972
- kind: 'decision',
973
- decision: 'use TypeScript',
974
- });
975
-
976
- expect(event.visibility).toBe('private');
977
- });
978
-
979
- it('observation events are public by default', async () => {
980
- const event = await addEvent(run, span.id, {
981
- kind: 'observation',
982
- observation: 'file exists',
983
- });
984
-
985
- expect(event.visibility).toBe('public');
986
- });
987
-
988
- it('error events are private by default', async () => {
989
- const event = await addEvent(run, span.id, {
990
- kind: 'error',
991
- error: 'Something went wrong',
992
- });
993
-
994
- expect(event.visibility).toBe('private');
995
- });
996
-
997
- it('custom events are private by default', async () => {
998
- const event = await addEvent(run, span.id, {
999
- kind: 'custom',
1000
- eventType: 'api-call',
1001
- data: { endpoint: '/api' },
1002
- });
1003
-
1004
- expect(event.visibility).toBe('private');
1005
- });
1006
-
1007
- it('spans are private by default', () => {
1008
- const span = addSpan(run, { name: 'build' });
1009
-
1010
- expect(span.visibility).toBe('private');
1011
- });
1012
- });
1
+ /**
2
+ * @summary Tests for trace building functionality in the process-trace package.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import {
7
+ createTrace,
8
+ addSpan,
9
+ addEvent,
10
+ closeSpan,
11
+ finalizeTrace,
12
+ getSpan,
13
+ getSpanEvents,
14
+ isFinalized,
15
+ getEventCount,
16
+ getSpanCount,
17
+ getRootSpans,
18
+ getChildSpans,
19
+ getEvent,
20
+ getEventsByKind,
21
+ DEFAULT_EVENT_VISIBILITY,
22
+ } from '../index.js';
23
+ import type {
24
+ TraceRun,
25
+ TraceSpan,
26
+ TraceEvent,
27
+ TraceBundle,
28
+ CommandEvent,
29
+ OutputEvent,
30
+ DecisionEvent,
31
+ } from '../index.js';
32
+
33
+ // -----------------------------------------------------------------------------
34
+ // createTrace Tests
35
+ // -----------------------------------------------------------------------------
36
+
37
+ describe('createTrace', () => {
38
+ it('initializes correctly with required fields', async () => {
39
+ const run = await createTrace({ agentId: 'test-agent' });
40
+
41
+ expect(run).toHaveProperty('id');
42
+ expect(run.id).toMatch(/^[0-9a-f-]{36}$/); // UUID format
43
+ expect(run.schemaVersion).toBe('1.0');
44
+ expect(run.agentId).toBe('test-agent');
45
+ expect(run.status).toBe('running');
46
+ expect(run.events).toEqual([]);
47
+ expect(run.spans).toEqual([]);
48
+ expect(run.nextSeq).toBe(0);
49
+ expect(run.nextSpanSeq).toBe(0);
50
+ });
51
+
52
+ it('sets startedAt timestamp', async () => {
53
+ const before = new Date().toISOString();
54
+ const run = await createTrace({ agentId: 'test-agent' });
55
+ const after = new Date().toISOString();
56
+
57
+ expect(run.startedAt).toBeDefined();
58
+ expect(run.startedAt >= before).toBe(true);
59
+ expect(run.startedAt <= after).toBe(true);
60
+ });
61
+
62
+ it('initializes rolling hash to genesis', async () => {
63
+ const run = await createTrace({ agentId: 'test-agent' });
64
+
65
+ expect(run.rollingHash).toBeDefined();
66
+ expect(run.rollingHash).toHaveLength(64);
67
+ expect(run.rollingHash).toMatch(/^[a-f0-9]+$/);
68
+ });
69
+
70
+ it('accepts optional description', async () => {
71
+ const run = await createTrace({
72
+ agentId: 'test-agent',
73
+ description: 'Test run description',
74
+ });
75
+
76
+ expect(run.metadata?.description).toBe('Test run description');
77
+ });
78
+
79
+ it('accepts optional metadata', async () => {
80
+ const run = await createTrace({
81
+ agentId: 'test-agent',
82
+ metadata: {
83
+ environment: 'test',
84
+ version: '1.0.0',
85
+ },
86
+ });
87
+
88
+ expect(run.metadata?.environment).toBe('test');
89
+ expect(run.metadata?.version).toBe('1.0.0');
90
+ });
91
+
92
+ it('throws error for missing agentId', async () => {
93
+ await expect(
94
+ // @ts-expect-error - Testing runtime validation
95
+ createTrace({})
96
+ ).rejects.toThrow(/agentId/);
97
+ });
98
+
99
+ it('throws error for empty agentId', async () => {
100
+ await expect(
101
+ createTrace({ agentId: '' })
102
+ ).rejects.toThrow(/agentId/);
103
+ });
104
+
105
+ it('generates unique run IDs', async () => {
106
+ const run1 = await createTrace({ agentId: 'test-agent' });
107
+ const run2 = await createTrace({ agentId: 'test-agent' });
108
+
109
+ expect(run1.id).not.toBe(run2.id);
110
+ });
111
+ });
112
+
113
+ // -----------------------------------------------------------------------------
114
+ // addSpan Tests
115
+ // -----------------------------------------------------------------------------
116
+
117
+ describe('addSpan', () => {
118
+ let run: TraceRun;
119
+
120
+ beforeEach(async () => {
121
+ run = await createTrace({ agentId: 'test-agent' });
122
+ });
123
+
124
+ it('creates span with correct fields', () => {
125
+ const span = addSpan(run, { name: 'build' });
126
+
127
+ expect(span.id).toMatch(/^[0-9a-f-]{36}$/);
128
+ expect(span.spanSeq).toBe(0);
129
+ expect(span.name).toBe('build');
130
+ expect(span.status).toBe('running');
131
+ expect(span.visibility).toBe('private'); // default
132
+ expect(span.eventIds).toEqual([]);
133
+ expect(span.childSpanIds).toEqual([]);
134
+ expect(span.startedAt).toBeDefined();
135
+ });
136
+
137
+ it('increments spanSeq monotonically', () => {
138
+ const span1 = addSpan(run, { name: 'span1' });
139
+ const span2 = addSpan(run, { name: 'span2' });
140
+ const span3 = addSpan(run, { name: 'span3' });
141
+
142
+ expect(span1.spanSeq).toBe(0);
143
+ expect(span2.spanSeq).toBe(1);
144
+ expect(span3.spanSeq).toBe(2);
145
+ });
146
+
147
+ it('accepts custom visibility', () => {
148
+ const publicSpan = addSpan(run, { name: 'public', visibility: 'public' });
149
+ const privateSpan = addSpan(run, { name: 'private', visibility: 'private' });
150
+ const secretSpan = addSpan(run, { name: 'secret', visibility: 'secret' });
151
+
152
+ expect(publicSpan.visibility).toBe('public');
153
+ expect(privateSpan.visibility).toBe('private');
154
+ expect(secretSpan.visibility).toBe('secret');
155
+ });
156
+
157
+ it('accepts parent span ID', () => {
158
+ const parentSpan = addSpan(run, { name: 'parent' });
159
+ const childSpan = addSpan(run, { name: 'child', parentSpanId: parentSpan.id });
160
+
161
+ expect(childSpan.parentSpanId).toBe(parentSpan.id);
162
+ expect(parentSpan.childSpanIds).toContain(childSpan.id);
163
+ });
164
+
165
+ it('accepts metadata', () => {
166
+ const span = addSpan(run, {
167
+ name: 'build',
168
+ metadata: { tool: 'npm', version: '10.0' },
169
+ });
170
+
171
+ expect(span.metadata?.tool).toBe('npm');
172
+ expect(span.metadata?.version).toBe('10.0');
173
+ });
174
+
175
+ it('adds span to run.spans', () => {
176
+ const span = addSpan(run, { name: 'build' });
177
+
178
+ expect(run.spans).toContain(span);
179
+ expect(run.spans).toHaveLength(1);
180
+ });
181
+
182
+ it('throws error for missing name', () => {
183
+ expect(() =>
184
+ // @ts-expect-error - Testing runtime validation
185
+ addSpan(run, {})
186
+ ).toThrow(/name/);
187
+ });
188
+
189
+ it('throws error for empty name', () => {
190
+ expect(() =>
191
+ addSpan(run, { name: '' })
192
+ ).toThrow(/name/);
193
+ });
194
+
195
+ it('throws error for non-existent parent span', () => {
196
+ expect(() =>
197
+ addSpan(run, { name: 'child', parentSpanId: 'non-existent-id' })
198
+ ).toThrow(/Parent span not found/);
199
+ });
200
+
201
+ it('throws error for closed parent span', async () => {
202
+ const parentSpan = addSpan(run, { name: 'parent' });
203
+ await closeSpan(run, parentSpan.id);
204
+
205
+ expect(() =>
206
+ addSpan(run, { name: 'child', parentSpanId: parentSpan.id })
207
+ ).toThrow(/not running/);
208
+ });
209
+
210
+ it('throws error after finalization', async () => {
211
+ const span = addSpan(run, { name: 'build' });
212
+ await closeSpan(run, span.id);
213
+ await finalizeTrace(run);
214
+
215
+ expect(() =>
216
+ addSpan(run, { name: 'new-span' })
217
+ ).toThrow(/finalized/);
218
+ });
219
+ });
220
+
221
+ // -----------------------------------------------------------------------------
222
+ // addEvent Tests
223
+ // -----------------------------------------------------------------------------
224
+
225
+ describe('addEvent', () => {
226
+ let run: TraceRun;
227
+ let span: TraceSpan;
228
+
229
+ beforeEach(async () => {
230
+ run = await createTrace({ agentId: 'test-agent' });
231
+ span = addSpan(run, { name: 'build' });
232
+ });
233
+
234
+ it('assigns seq number', async () => {
235
+ const event = await addEvent(run, span.id, {
236
+ kind: 'command',
237
+ command: 'npm install',
238
+ visibility: 'public',
239
+ });
240
+
241
+ expect(event.seq).toBe(0);
242
+ });
243
+
244
+ it('increments seq monotonically', async () => {
245
+ const event1 = await addEvent(run, span.id, {
246
+ kind: 'command',
247
+ command: 'npm install',
248
+ visibility: 'public',
249
+ });
250
+ const event2 = await addEvent(run, span.id, {
251
+ kind: 'output',
252
+ stream: 'stdout',
253
+ content: 'done',
254
+ visibility: 'private',
255
+ });
256
+ const event3 = await addEvent(run, span.id, {
257
+ kind: 'command',
258
+ command: 'npm build',
259
+ visibility: 'public',
260
+ });
261
+
262
+ expect(event1.seq).toBe(0);
263
+ expect(event2.seq).toBe(1);
264
+ expect(event3.seq).toBe(2);
265
+ });
266
+
267
+ it('computes event hash', async () => {
268
+ const event = await addEvent(run, span.id, {
269
+ kind: 'command',
270
+ command: 'npm install',
271
+ visibility: 'public',
272
+ });
273
+
274
+ expect(event.hash).toBeDefined();
275
+ expect(event.hash).toHaveLength(64);
276
+ expect(event.hash).toMatch(/^[a-f0-9]+$/);
277
+ });
278
+
279
+ it('updates rolling hash', async () => {
280
+ const initialHash = run.rollingHash;
281
+
282
+ await addEvent(run, span.id, {
283
+ kind: 'command',
284
+ command: 'npm install',
285
+ visibility: 'public',
286
+ });
287
+
288
+ expect(run.rollingHash).not.toBe(initialHash);
289
+ expect(run.rollingHash).toHaveLength(64);
290
+ });
291
+
292
+ it('adds event ID to span.eventIds', async () => {
293
+ const event = await addEvent(run, span.id, {
294
+ kind: 'command',
295
+ command: 'npm install',
296
+ visibility: 'public',
297
+ });
298
+
299
+ expect(span.eventIds).toContain(event.id);
300
+ });
301
+
302
+ it('adds event to run.events', async () => {
303
+ const event = await addEvent(run, span.id, {
304
+ kind: 'command',
305
+ command: 'npm install',
306
+ visibility: 'public',
307
+ });
308
+
309
+ expect(run.events).toContain(event);
310
+ });
311
+
312
+ it('sets timestamp', async () => {
313
+ const before = new Date().toISOString();
314
+ const event = await addEvent(run, span.id, {
315
+ kind: 'command',
316
+ command: 'npm install',
317
+ visibility: 'public',
318
+ });
319
+ const after = new Date().toISOString();
320
+
321
+ expect(event.timestamp >= before).toBe(true);
322
+ expect(event.timestamp <= after).toBe(true);
323
+ });
324
+
325
+ it('applies default visibility for command events', async () => {
326
+ const event = await addEvent(run, span.id, {
327
+ kind: 'command',
328
+ command: 'npm install',
329
+ });
330
+
331
+ expect(event.visibility).toBe(DEFAULT_EVENT_VISIBILITY.command);
332
+ expect(event.visibility).toBe('public');
333
+ });
334
+
335
+ it('applies default visibility for output events', async () => {
336
+ const event = await addEvent(run, span.id, {
337
+ kind: 'output',
338
+ stream: 'stdout',
339
+ content: 'output',
340
+ });
341
+
342
+ expect(event.visibility).toBe(DEFAULT_EVENT_VISIBILITY.output);
343
+ expect(event.visibility).toBe('private');
344
+ });
345
+
346
+ it('applies default visibility for decision events', async () => {
347
+ const event = await addEvent(run, span.id, {
348
+ kind: 'decision',
349
+ decision: 'use TypeScript',
350
+ });
351
+
352
+ expect(event.visibility).toBe(DEFAULT_EVENT_VISIBILITY.decision);
353
+ expect(event.visibility).toBe('private');
354
+ });
355
+
356
+ it('allows overriding default visibility', async () => {
357
+ const event = await addEvent(run, span.id, {
358
+ kind: 'output',
359
+ stream: 'stdout',
360
+ content: 'public output',
361
+ visibility: 'public',
362
+ });
363
+
364
+ expect(event.visibility).toBe('public');
365
+ });
366
+
367
+ it('throws error for non-existent span', async () => {
368
+ await expect(
369
+ addEvent(run, 'non-existent-span', {
370
+ kind: 'command',
371
+ command: 'npm install',
372
+ visibility: 'public',
373
+ })
374
+ ).rejects.toThrow(/Span not found/);
375
+ });
376
+
377
+ it('throws error for closed span', async () => {
378
+ await closeSpan(run, span.id);
379
+
380
+ await expect(
381
+ addEvent(run, span.id, {
382
+ kind: 'command',
383
+ command: 'npm install',
384
+ visibility: 'public',
385
+ })
386
+ ).rejects.toThrow(/closed span/);
387
+ });
388
+
389
+ it('throws error after finalization', async () => {
390
+ await closeSpan(run, span.id);
391
+ await finalizeTrace(run);
392
+
393
+ await expect(
394
+ addEvent(run, span.id, {
395
+ kind: 'command',
396
+ command: 'npm install',
397
+ visibility: 'public',
398
+ })
399
+ ).rejects.toThrow(/finalized/);
400
+ });
401
+
402
+ it('throws error for missing kind', async () => {
403
+ await expect(
404
+ addEvent(run, span.id, {
405
+ // @ts-expect-error - Testing runtime validation
406
+ command: 'npm install',
407
+ })
408
+ ).rejects.toThrow(/kind/);
409
+ });
410
+ });
411
+
412
+ // -----------------------------------------------------------------------------
413
+ // closeSpan Tests
414
+ // -----------------------------------------------------------------------------
415
+
416
+ describe('closeSpan', () => {
417
+ let run: TraceRun;
418
+ let span: TraceSpan;
419
+
420
+ beforeEach(async () => {
421
+ run = await createTrace({ agentId: 'test-agent' });
422
+ span = addSpan(run, { name: 'build' });
423
+ });
424
+
425
+ it('sets endedAt timestamp', async () => {
426
+ const before = new Date().toISOString();
427
+ await closeSpan(run, span.id);
428
+ const after = new Date().toISOString();
429
+
430
+ expect(span.endedAt).toBeDefined();
431
+ expect(span.endedAt! >= before).toBe(true);
432
+ expect(span.endedAt! <= after).toBe(true);
433
+ });
434
+
435
+ it('sets durationMs', async () => {
436
+ await closeSpan(run, span.id);
437
+
438
+ expect(span.durationMs).toBeDefined();
439
+ expect(span.durationMs).toBeGreaterThanOrEqual(0);
440
+ });
441
+
442
+ it('computes span hash', async () => {
443
+ await addEvent(run, span.id, {
444
+ kind: 'command',
445
+ command: 'npm install',
446
+ visibility: 'public',
447
+ });
448
+ await closeSpan(run, span.id);
449
+
450
+ expect(span.hash).toBeDefined();
451
+ expect(span.hash).toHaveLength(64);
452
+ expect(span.hash).toMatch(/^[a-f0-9]+$/);
453
+ });
454
+
455
+ it('sets status to completed by default', async () => {
456
+ await closeSpan(run, span.id);
457
+
458
+ expect(span.status).toBe('completed');
459
+ });
460
+
461
+ it('accepts custom status', async () => {
462
+ await closeSpan(run, span.id, 'failed');
463
+
464
+ expect(span.status).toBe('failed');
465
+ });
466
+
467
+ it('throws error for non-existent span', async () => {
468
+ await expect(
469
+ closeSpan(run, 'non-existent-span')
470
+ ).rejects.toThrow(/Span not found/);
471
+ });
472
+
473
+ it('throws error for already closed span', async () => {
474
+ await closeSpan(run, span.id);
475
+
476
+ await expect(
477
+ closeSpan(run, span.id)
478
+ ).rejects.toThrow(/already closed/);
479
+ });
480
+ });
481
+
482
+ // -----------------------------------------------------------------------------
483
+ // finalizeTrace Tests
484
+ // -----------------------------------------------------------------------------
485
+
486
+ describe('finalizeTrace', () => {
487
+ let run: TraceRun;
488
+
489
+ beforeEach(async () => {
490
+ run = await createTrace({ agentId: 'test-agent' });
491
+ });
492
+
493
+ it('builds bundle with all hashes', async () => {
494
+ const span = addSpan(run, { name: 'build', visibility: 'public' });
495
+ await addEvent(run, span.id, {
496
+ kind: 'command',
497
+ command: 'npm install',
498
+ visibility: 'public',
499
+ });
500
+ await closeSpan(run, span.id);
501
+
502
+ const bundle = await finalizeTrace(run);
503
+
504
+ expect(bundle).toHaveProperty('rootHash');
505
+ expect(bundle).toHaveProperty('merkleRoot');
506
+ expect(bundle).toHaveProperty('publicView');
507
+ expect(bundle).toHaveProperty('privateRun');
508
+ expect(bundle.rootHash).toHaveLength(64);
509
+ expect(bundle.merkleRoot).toHaveLength(64);
510
+ });
511
+
512
+ it('sets run status to completed', async () => {
513
+ const span = addSpan(run, { name: 'build' });
514
+ await closeSpan(run, span.id);
515
+
516
+ await finalizeTrace(run);
517
+
518
+ expect(run.status).toBe('completed');
519
+ });
520
+
521
+ it('sets run endedAt and durationMs', async () => {
522
+ const span = addSpan(run, { name: 'build' });
523
+ await closeSpan(run, span.id);
524
+
525
+ await finalizeTrace(run);
526
+
527
+ expect(run.endedAt).toBeDefined();
528
+ expect(run.durationMs).toBeDefined();
529
+ expect(run.durationMs).toBeGreaterThanOrEqual(0);
530
+ });
531
+
532
+ it('closes open spans automatically', async () => {
533
+ const span = addSpan(run, { name: 'build' });
534
+ // Don't close the span
535
+
536
+ await finalizeTrace(run);
537
+
538
+ expect(span.status).toBe('completed');
539
+ expect(span.hash).toBeDefined();
540
+ });
541
+
542
+ it('throws error if already finalized', async () => {
543
+ const span = addSpan(run, { name: 'build' });
544
+ await closeSpan(run, span.id);
545
+ await finalizeTrace(run);
546
+
547
+ await expect(
548
+ finalizeTrace(run)
549
+ ).rejects.toThrow(/already finalized/);
550
+ });
551
+
552
+ it('bundle contains public view with public spans', async () => {
553
+ const publicSpan = addSpan(run, { name: 'public', visibility: 'public' });
554
+ await addEvent(run, publicSpan.id, {
555
+ kind: 'command',
556
+ command: 'npm install',
557
+ visibility: 'public',
558
+ });
559
+ await closeSpan(run, publicSpan.id);
560
+
561
+ const privateSpan = addSpan(run, { name: 'private', visibility: 'private' });
562
+ await addEvent(run, privateSpan.id, {
563
+ kind: 'command',
564
+ command: 'secret command',
565
+ visibility: 'private',
566
+ });
567
+ await closeSpan(run, privateSpan.id);
568
+
569
+ const bundle = await finalizeTrace(run);
570
+
571
+ expect(bundle.publicView.publicSpans).toHaveLength(1);
572
+ expect(bundle.publicView.publicSpans[0]?.name).toBe('public');
573
+ expect(bundle.publicView.redactedSpanHashes).toHaveLength(1);
574
+ });
575
+
576
+ it('handles empty trace', async () => {
577
+ const bundle = await finalizeTrace(run);
578
+
579
+ expect(bundle.rootHash).toBeDefined();
580
+ expect(bundle.publicView.publicSpans).toHaveLength(0);
581
+ expect(bundle.publicView.totalSpans).toBe(0);
582
+ expect(bundle.publicView.totalEvents).toBe(0);
583
+ });
584
+
585
+ it('formatVersion matches schema version', async () => {
586
+ const span = addSpan(run, { name: 'build' });
587
+ await closeSpan(run, span.id);
588
+
589
+ const bundle = await finalizeTrace(run);
590
+
591
+ expect(bundle.formatVersion).toBe('1.0');
592
+ });
593
+ });
594
+
595
+ // -----------------------------------------------------------------------------
596
+ // Query Function Tests
597
+ // -----------------------------------------------------------------------------
598
+
599
+ describe('getSpan', () => {
600
+ let run: TraceRun;
601
+
602
+ beforeEach(async () => {
603
+ run = await createTrace({ agentId: 'test-agent' });
604
+ });
605
+
606
+ it('returns span by ID', () => {
607
+ const span = addSpan(run, { name: 'build' });
608
+
609
+ const found = getSpan(run, span.id);
610
+
611
+ expect(found).toBe(span);
612
+ });
613
+
614
+ it('returns undefined for non-existent ID', () => {
615
+ const found = getSpan(run, 'non-existent');
616
+
617
+ expect(found).toBeUndefined();
618
+ });
619
+ });
620
+
621
+ describe('getSpanEvents', () => {
622
+ let run: TraceRun;
623
+ let span: TraceSpan;
624
+
625
+ beforeEach(async () => {
626
+ run = await createTrace({ agentId: 'test-agent' });
627
+ span = addSpan(run, { name: 'build' });
628
+ });
629
+
630
+ it('returns events for span sorted by seq', async () => {
631
+ const event1 = await addEvent(run, span.id, {
632
+ kind: 'command',
633
+ command: 'first',
634
+ visibility: 'public',
635
+ });
636
+ const event2 = await addEvent(run, span.id, {
637
+ kind: 'output',
638
+ stream: 'stdout',
639
+ content: 'second',
640
+ visibility: 'private',
641
+ });
642
+
643
+ const events = getSpanEvents(run, span.id);
644
+
645
+ expect(events).toHaveLength(2);
646
+ expect(events[0]).toBe(event1);
647
+ expect(events[1]).toBe(event2);
648
+ });
649
+
650
+ it('returns empty array for span with no events', () => {
651
+ const events = getSpanEvents(run, span.id);
652
+
653
+ expect(events).toEqual([]);
654
+ });
655
+
656
+ it('returns empty array for non-existent span', () => {
657
+ const events = getSpanEvents(run, 'non-existent');
658
+
659
+ expect(events).toEqual([]);
660
+ });
661
+ });
662
+
663
+ describe('isFinalized', () => {
664
+ let run: TraceRun;
665
+
666
+ beforeEach(async () => {
667
+ run = await createTrace({ agentId: 'test-agent' });
668
+ });
669
+
670
+ it('returns false for new run', () => {
671
+ expect(isFinalized(run)).toBe(false);
672
+ });
673
+
674
+ it('returns true after finalization', async () => {
675
+ const span = addSpan(run, { name: 'build' });
676
+ await closeSpan(run, span.id);
677
+ await finalizeTrace(run);
678
+
679
+ expect(isFinalized(run)).toBe(true);
680
+ });
681
+ });
682
+
683
+ describe('getEventCount', () => {
684
+ let run: TraceRun;
685
+ let span: TraceSpan;
686
+
687
+ beforeEach(async () => {
688
+ run = await createTrace({ agentId: 'test-agent' });
689
+ span = addSpan(run, { name: 'build' });
690
+ });
691
+
692
+ it('returns 0 for new run', () => {
693
+ expect(getEventCount(run)).toBe(0);
694
+ });
695
+
696
+ it('returns correct count after adding events', async () => {
697
+ await addEvent(run, span.id, {
698
+ kind: 'command',
699
+ command: 'first',
700
+ visibility: 'public',
701
+ });
702
+ await addEvent(run, span.id, {
703
+ kind: 'command',
704
+ command: 'second',
705
+ visibility: 'public',
706
+ });
707
+
708
+ expect(getEventCount(run)).toBe(2);
709
+ });
710
+ });
711
+
712
+ describe('getSpanCount', () => {
713
+ let run: TraceRun;
714
+
715
+ beforeEach(async () => {
716
+ run = await createTrace({ agentId: 'test-agent' });
717
+ });
718
+
719
+ it('returns 0 for new run', () => {
720
+ expect(getSpanCount(run)).toBe(0);
721
+ });
722
+
723
+ it('returns correct count after adding spans', () => {
724
+ addSpan(run, { name: 'span1' });
725
+ addSpan(run, { name: 'span2' });
726
+ addSpan(run, { name: 'span3' });
727
+
728
+ expect(getSpanCount(run)).toBe(3);
729
+ });
730
+ });
731
+
732
+ describe('getRootSpans', () => {
733
+ let run: TraceRun;
734
+
735
+ beforeEach(async () => {
736
+ run = await createTrace({ agentId: 'test-agent' });
737
+ });
738
+
739
+ it('returns only spans without parents', () => {
740
+ const root1 = addSpan(run, { name: 'root1' });
741
+ const root2 = addSpan(run, { name: 'root2' });
742
+ addSpan(run, { name: 'child1', parentSpanId: root1.id });
743
+ addSpan(run, { name: 'child2', parentSpanId: root2.id });
744
+
745
+ const rootSpans = getRootSpans(run);
746
+
747
+ expect(rootSpans).toHaveLength(2);
748
+ expect(rootSpans).toContain(root1);
749
+ expect(rootSpans).toContain(root2);
750
+ });
751
+
752
+ it('returns empty array for no spans', () => {
753
+ expect(getRootSpans(run)).toEqual([]);
754
+ });
755
+ });
756
+
757
+ describe('getChildSpans', () => {
758
+ let run: TraceRun;
759
+
760
+ beforeEach(async () => {
761
+ run = await createTrace({ agentId: 'test-agent' });
762
+ });
763
+
764
+ it('returns child spans for parent', () => {
765
+ const parent = addSpan(run, { name: 'parent' });
766
+ const child1 = addSpan(run, { name: 'child1', parentSpanId: parent.id });
767
+ const child2 = addSpan(run, { name: 'child2', parentSpanId: parent.id });
768
+
769
+ const children = getChildSpans(run, parent.id);
770
+
771
+ expect(children).toHaveLength(2);
772
+ expect(children).toContain(child1);
773
+ expect(children).toContain(child2);
774
+ });
775
+
776
+ it('returns empty array for span with no children', () => {
777
+ const span = addSpan(run, { name: 'lonely' });
778
+
779
+ expect(getChildSpans(run, span.id)).toEqual([]);
780
+ });
781
+ });
782
+
783
+ describe('getEvent', () => {
784
+ let run: TraceRun;
785
+ let span: TraceSpan;
786
+
787
+ beforeEach(async () => {
788
+ run = await createTrace({ agentId: 'test-agent' });
789
+ span = addSpan(run, { name: 'build' });
790
+ });
791
+
792
+ it('returns event by ID', async () => {
793
+ const event = await addEvent(run, span.id, {
794
+ kind: 'command',
795
+ command: 'npm install',
796
+ visibility: 'public',
797
+ });
798
+
799
+ const found = getEvent(run, event.id);
800
+
801
+ expect(found).toBe(event);
802
+ });
803
+
804
+ it('returns undefined for non-existent ID', () => {
805
+ const found = getEvent(run, 'non-existent');
806
+
807
+ expect(found).toBeUndefined();
808
+ });
809
+ });
810
+
811
+ describe('getEventsByKind', () => {
812
+ let run: TraceRun;
813
+ let span: TraceSpan;
814
+
815
+ beforeEach(async () => {
816
+ run = await createTrace({ agentId: 'test-agent' });
817
+ span = addSpan(run, { name: 'build' });
818
+ });
819
+
820
+ it('returns events of specified kind', async () => {
821
+ const cmd1 = await addEvent(run, span.id, {
822
+ kind: 'command',
823
+ command: 'first',
824
+ visibility: 'public',
825
+ });
826
+ await addEvent(run, span.id, {
827
+ kind: 'output',
828
+ stream: 'stdout',
829
+ content: 'output',
830
+ visibility: 'private',
831
+ });
832
+ const cmd2 = await addEvent(run, span.id, {
833
+ kind: 'command',
834
+ command: 'second',
835
+ visibility: 'public',
836
+ });
837
+
838
+ const commands = getEventsByKind(run, 'command');
839
+
840
+ expect(commands).toHaveLength(2);
841
+ expect(commands).toContain(cmd1);
842
+ expect(commands).toContain(cmd2);
843
+ });
844
+
845
+ it('returns empty array if no events of kind exist', async () => {
846
+ await addEvent(run, span.id, {
847
+ kind: 'command',
848
+ command: 'test',
849
+ visibility: 'public',
850
+ });
851
+
852
+ const decisions = getEventsByKind(run, 'decision');
853
+
854
+ expect(decisions).toEqual([]);
855
+ });
856
+ });
857
+
858
+ // -----------------------------------------------------------------------------
859
+ // Event Ordering Tests
860
+ // -----------------------------------------------------------------------------
861
+
862
+ describe('event ordering', () => {
863
+ let run: TraceRun;
864
+
865
+ beforeEach(async () => {
866
+ run = await createTrace({ agentId: 'test-agent' });
867
+ });
868
+
869
+ it('seq is monotonic across multiple spans', async () => {
870
+ const span1 = addSpan(run, { name: 'span1' });
871
+ const span2 = addSpan(run, { name: 'span2' });
872
+
873
+ const e1 = await addEvent(run, span1.id, {
874
+ kind: 'command',
875
+ command: 'cmd1',
876
+ visibility: 'public',
877
+ });
878
+ const e2 = await addEvent(run, span2.id, {
879
+ kind: 'command',
880
+ command: 'cmd2',
881
+ visibility: 'public',
882
+ });
883
+ const e3 = await addEvent(run, span1.id, {
884
+ kind: 'command',
885
+ command: 'cmd3',
886
+ visibility: 'public',
887
+ });
888
+
889
+ expect(e1.seq).toBe(0);
890
+ expect(e2.seq).toBe(1);
891
+ expect(e3.seq).toBe(2);
892
+ });
893
+
894
+ it('events are stored in insertion order', async () => {
895
+ const span = addSpan(run, { name: 'span' });
896
+
897
+ await addEvent(run, span.id, { kind: 'command', command: 'first', visibility: 'public' });
898
+ await addEvent(run, span.id, { kind: 'command', command: 'second', visibility: 'public' });
899
+ await addEvent(run, span.id, { kind: 'command', command: 'third', visibility: 'public' });
900
+
901
+ expect(run.events[0]?.seq).toBe(0);
902
+ expect(run.events[1]?.seq).toBe(1);
903
+ expect(run.events[2]?.seq).toBe(2);
904
+ });
905
+ });
906
+
907
+ // -----------------------------------------------------------------------------
908
+ // Span Ordering Tests
909
+ // -----------------------------------------------------------------------------
910
+
911
+ describe('span ordering', () => {
912
+ let run: TraceRun;
913
+
914
+ beforeEach(async () => {
915
+ run = await createTrace({ agentId: 'test-agent' });
916
+ });
917
+
918
+ it('spanSeq is monotonic', () => {
919
+ const span1 = addSpan(run, { name: 'span1' });
920
+ const span2 = addSpan(run, { name: 'span2' });
921
+ const span3 = addSpan(run, { name: 'span3' });
922
+
923
+ expect(span1.spanSeq).toBe(0);
924
+ expect(span2.spanSeq).toBe(1);
925
+ expect(span3.spanSeq).toBe(2);
926
+ });
927
+
928
+ it('child spans have higher spanSeq than parent', () => {
929
+ const parent = addSpan(run, { name: 'parent' });
930
+ const child = addSpan(run, { name: 'child', parentSpanId: parent.id });
931
+ const grandchild = addSpan(run, { name: 'grandchild', parentSpanId: child.id });
932
+
933
+ expect(child.spanSeq).toBeGreaterThan(parent.spanSeq);
934
+ expect(grandchild.spanSeq).toBeGreaterThan(child.spanSeq);
935
+ });
936
+ });
937
+
938
+ // -----------------------------------------------------------------------------
939
+ // Visibility Defaults Tests
940
+ // -----------------------------------------------------------------------------
941
+
942
+ describe('visibility defaults', () => {
943
+ let run: TraceRun;
944
+ let span: TraceSpan;
945
+
946
+ beforeEach(async () => {
947
+ run = await createTrace({ agentId: 'test-agent' });
948
+ span = addSpan(run, { name: 'build' });
949
+ });
950
+
951
+ it('command events are public by default', async () => {
952
+ const event = await addEvent(run, span.id, {
953
+ kind: 'command',
954
+ command: 'npm install',
955
+ });
956
+
957
+ expect(event.visibility).toBe('public');
958
+ });
959
+
960
+ it('output events are private by default', async () => {
961
+ const event = await addEvent(run, span.id, {
962
+ kind: 'output',
963
+ stream: 'stdout',
964
+ content: 'output',
965
+ });
966
+
967
+ expect(event.visibility).toBe('private');
968
+ });
969
+
970
+ it('decision events are private by default', async () => {
971
+ const event = await addEvent(run, span.id, {
972
+ kind: 'decision',
973
+ decision: 'use TypeScript',
974
+ });
975
+
976
+ expect(event.visibility).toBe('private');
977
+ });
978
+
979
+ it('observation events are public by default', async () => {
980
+ const event = await addEvent(run, span.id, {
981
+ kind: 'observation',
982
+ observation: 'file exists',
983
+ });
984
+
985
+ expect(event.visibility).toBe('public');
986
+ });
987
+
988
+ it('error events are private by default', async () => {
989
+ const event = await addEvent(run, span.id, {
990
+ kind: 'error',
991
+ error: 'Something went wrong',
992
+ });
993
+
994
+ expect(event.visibility).toBe('private');
995
+ });
996
+
997
+ it('custom events are private by default', async () => {
998
+ const event = await addEvent(run, span.id, {
999
+ kind: 'custom',
1000
+ eventType: 'api-call',
1001
+ data: { endpoint: '/api' },
1002
+ });
1003
+
1004
+ expect(event.visibility).toBe('private');
1005
+ });
1006
+
1007
+ it('spans are private by default', () => {
1008
+ const span = addSpan(run, { name: 'build' });
1009
+
1010
+ expect(span.visibility).toBe('private');
1011
+ });
1012
+ });