Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chronicle/common/test/eventTranslator.spec.ts
13405 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 type { ICompletedSpanData } from '../../../../platform/otel/common/otelService';
8
import { createSessionTranslationState, makeIdleEvent, makeShutdownEvent, translateSpan } from '../eventTranslator';
9
10
function makeSpan(overrides: Partial<ICompletedSpanData> = {}): ICompletedSpanData {
11
return {
12
name: 'test-span',
13
spanId: 'span-1',
14
traceId: 'trace-1',
15
startTime: Date.now(),
16
endTime: Date.now() + 100,
17
status: { code: 0 },
18
attributes: {},
19
events: [],
20
...overrides,
21
};
22
}
23
24
describe('translateSpan', () => {
25
it('emits session.start + user.message for first invoke_agent span', () => {
26
const state = createSessionTranslationState();
27
const span = makeSpan({
28
attributes: {
29
'gen_ai.operation.name': 'invoke_agent',
30
'copilot_chat.chat_session_id': 'sess-1',
31
'copilot_chat.user_request': 'How do I fix this bug?',
32
},
33
});
34
35
const events = translateSpan(span, state);
36
37
expect(events.length).toBeGreaterThanOrEqual(2);
38
expect(events[0].type).toBe('session.start');
39
expect(events[0].parentId).toBeNull();
40
expect(events[0].data.producer).toBe('vscode-copilot-chat');
41
expect(events[1].type).toBe('user.message');
42
expect(events[1].data.content).toBe('How do I fix this bug?');
43
expect(events[1].parentId).toBe(events[0].id);
44
});
45
46
it('does not emit session.start on subsequent invoke_agent spans', () => {
47
const state = createSessionTranslationState();
48
const span1 = makeSpan({
49
attributes: {
50
'gen_ai.operation.name': 'invoke_agent',
51
'copilot_chat.user_request': 'First message',
52
},
53
});
54
const span2 = makeSpan({
55
attributes: {
56
'gen_ai.operation.name': 'invoke_agent',
57
'copilot_chat.user_request': 'Second message',
58
},
59
});
60
61
translateSpan(span1, state);
62
const events = translateSpan(span2, state);
63
64
const starts = events.filter(e => e.type === 'session.start');
65
expect(starts).toHaveLength(0);
66
expect(events.some(e => e.type === 'user.message' && e.data.content === 'Second message')).toBe(true);
67
});
68
69
it('emits assistant.message when output_messages is present', () => {
70
const state = createSessionTranslationState();
71
const outputMessages = JSON.stringify([
72
{ role: 'assistant', parts: [{ type: 'text', content: 'Here is the fix.' }] },
73
]);
74
const span = makeSpan({
75
attributes: {
76
'gen_ai.operation.name': 'invoke_agent',
77
'copilot_chat.user_request': 'Fix it',
78
'gen_ai.output.messages': outputMessages,
79
},
80
});
81
82
const events = translateSpan(span, state);
83
const assistantEvents = events.filter(e => e.type === 'assistant.message');
84
expect(assistantEvents).toHaveLength(1);
85
expect(assistantEvents[0].data.content).toBe('Here is the fix.');
86
});
87
88
it('emits tool.result for execute_tool spans', () => {
89
const state = createSessionTranslationState();
90
state.started = true; // simulate after session.start
91
92
const span = makeSpan({
93
attributes: {
94
'gen_ai.operation.name': 'execute_tool',
95
'gen_ai.tool.name': 'read_file',
96
'gen_ai.tool.call.id': 'call-1',
97
'gen_ai.tool.result': 'File contents here...',
98
},
99
status: { code: 0 },
100
});
101
102
const events = translateSpan(span, state);
103
104
expect(events).toHaveLength(1);
105
expect(events[0].type).toBe('tool.execution_complete');
106
expect(events[0].data.success).toBe(true);
107
expect(events[0].data.result).toBeDefined();
108
});
109
110
it('marks tool.result as failed when span status is ERROR', () => {
111
const state = createSessionTranslationState();
112
state.started = true;
113
114
const span = makeSpan({
115
attributes: {
116
'gen_ai.operation.name': 'execute_tool',
117
'gen_ai.tool.name': 'apply_patch',
118
},
119
status: { code: 2 }, // ERROR
120
});
121
122
const events = translateSpan(span, state);
123
expect(events[0].data.success).toBe(false);
124
expect(events[0].data.error).toBeDefined();
125
});
126
127
it('ignores non-relevant operation names', () => {
128
const state = createSessionTranslationState();
129
const span = makeSpan({
130
attributes: {
131
'gen_ai.operation.name': 'chat',
132
},
133
});
134
135
const events = translateSpan(span, state);
136
expect(events).toHaveLength(0);
137
});
138
139
it('truncates oversized user message content', () => {
140
const state = createSessionTranslationState();
141
const longMessage = 'x'.repeat(20_000);
142
const span = makeSpan({
143
attributes: {
144
'gen_ai.operation.name': 'invoke_agent',
145
'copilot_chat.user_request': longMessage,
146
},
147
});
148
149
const events = translateSpan(span, state);
150
const userEvent = events.find(e => e.type === 'user.message');
151
expect(userEvent).toBeDefined();
152
expect((userEvent!.data.content as string).length).toBeLessThan(longMessage.length);
153
expect((userEvent!.data.content as string)).toContain('[truncated]');
154
});
155
156
it('truncates oversized tool result content', () => {
157
const state = createSessionTranslationState();
158
state.started = true;
159
const longResult = 'x'.repeat(10_000);
160
161
const span = makeSpan({
162
attributes: {
163
'gen_ai.operation.name': 'execute_tool',
164
'gen_ai.tool.name': 'read_file',
165
'gen_ai.tool.result': longResult,
166
},
167
});
168
169
const events = translateSpan(span, state);
170
const result = events[0].data.result as { content: string };
171
expect(result.content.length).toBeLessThan(longResult.length);
172
expect(result.content).toContain('[truncated]');
173
});
174
175
it('chains parentId across events', () => {
176
const state = createSessionTranslationState();
177
const span1 = makeSpan({
178
attributes: {
179
'gen_ai.operation.name': 'invoke_agent',
180
'copilot_chat.user_request': 'First',
181
},
182
});
183
const span2 = makeSpan({
184
attributes: {
185
'gen_ai.operation.name': 'invoke_agent',
186
'copilot_chat.user_request': 'Second',
187
},
188
});
189
190
const events1 = translateSpan(span1, state);
191
const events2 = translateSpan(span2, state);
192
193
// Second batch should chain from last event of first batch
194
const lastEvent1 = events1[events1.length - 1];
195
expect(events2[0].parentId).toBe(lastEvent1.id);
196
});
197
198
it('includes context in session.start when provided', () => {
199
const state = createSessionTranslationState();
200
const context = { repository: 'microsoft/vscode', branch: 'main', headCommit: 'abc123' };
201
const span = makeSpan({
202
attributes: {
203
'gen_ai.operation.name': 'invoke_agent',
204
'copilot_chat.user_request': 'Hello',
205
},
206
});
207
208
const events = translateSpan(span, state, context);
209
const ctx = events[0].data.context as Record<string, unknown>;
210
expect(ctx.repository).toBe('microsoft/vscode');
211
expect(ctx.branch).toBe('main');
212
expect(ctx.headCommit).toBe('abc123');
213
expect(ctx.hostType).toBe('github');
214
});
215
});
216
217
describe('makeIdleEvent', () => {
218
it('creates an ephemeral session.idle event', () => {
219
const state = createSessionTranslationState();
220
const event = makeIdleEvent(state);
221
expect(event.type).toBe('session.idle');
222
expect(event.ephemeral).toBe(true);
223
});
224
});
225
226
describe('makeShutdownEvent', () => {
227
it('creates a session.shutdown event', () => {
228
const state = createSessionTranslationState();
229
const event = makeShutdownEvent(state);
230
expect(event.type).toBe('session.shutdown');
231
expect(event.ephemeral).toBeUndefined();
232
});
233
234
it('chains parentId from prior events', () => {
235
const state = createSessionTranslationState();
236
state.lastEventId = 'prev-event-id';
237
const event = makeShutdownEvent(state);
238
expect(event.parentId).toBe('prev-event-id');
239
});
240
});
241
242