Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/otel/node/test/traceContextPropagation.spec.ts
13580 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
7
import { resolveOTelConfig } from '../../common/otelConfig';
8
import type { TraceContext } from '../../common/otelService';
9
import { NodeOTelService } from '../otelServiceImpl';
10
11
/**
12
* Tests for trace context propagation, specifically verifying that
13
* subagent invoke_agent spans can be linked as children of the parent
14
* agent's trace via storeTraceContext / getStoredTraceContext / parentTraceContext.
15
*/
16
describe('Trace Context Propagation', () => {
17
let service: NodeOTelService;
18
19
beforeAll(async () => {
20
const config = resolveOTelConfig({
21
env: {
22
'COPILOT_OTEL_ENABLED': 'true',
23
'COPILOT_OTEL_EXPORTER': 'console',
24
},
25
extensionVersion: '1.0.0',
26
sessionId: 'test-session',
27
});
28
service = new NodeOTelService(config);
29
// Wait for async SDK initialization — poll until _initialized
30
for (let i = 0; i < 50; i++) {
31
if ((service as any)._initialized) { break; }
32
await new Promise(r => setTimeout(r, 50));
33
}
34
});
35
36
afterAll(async () => {
37
await service.shutdown();
38
});
39
40
describe('storeTraceContext / getStoredTraceContext', () => {
41
it('round-trips a stored trace context', () => {
42
const ctx: TraceContext = { traceId: 'aaaa0000bbbb1111cccc2222dddd3333', spanId: 'eeee4444ffff5555' };
43
service.storeTraceContext('test-key', ctx);
44
const retrieved = service.getStoredTraceContext('test-key');
45
expect(retrieved).toEqual(ctx);
46
});
47
48
it('returns undefined for unknown key', () => {
49
expect(service.getStoredTraceContext('nonexistent')).toBeUndefined();
50
});
51
52
it('deletes context after retrieval (single-use)', () => {
53
const ctx: TraceContext = { traceId: 'aaaa0000bbbb1111cccc2222dddd3333', spanId: 'eeee4444ffff5555' };
54
service.storeTraceContext('one-shot', ctx);
55
service.getStoredTraceContext('one-shot');
56
expect(service.getStoredTraceContext('one-shot')).toBeUndefined();
57
});
58
});
59
60
describe('getActiveTraceContext', () => {
61
it('returns undefined when no span is active', () => {
62
expect(service.getActiveTraceContext()).toBeUndefined();
63
});
64
65
it('returns trace context inside startActiveSpan', async () => {
66
let capturedCtx: TraceContext | undefined;
67
await service.startActiveSpan('test-parent', { attributes: {} }, async () => {
68
capturedCtx = service.getActiveTraceContext();
69
});
70
expect(capturedCtx).toBeDefined();
71
expect(capturedCtx!.traceId).toMatch(/^[0-9a-f]{32}$/);
72
expect(capturedCtx!.spanId).toMatch(/^[0-9a-f]{16}$/);
73
});
74
});
75
76
describe('parentTraceContext links subagent to parent trace', () => {
77
it('child span inherits traceId from parent via parentTraceContext', async () => {
78
// Phase 1: Parent agent creates a span, captures context
79
let parentCtx: TraceContext | undefined;
80
await service.startActiveSpan('invoke_agent parent', { attributes: {} }, async () => {
81
parentCtx = service.getActiveTraceContext();
82
});
83
expect(parentCtx).toBeDefined();
84
85
// Phase 2: Subagent uses parentTraceContext (new async context, no active parent)
86
let childCtx: TraceContext | undefined;
87
await service.startActiveSpan('invoke_agent subagent', {
88
attributes: {},
89
parentTraceContext: parentCtx,
90
}, async () => {
91
childCtx = service.getActiveTraceContext();
92
});
93
94
// Same traceId (same distributed trace), different spanId
95
expect(childCtx!.traceId).toBe(parentCtx!.traceId);
96
expect(childCtx!.spanId).not.toBe(parentCtx!.spanId);
97
});
98
99
it('without parentTraceContext, spans get independent traceIds', async () => {
100
let trace1: string | undefined;
101
let trace2: string | undefined;
102
103
await service.startActiveSpan('agent-1', { attributes: {} }, async () => {
104
trace1 = service.getActiveTraceContext()!.traceId;
105
});
106
107
await service.startActiveSpan('agent-2', { attributes: {} }, async () => {
108
trace2 = service.getActiveTraceContext()!.traceId;
109
});
110
111
expect(trace1).not.toBe(trace2);
112
});
113
114
it('full subagent flow: store in tool call, retrieve in subagent', async () => {
115
let parentTraceId: string | undefined;
116
let subagentTraceId: string | undefined;
117
118
// Phase 1: Parent agent runs, tool calls runSubagent, stores context
119
await service.startActiveSpan('invoke_agent main', { attributes: {} }, async () => {
120
const ctx = service.getActiveTraceContext()!;
121
parentTraceId = ctx.traceId;
122
// Simulate execute_tool runSubagent storing the context
123
service.storeTraceContext('subagent:req-abc', ctx);
124
});
125
126
// Phase 2: Subagent request arrives (new async context, no parent span active)
127
const restoredCtx = service.getStoredTraceContext('subagent:req-abc');
128
expect(restoredCtx).toBeDefined();
129
130
await service.startActiveSpan('invoke_agent subagent', {
131
attributes: {},
132
parentTraceContext: restoredCtx,
133
}, async () => {
134
subagentTraceId = service.getActiveTraceContext()!.traceId;
135
});
136
137
// Both agents share the same traceId
138
expect(subagentTraceId).toBe(parentTraceId);
139
140
// The stored context was consumed (single-use)
141
expect(service.getStoredTraceContext('subagent:req-abc')).toBeUndefined();
142
});
143
});
144
});
145
146