Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/otel/common/test/chatMLFetcherSpanLifecycle.spec.ts
13406 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 { describe, expect, it } from 'vitest';
7
import { CopilotChatAttr, GenAiAttr, GenAiOperationName, GenAiProviderName } from '../genAiAttributes';
8
import { SpanKind, SpanStatusCode } from '../otelService';
9
import { CapturingOTelService } from './capturingOTelService';
10
11
/**
12
* Tests the two-phase span lifecycle used in chatMLFetcher:
13
* 1. _doFetch creates a span, returns it alongside the result
14
* 2. fetchMany enriches it with token usage and response data, then ends it
15
*
16
* This pattern is unique because the span is created in one method
17
* and ended in another — testing lifecycle correctness.
18
*/
19
describe('chatMLFetcher Span Lifecycle', () => {
20
it('span is created with model and conversation ID in _doFetch phase', () => {
21
const otel = new CapturingOTelService();
22
23
// Phase 1: _doFetch creates the span
24
const span = otel.startSpan('chat gpt-4o', {
25
kind: SpanKind.CLIENT,
26
attributes: {
27
[GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT,
28
[GenAiAttr.PROVIDER_NAME]: GenAiProviderName.GITHUB,
29
[GenAiAttr.REQUEST_MODEL]: 'gpt-4o',
30
[GenAiAttr.CONVERSATION_ID]: 'req-abc',
31
[GenAiAttr.REQUEST_MAX_TOKENS]: 2048,
32
[CopilotChatAttr.MAX_PROMPT_TOKENS]: 128000,
33
},
34
});
35
36
const s = otel.spans[0];
37
expect(s.name).toBe('chat gpt-4o');
38
expect(s.kind).toBe(SpanKind.CLIENT);
39
expect(s.attributes[GenAiAttr.REQUEST_MODEL]).toBe('gpt-4o');
40
expect(s.attributes[GenAiAttr.CONVERSATION_ID]).toBe('req-abc');
41
expect(s.ended).toBe(false);
42
43
// Phase 2: fetchMany enriches with response data
44
span.setAttributes({
45
[GenAiAttr.USAGE_INPUT_TOKENS]: 1500,
46
[GenAiAttr.USAGE_OUTPUT_TOKENS]: 250,
47
[GenAiAttr.RESPONSE_MODEL]: 'gpt-4o-2024-08-06',
48
[GenAiAttr.RESPONSE_ID]: 'chatcmpl-xyz',
49
[GenAiAttr.RESPONSE_FINISH_REASONS]: ['stop'],
50
[CopilotChatAttr.TIME_TO_FIRST_TOKEN]: 450,
51
});
52
span.setStatus(SpanStatusCode.OK);
53
span.end();
54
55
expect(s.attributes[GenAiAttr.USAGE_INPUT_TOKENS]).toBe(1500);
56
expect(s.attributes[GenAiAttr.RESPONSE_MODEL]).toBe('gpt-4o-2024-08-06');
57
expect(s.statusCode).toBe(SpanStatusCode.OK);
58
expect(s.ended).toBe(true);
59
});
60
61
it('span is ended on error path (not leaked)', () => {
62
const otel = new CapturingOTelService();
63
64
// Phase 1: span created
65
const span = otel.startSpan('chat gpt-4o', {
66
kind: SpanKind.CLIENT,
67
attributes: { [GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT },
68
});
69
70
// Phase 2: error occurs — fetchMany Error path
71
span.setStatus(SpanStatusCode.ERROR, 'Connection reset');
72
span.setAttribute('error.type', 'FetchError');
73
span.recordException(new Error('Connection reset'));
74
span.end();
75
76
const s = otel.spans[0];
77
expect(s.statusCode).toBe(SpanStatusCode.ERROR);
78
expect(s.ended).toBe(true);
79
expect(s.exceptions).toHaveLength(1);
80
});
81
82
it('operation duration metric is recorded in _doFetch finally block', () => {
83
const otel = new CapturingOTelService();
84
85
// Simulate the finally block in _doFetch
86
const durationSec = 3.5;
87
otel.recordMetric('gen_ai.client.operation.duration', durationSec, {
88
[GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT,
89
[GenAiAttr.PROVIDER_NAME]: GenAiProviderName.GITHUB,
90
[GenAiAttr.REQUEST_MODEL]: 'gpt-4o',
91
});
92
93
expect(otel.metrics).toHaveLength(1);
94
expect(otel.metrics[0].name).toBe('gen_ai.client.operation.duration');
95
expect(otel.metrics[0].value).toBe(3.5);
96
});
97
98
it('debug name attribute is set after span is returned to fetchMany', () => {
99
const otel = new CapturingOTelService();
100
101
const span = otel.startSpan('chat gpt-4o', {
102
kind: SpanKind.CLIENT,
103
attributes: { [GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT },
104
});
105
106
// fetchMany adds debug name after receiving the span from _doFetch
107
span.setAttribute(GenAiAttr.AGENT_NAME, 'agentMode');
108
span.end();
109
110
expect(otel.spans[0].attributes[GenAiAttr.AGENT_NAME]).toBe('agentMode');
111
});
112
});
113
114