apcore-js 0.4.0 → 0.5.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.
@@ -0,0 +1,310 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ExtensionManager } from '../src/extensions.js';
3
+ import type { ExtensionPoint } from '../src/extensions.js';
4
+ import { Middleware } from '../src/middleware/index.js';
5
+ import { ACL } from '../src/acl.js';
6
+ import { Registry } from '../src/registry/registry.js';
7
+ import { Executor } from '../src/executor.js';
8
+ import { TracingMiddleware, InMemoryExporter } from '../src/observability/tracing.js';
9
+ import type { Discoverer, ModuleValidator } from '../src/registry/registry.js';
10
+ import type { SpanExporter, Span } from '../src/observability/tracing.js';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers: concrete implementations that satisfy the interfaces
14
+ // ---------------------------------------------------------------------------
15
+
16
+ class StubDiscoverer implements Discoverer {
17
+ discover(_roots: string[]) {
18
+ return [];
19
+ }
20
+ }
21
+
22
+ class StubValidator implements ModuleValidator {
23
+ validate(_module: unknown) {
24
+ return [];
25
+ }
26
+ }
27
+
28
+ class StubMiddleware extends Middleware {}
29
+
30
+ class StubExporter implements SpanExporter {
31
+ export(_span: Span): void {
32
+ // no-op
33
+ }
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Tests: ExtensionManager basics
38
+ // ---------------------------------------------------------------------------
39
+
40
+ describe('ExtensionManager init', () => {
41
+ it('has five built-in extension points', () => {
42
+ const mgr = new ExtensionManager();
43
+ const points = mgr.listPoints();
44
+ expect(points).toHaveLength(5);
45
+ });
46
+
47
+ it('built-in point names match expected set', () => {
48
+ const mgr = new ExtensionManager();
49
+ const names = new Set(mgr.listPoints().map((p: ExtensionPoint) => p.name));
50
+ expect(names).toEqual(new Set(['discoverer', 'middleware', 'acl', 'span_exporter', 'module_validator']));
51
+ });
52
+
53
+ it('listPoints returns ExtensionPoint objects', () => {
54
+ const mgr = new ExtensionManager();
55
+ for (const p of mgr.listPoints()) {
56
+ expect(p).toHaveProperty('name');
57
+ expect(p).toHaveProperty('description');
58
+ expect(p).toHaveProperty('multiple');
59
+ }
60
+ });
61
+ });
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Tests: register / get / getAll / unregister
65
+ // ---------------------------------------------------------------------------
66
+
67
+ describe('Discoverer extension', () => {
68
+ it('register and get', () => {
69
+ const mgr = new ExtensionManager();
70
+ const disc = new StubDiscoverer();
71
+ mgr.register('discoverer', disc);
72
+ expect(mgr.get('discoverer')).toBe(disc);
73
+ });
74
+
75
+ it('register replaces single', () => {
76
+ const mgr = new ExtensionManager();
77
+ const disc1 = new StubDiscoverer();
78
+ const disc2 = new StubDiscoverer();
79
+ mgr.register('discoverer', disc1);
80
+ mgr.register('discoverer', disc2);
81
+ expect(mgr.get('discoverer')).toBe(disc2);
82
+ });
83
+
84
+ it('unregister returns true', () => {
85
+ const mgr = new ExtensionManager();
86
+ const disc = new StubDiscoverer();
87
+ mgr.register('discoverer', disc);
88
+ expect(mgr.unregister('discoverer', disc)).toBe(true);
89
+ expect(mgr.get('discoverer')).toBeNull();
90
+ });
91
+
92
+ it('unregister returns false when missing', () => {
93
+ const mgr = new ExtensionManager();
94
+ const disc = new StubDiscoverer();
95
+ expect(mgr.unregister('discoverer', disc)).toBe(false);
96
+ });
97
+
98
+ it('get returns null when empty', () => {
99
+ const mgr = new ExtensionManager();
100
+ expect(mgr.get('discoverer')).toBeNull();
101
+ });
102
+ });
103
+
104
+ describe('Middleware extension', () => {
105
+ it('register and getAll', () => {
106
+ const mgr = new ExtensionManager();
107
+ const mw1 = new StubMiddleware();
108
+ const mw2 = new StubMiddleware();
109
+ mgr.register('middleware', mw1);
110
+ mgr.register('middleware', mw2);
111
+ expect(mgr.getAll('middleware')).toEqual([mw1, mw2]);
112
+ });
113
+
114
+ it('unregister specific', () => {
115
+ const mgr = new ExtensionManager();
116
+ const mw1 = new StubMiddleware();
117
+ const mw2 = new StubMiddleware();
118
+ mgr.register('middleware', mw1);
119
+ mgr.register('middleware', mw2);
120
+ mgr.unregister('middleware', mw1);
121
+ expect(mgr.getAll('middleware')).toEqual([mw2]);
122
+ });
123
+ });
124
+
125
+ describe('ACL extension', () => {
126
+ it('register and get', () => {
127
+ const mgr = new ExtensionManager();
128
+ const acl = new ACL([]);
129
+ mgr.register('acl', acl);
130
+ expect(mgr.get('acl')).toBe(acl);
131
+ });
132
+ });
133
+
134
+ describe('SpanExporter extension', () => {
135
+ it('register multiple', () => {
136
+ const mgr = new ExtensionManager();
137
+ const exp1 = new StubExporter();
138
+ const exp2 = new StubExporter();
139
+ mgr.register('span_exporter', exp1);
140
+ mgr.register('span_exporter', exp2);
141
+ expect(mgr.getAll('span_exporter')).toEqual([exp1, exp2]);
142
+ });
143
+ });
144
+
145
+ describe('ModuleValidator extension', () => {
146
+ it('register and get', () => {
147
+ const mgr = new ExtensionManager();
148
+ const val = new StubValidator();
149
+ mgr.register('module_validator', val);
150
+ expect(mgr.get('module_validator')).toBe(val);
151
+ });
152
+ });
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Tests: validation errors
156
+ // ---------------------------------------------------------------------------
157
+
158
+ describe('Validation', () => {
159
+ it('unknown point throws on register', () => {
160
+ const mgr = new ExtensionManager();
161
+ expect(() => mgr.register('nonexistent', {})).toThrow('Unknown extension point');
162
+ });
163
+
164
+ it('wrong type throws TypeError on register', () => {
165
+ const mgr = new ExtensionManager();
166
+ expect(() => mgr.register('middleware', 'not_a_middleware')).toThrow(TypeError);
167
+ });
168
+
169
+ it('get unknown point throws', () => {
170
+ const mgr = new ExtensionManager();
171
+ expect(() => mgr.get('nonexistent')).toThrow('Unknown extension point');
172
+ });
173
+
174
+ it('getAll unknown point throws', () => {
175
+ const mgr = new ExtensionManager();
176
+ expect(() => mgr.getAll('nonexistent')).toThrow('Unknown extension point');
177
+ });
178
+
179
+ it('unregister unknown point throws', () => {
180
+ const mgr = new ExtensionManager();
181
+ expect(() => mgr.unregister('nonexistent', {})).toThrow('Unknown extension point');
182
+ });
183
+
184
+ it('discoverer rejects wrong type', () => {
185
+ const mgr = new ExtensionManager();
186
+ expect(() => mgr.register('discoverer', new StubMiddleware())).toThrow(TypeError);
187
+ });
188
+
189
+ it('acl rejects wrong type', () => {
190
+ const mgr = new ExtensionManager();
191
+ expect(() => mgr.register('acl', new StubMiddleware())).toThrow(TypeError);
192
+ });
193
+ });
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Tests: apply()
197
+ // ---------------------------------------------------------------------------
198
+
199
+ describe('apply()', () => {
200
+ it('wires discoverer into registry', () => {
201
+ const mgr = new ExtensionManager();
202
+ const disc = new StubDiscoverer();
203
+ mgr.register('discoverer', disc);
204
+
205
+ const registry = { setDiscoverer: vi.fn(), setValidator: vi.fn() } as unknown as Registry;
206
+ const executor = { use: vi.fn(), setAcl: vi.fn(), middlewares: [] } as unknown as Executor;
207
+ mgr.apply(registry, executor);
208
+
209
+ expect(registry.setDiscoverer).toHaveBeenCalledWith(disc);
210
+ });
211
+
212
+ it('wires validator into registry', () => {
213
+ const mgr = new ExtensionManager();
214
+ const val = new StubValidator();
215
+ mgr.register('module_validator', val);
216
+
217
+ const registry = { setDiscoverer: vi.fn(), setValidator: vi.fn() } as unknown as Registry;
218
+ const executor = { use: vi.fn(), setAcl: vi.fn(), middlewares: [] } as unknown as Executor;
219
+ mgr.apply(registry, executor);
220
+
221
+ expect(registry.setValidator).toHaveBeenCalledWith(val);
222
+ });
223
+
224
+ it('wires ACL into executor', () => {
225
+ const mgr = new ExtensionManager();
226
+ const acl = new ACL([]);
227
+ mgr.register('acl', acl);
228
+
229
+ const registry = { setDiscoverer: vi.fn(), setValidator: vi.fn() } as unknown as Registry;
230
+ const executor = { use: vi.fn(), setAcl: vi.fn(), middlewares: [] } as unknown as Executor;
231
+ mgr.apply(registry, executor);
232
+
233
+ expect(executor.setAcl).toHaveBeenCalledWith(acl);
234
+ });
235
+
236
+ it('wires middleware into executor', () => {
237
+ const mgr = new ExtensionManager();
238
+ const mw = new StubMiddleware();
239
+ mgr.register('middleware', mw);
240
+
241
+ const registry = { setDiscoverer: vi.fn(), setValidator: vi.fn() } as unknown as Registry;
242
+ const executor = { use: vi.fn(), setAcl: vi.fn(), middlewares: [] } as unknown as Executor;
243
+ mgr.apply(registry, executor);
244
+
245
+ expect(executor.use).toHaveBeenCalledWith(mw);
246
+ });
247
+
248
+ it('wires single span exporter into TracingMiddleware', () => {
249
+ const mgr = new ExtensionManager();
250
+ const newExp = new StubExporter();
251
+ mgr.register('span_exporter', newExp);
252
+
253
+ const inMem = new InMemoryExporter();
254
+ const tracingMw = new TracingMiddleware(inMem);
255
+
256
+ const registry = { setDiscoverer: vi.fn(), setValidator: vi.fn() } as unknown as Registry;
257
+ const executor = { use: vi.fn(), setAcl: vi.fn(), middlewares: [tracingMw] } as unknown as Executor;
258
+ mgr.apply(registry, executor);
259
+
260
+ // setExporter is used instead of direct private access
261
+ expect((tracingMw as unknown as Record<string, unknown>)['_exporter']).toBe(newExp);
262
+ });
263
+
264
+ it('wires multiple span exporters via composite exporter', () => {
265
+ const mgr = new ExtensionManager();
266
+ const exp1 = new StubExporter();
267
+ const exp2 = new StubExporter();
268
+ mgr.register('span_exporter', exp1);
269
+ mgr.register('span_exporter', exp2);
270
+
271
+ const inMem = new InMemoryExporter();
272
+ const tracingMw = new TracingMiddleware(inMem);
273
+
274
+ const registry = { setDiscoverer: vi.fn(), setValidator: vi.fn() } as unknown as Registry;
275
+ const executor = { use: vi.fn(), setAcl: vi.fn(), middlewares: [tracingMw] } as unknown as Executor;
276
+ mgr.apply(registry, executor);
277
+
278
+ // A composite exporter should have been set that delegates to both
279
+ const composite = (tracingMw as unknown as Record<string, unknown>)['_exporter'] as Record<string, unknown>;
280
+ expect(composite['_exporters']).toEqual([exp1, exp2]);
281
+ });
282
+
283
+ it('apply with no extensions is safe', () => {
284
+ const mgr = new ExtensionManager();
285
+ const registry = { setDiscoverer: vi.fn(), setValidator: vi.fn() } as unknown as Registry;
286
+ const executor = { use: vi.fn(), setAcl: vi.fn(), middlewares: [] } as unknown as Executor;
287
+ mgr.apply(registry, executor);
288
+
289
+ expect(registry.setDiscoverer).not.toHaveBeenCalled();
290
+ expect(registry.setValidator).not.toHaveBeenCalled();
291
+ expect(executor.use).not.toHaveBeenCalled();
292
+ });
293
+
294
+ it('warns when span_exporter registered but no TracingMiddleware present', () => {
295
+ const mgr = new ExtensionManager();
296
+ const newExp = new StubExporter();
297
+ mgr.register('span_exporter', newExp);
298
+
299
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
300
+
301
+ const registry = { setDiscoverer: vi.fn(), setValidator: vi.fn() } as unknown as Registry;
302
+ const executor = { use: vi.fn(), setAcl: vi.fn(), middlewares: [] } as unknown as Executor;
303
+ mgr.apply(registry, executor);
304
+
305
+ expect(warnSpy).toHaveBeenCalledWith(
306
+ expect.stringContaining('span_exporter'),
307
+ );
308
+ warnSpy.mockRestore();
309
+ });
310
+ });
@@ -0,0 +1,251 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { TraceContext } from '../src/trace-context.js';
3
+ import type { TraceParent } from '../src/trace-context.js';
4
+ import { Context } from '../src/context.js';
5
+ import type { Span } from '../src/observability/tracing.js';
6
+
7
+ const TRACEPARENT_RE = /^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/;
8
+
9
+ describe('TraceContext.inject()', () => {
10
+ it('produces a valid traceparent format', () => {
11
+ const ctx = Context.create();
12
+ const headers = TraceContext.inject(ctx);
13
+ expect(headers).toHaveProperty('traceparent');
14
+ expect(headers.traceparent).toMatch(TRACEPARENT_RE);
15
+ });
16
+
17
+ it('uses context traceId (dashes stripped)', () => {
18
+ const ctx = Context.create();
19
+ const headers = TraceContext.inject(ctx);
20
+ const parts = headers.traceparent.split('-');
21
+ const expectedHex = ctx.traceId.replace(/-/g, '');
22
+ // parts[1] is the trace_id field (but split by '-' means we need to
23
+ // reconstruct from parts 1..4 since the 32-hex is split by the overall format)
24
+ // Actually the format is: 00-<32hex>-<16hex>-<2hex>
25
+ // Splitting by '-' gives: ["00", <32hex>, <16hex>, <2hex>]
26
+ expect(parts[1]).toBe(expectedHex);
27
+ });
28
+
29
+ it('starts with version 00', () => {
30
+ const ctx = Context.create();
31
+ const headers = TraceContext.inject(ctx);
32
+ expect(headers.traceparent.startsWith('00-')).toBe(true);
33
+ });
34
+
35
+ it('ends with trace flags 01', () => {
36
+ const ctx = Context.create();
37
+ const headers = TraceContext.inject(ctx);
38
+ expect(headers.traceparent.endsWith('-01')).toBe(true);
39
+ });
40
+
41
+ it('uses spanId from tracing stack when available', () => {
42
+ const ctx = Context.create();
43
+ const fakeSpan: Span = {
44
+ traceId: ctx.traceId,
45
+ name: 'test',
46
+ startTime: 0,
47
+ spanId: 'abcdef0123456789',
48
+ parentSpanId: null,
49
+ attributes: {},
50
+ endTime: null,
51
+ status: 'ok',
52
+ events: [],
53
+ };
54
+ ctx.data['_tracing_spans'] = [fakeSpan];
55
+
56
+ const headers = TraceContext.inject(ctx);
57
+ const parts = headers.traceparent.split('-');
58
+ expect(parts[2]).toBe('abcdef0123456789');
59
+ });
60
+
61
+ it('generates random parentId when no spans exist', () => {
62
+ const ctx = Context.create();
63
+ const headers = TraceContext.inject(ctx);
64
+ const parts = headers.traceparent.split('-');
65
+ const parentId = parts[2];
66
+ expect(parentId).toHaveLength(16);
67
+ expect(parentId).toMatch(/^[0-9a-f]{16}$/);
68
+ });
69
+ });
70
+
71
+ describe('TraceContext.extract()', () => {
72
+ it('parses a valid traceparent header', () => {
73
+ const headers = { traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' };
74
+ const result = TraceContext.extract(headers);
75
+ expect(result).not.toBeNull();
76
+ expect(result!.version).toBe('00');
77
+ expect(result!.traceId).toBe('4bf92f3577b34da6a3ce929d0e0e4736');
78
+ expect(result!.parentId).toBe('00f067aa0ba902b7');
79
+ expect(result!.traceFlags).toBe('01');
80
+ });
81
+
82
+ it('returns null for missing header', () => {
83
+ const result = TraceContext.extract({});
84
+ expect(result).toBeNull();
85
+ });
86
+
87
+ it('returns null for unrelated headers', () => {
88
+ const result = TraceContext.extract({ 'other-header': 'value' });
89
+ expect(result).toBeNull();
90
+ });
91
+
92
+ it('returns null for malformed traceparent', () => {
93
+ const result = TraceContext.extract({ traceparent: 'invalid-format' });
94
+ expect(result).toBeNull();
95
+ });
96
+
97
+ it('returns null for short trace_id', () => {
98
+ const result = TraceContext.extract({ traceparent: '00-abc-00f067aa0ba902b7-01' });
99
+ expect(result).toBeNull();
100
+ });
101
+
102
+ it('normalizes uppercase to lowercase', () => {
103
+ const headers = { traceparent: '00-4BF92F3577B34DA6A3CE929D0E0E4736-00F067AA0BA902B7-01' };
104
+ const result = TraceContext.extract(headers);
105
+ expect(result).not.toBeNull();
106
+ expect(result!.traceId).toBe('4bf92f3577b34da6a3ce929d0e0e4736');
107
+ });
108
+
109
+ it('parses unsampled trace flags', () => {
110
+ const headers = { traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00' };
111
+ const result = TraceContext.extract(headers);
112
+ expect(result).not.toBeNull();
113
+ expect(result!.traceFlags).toBe('00');
114
+ });
115
+
116
+ it('returns null for all-zero trace_id', () => {
117
+ const result = TraceContext.extract({ traceparent: '00-00000000000000000000000000000000-00f067aa0ba902b7-01' });
118
+ expect(result).toBeNull();
119
+ });
120
+
121
+ it('returns null for all-zero parent_id', () => {
122
+ const result = TraceContext.extract({ traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01' });
123
+ expect(result).toBeNull();
124
+ });
125
+
126
+ it('returns null for version ff', () => {
127
+ const result = TraceContext.extract({ traceparent: 'ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' });
128
+ expect(result).toBeNull();
129
+ });
130
+
131
+ it('returns a frozen object', () => {
132
+ const headers = { traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' };
133
+ const result = TraceContext.extract(headers);
134
+ expect(result).not.toBeNull();
135
+ expect(Object.isFrozen(result)).toBe(true);
136
+ });
137
+ });
138
+
139
+ describe('TraceContext.fromTraceparent()', () => {
140
+ it('parses a valid traceparent string', () => {
141
+ const tp = TraceContext.fromTraceparent('00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01');
142
+ expect(tp.version).toBe('00');
143
+ expect(tp.traceId).toBe('4bf92f3577b34da6a3ce929d0e0e4736');
144
+ expect(tp.parentId).toBe('00f067aa0ba902b7');
145
+ expect(tp.traceFlags).toBe('01');
146
+ });
147
+
148
+ it('throws on invalid traceparent', () => {
149
+ expect(() => TraceContext.fromTraceparent('not-a-valid-traceparent'))
150
+ .toThrow('Malformed traceparent');
151
+ });
152
+
153
+ it('throws on empty string', () => {
154
+ expect(() => TraceContext.fromTraceparent(''))
155
+ .toThrow('Malformed traceparent');
156
+ });
157
+
158
+ it('throws on missing parts', () => {
159
+ expect(() => TraceContext.fromTraceparent('00-4bf92f3577b34da6a3ce929d0e0e4736'))
160
+ .toThrow('Malformed traceparent');
161
+ });
162
+
163
+ it('throws on all-zero trace_id', () => {
164
+ expect(() => TraceContext.fromTraceparent('00-00000000000000000000000000000000-00f067aa0ba902b7-01'))
165
+ .toThrow('all-zero trace_id or parent_id');
166
+ });
167
+
168
+ it('throws on all-zero parent_id', () => {
169
+ expect(() => TraceContext.fromTraceparent('00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01'))
170
+ .toThrow('all-zero trace_id or parent_id');
171
+ });
172
+
173
+ it('throws on version ff', () => {
174
+ expect(() => TraceContext.fromTraceparent('ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'))
175
+ .toThrow('version ff is not allowed');
176
+ });
177
+
178
+ it('returns a frozen object', () => {
179
+ const tp = TraceContext.fromTraceparent('00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01');
180
+ expect(Object.isFrozen(tp)).toBe(true);
181
+ });
182
+ });
183
+
184
+ describe('Round-trip: inject -> extract', () => {
185
+ it('preserves trace_id through inject then extract', () => {
186
+ const ctx = Context.create();
187
+ const headers = TraceContext.inject(ctx);
188
+ const parsed = TraceContext.extract(headers);
189
+
190
+ expect(parsed).not.toBeNull();
191
+ const expectedHex = ctx.traceId.replace(/-/g, '');
192
+ expect(parsed!.traceId).toBe(expectedHex);
193
+ });
194
+
195
+ it('preserves parent_id through inject then extract', () => {
196
+ const ctx = Context.create();
197
+ const headers = TraceContext.inject(ctx);
198
+ const parsed = TraceContext.extract(headers);
199
+
200
+ expect(parsed).not.toBeNull();
201
+ const parts = headers.traceparent.split('-');
202
+ expect(parsed!.parentId).toBe(parts[2]);
203
+ });
204
+ });
205
+
206
+ describe('Context.create() with traceParent', () => {
207
+ it('uses traceParent traceId converted to UUID format', () => {
208
+ const tp: TraceParent = {
209
+ version: '00',
210
+ traceId: '4bf92f3577b34da6a3ce929d0e0e4736',
211
+ parentId: '00f067aa0ba902b7',
212
+ traceFlags: '01',
213
+ };
214
+ const ctx = Context.create(null, null, undefined, tp);
215
+ expect(ctx.traceId).toBe('4bf92f35-77b3-4da6-a3ce-929d0e0e4736');
216
+ });
217
+
218
+ it('produces a valid UUID string from traceParent', () => {
219
+ const tp: TraceParent = {
220
+ version: '00',
221
+ traceId: '4bf92f3577b34da6a3ce929d0e0e4736',
222
+ parentId: '00f067aa0ba902b7',
223
+ traceFlags: '01',
224
+ };
225
+ const ctx = Context.create(null, null, undefined, tp);
226
+ // Should match UUID format: 8-4-4-4-12
227
+ expect(ctx.traceId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
228
+ });
229
+
230
+ it('still works without traceParent', () => {
231
+ const ctx = Context.create();
232
+ expect(ctx.traceId).toBeDefined();
233
+ expect(ctx.traceId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
234
+ });
235
+
236
+ it('generates unique traceIds without traceParent', () => {
237
+ const ctx1 = Context.create();
238
+ const ctx2 = Context.create();
239
+ expect(ctx1.traceId).not.toBe(ctx2.traceId);
240
+ });
241
+
242
+ it('full round-trip: context -> inject -> extract -> create', () => {
243
+ const original = Context.create();
244
+ const headers = TraceContext.inject(original);
245
+ const parsed = TraceContext.extract(headers);
246
+ expect(parsed).not.toBeNull();
247
+
248
+ const restored = Context.create(null, null, undefined, parsed);
249
+ expect(restored.traceId).toBe(original.traceId);
250
+ });
251
+ });