Path: blob/main/extensions/copilot/src/platform/otel/node/test/traceContextPropagation.spec.ts
13580 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { afterAll, beforeAll, describe, expect, it } from 'vitest';6import { resolveOTelConfig } from '../../common/otelConfig';7import type { TraceContext } from '../../common/otelService';8import { NodeOTelService } from '../otelServiceImpl';910/**11* Tests for trace context propagation, specifically verifying that12* subagent invoke_agent spans can be linked as children of the parent13* agent's trace via storeTraceContext / getStoredTraceContext / parentTraceContext.14*/15describe('Trace Context Propagation', () => {16let service: NodeOTelService;1718beforeAll(async () => {19const config = resolveOTelConfig({20env: {21'COPILOT_OTEL_ENABLED': 'true',22'COPILOT_OTEL_EXPORTER': 'console',23},24extensionVersion: '1.0.0',25sessionId: 'test-session',26});27service = new NodeOTelService(config);28// Wait for async SDK initialization — poll until _initialized29for (let i = 0; i < 50; i++) {30if ((service as any)._initialized) { break; }31await new Promise(r => setTimeout(r, 50));32}33});3435afterAll(async () => {36await service.shutdown();37});3839describe('storeTraceContext / getStoredTraceContext', () => {40it('round-trips a stored trace context', () => {41const ctx: TraceContext = { traceId: 'aaaa0000bbbb1111cccc2222dddd3333', spanId: 'eeee4444ffff5555' };42service.storeTraceContext('test-key', ctx);43const retrieved = service.getStoredTraceContext('test-key');44expect(retrieved).toEqual(ctx);45});4647it('returns undefined for unknown key', () => {48expect(service.getStoredTraceContext('nonexistent')).toBeUndefined();49});5051it('deletes context after retrieval (single-use)', () => {52const ctx: TraceContext = { traceId: 'aaaa0000bbbb1111cccc2222dddd3333', spanId: 'eeee4444ffff5555' };53service.storeTraceContext('one-shot', ctx);54service.getStoredTraceContext('one-shot');55expect(service.getStoredTraceContext('one-shot')).toBeUndefined();56});57});5859describe('getActiveTraceContext', () => {60it('returns undefined when no span is active', () => {61expect(service.getActiveTraceContext()).toBeUndefined();62});6364it('returns trace context inside startActiveSpan', async () => {65let capturedCtx: TraceContext | undefined;66await service.startActiveSpan('test-parent', { attributes: {} }, async () => {67capturedCtx = service.getActiveTraceContext();68});69expect(capturedCtx).toBeDefined();70expect(capturedCtx!.traceId).toMatch(/^[0-9a-f]{32}$/);71expect(capturedCtx!.spanId).toMatch(/^[0-9a-f]{16}$/);72});73});7475describe('parentTraceContext links subagent to parent trace', () => {76it('child span inherits traceId from parent via parentTraceContext', async () => {77// Phase 1: Parent agent creates a span, captures context78let parentCtx: TraceContext | undefined;79await service.startActiveSpan('invoke_agent parent', { attributes: {} }, async () => {80parentCtx = service.getActiveTraceContext();81});82expect(parentCtx).toBeDefined();8384// Phase 2: Subagent uses parentTraceContext (new async context, no active parent)85let childCtx: TraceContext | undefined;86await service.startActiveSpan('invoke_agent subagent', {87attributes: {},88parentTraceContext: parentCtx,89}, async () => {90childCtx = service.getActiveTraceContext();91});9293// Same traceId (same distributed trace), different spanId94expect(childCtx!.traceId).toBe(parentCtx!.traceId);95expect(childCtx!.spanId).not.toBe(parentCtx!.spanId);96});9798it('without parentTraceContext, spans get independent traceIds', async () => {99let trace1: string | undefined;100let trace2: string | undefined;101102await service.startActiveSpan('agent-1', { attributes: {} }, async () => {103trace1 = service.getActiveTraceContext()!.traceId;104});105106await service.startActiveSpan('agent-2', { attributes: {} }, async () => {107trace2 = service.getActiveTraceContext()!.traceId;108});109110expect(trace1).not.toBe(trace2);111});112113it('full subagent flow: store in tool call, retrieve in subagent', async () => {114let parentTraceId: string | undefined;115let subagentTraceId: string | undefined;116117// Phase 1: Parent agent runs, tool calls runSubagent, stores context118await service.startActiveSpan('invoke_agent main', { attributes: {} }, async () => {119const ctx = service.getActiveTraceContext()!;120parentTraceId = ctx.traceId;121// Simulate execute_tool runSubagent storing the context122service.storeTraceContext('subagent:req-abc', ctx);123});124125// Phase 2: Subagent request arrives (new async context, no parent span active)126const restoredCtx = service.getStoredTraceContext('subagent:req-abc');127expect(restoredCtx).toBeDefined();128129await service.startActiveSpan('invoke_agent subagent', {130attributes: {},131parentTraceContext: restoredCtx,132}, async () => {133subagentTraceId = service.getActiveTraceContext()!.traceId;134});135136// Both agents share the same traceId137expect(subagentTraceId).toBe(parentTraceId);138139// The stored context was consumed (single-use)140expect(service.getStoredTraceContext('subagent:req-abc')).toBeUndefined();141});142});143});144145146