Path: blob/main/extensions/copilot/src/extension/chronicle/common/test/eventTranslator.spec.ts
13405 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 { describe, expect, it } from 'vitest';6import type { ICompletedSpanData } from '../../../../platform/otel/common/otelService';7import { createSessionTranslationState, makeIdleEvent, makeShutdownEvent, translateSpan } from '../eventTranslator';89function makeSpan(overrides: Partial<ICompletedSpanData> = {}): ICompletedSpanData {10return {11name: 'test-span',12spanId: 'span-1',13traceId: 'trace-1',14startTime: Date.now(),15endTime: Date.now() + 100,16status: { code: 0 },17attributes: {},18events: [],19...overrides,20};21}2223describe('translateSpan', () => {24it('emits session.start + user.message for first invoke_agent span', () => {25const state = createSessionTranslationState();26const span = makeSpan({27attributes: {28'gen_ai.operation.name': 'invoke_agent',29'copilot_chat.chat_session_id': 'sess-1',30'copilot_chat.user_request': 'How do I fix this bug?',31},32});3334const events = translateSpan(span, state);3536expect(events.length).toBeGreaterThanOrEqual(2);37expect(events[0].type).toBe('session.start');38expect(events[0].parentId).toBeNull();39expect(events[0].data.producer).toBe('vscode-copilot-chat');40expect(events[1].type).toBe('user.message');41expect(events[1].data.content).toBe('How do I fix this bug?');42expect(events[1].parentId).toBe(events[0].id);43});4445it('does not emit session.start on subsequent invoke_agent spans', () => {46const state = createSessionTranslationState();47const span1 = makeSpan({48attributes: {49'gen_ai.operation.name': 'invoke_agent',50'copilot_chat.user_request': 'First message',51},52});53const span2 = makeSpan({54attributes: {55'gen_ai.operation.name': 'invoke_agent',56'copilot_chat.user_request': 'Second message',57},58});5960translateSpan(span1, state);61const events = translateSpan(span2, state);6263const starts = events.filter(e => e.type === 'session.start');64expect(starts).toHaveLength(0);65expect(events.some(e => e.type === 'user.message' && e.data.content === 'Second message')).toBe(true);66});6768it('emits assistant.message when output_messages is present', () => {69const state = createSessionTranslationState();70const outputMessages = JSON.stringify([71{ role: 'assistant', parts: [{ type: 'text', content: 'Here is the fix.' }] },72]);73const span = makeSpan({74attributes: {75'gen_ai.operation.name': 'invoke_agent',76'copilot_chat.user_request': 'Fix it',77'gen_ai.output.messages': outputMessages,78},79});8081const events = translateSpan(span, state);82const assistantEvents = events.filter(e => e.type === 'assistant.message');83expect(assistantEvents).toHaveLength(1);84expect(assistantEvents[0].data.content).toBe('Here is the fix.');85});8687it('emits tool.result for execute_tool spans', () => {88const state = createSessionTranslationState();89state.started = true; // simulate after session.start9091const span = makeSpan({92attributes: {93'gen_ai.operation.name': 'execute_tool',94'gen_ai.tool.name': 'read_file',95'gen_ai.tool.call.id': 'call-1',96'gen_ai.tool.result': 'File contents here...',97},98status: { code: 0 },99});100101const events = translateSpan(span, state);102103expect(events).toHaveLength(1);104expect(events[0].type).toBe('tool.execution_complete');105expect(events[0].data.success).toBe(true);106expect(events[0].data.result).toBeDefined();107});108109it('marks tool.result as failed when span status is ERROR', () => {110const state = createSessionTranslationState();111state.started = true;112113const span = makeSpan({114attributes: {115'gen_ai.operation.name': 'execute_tool',116'gen_ai.tool.name': 'apply_patch',117},118status: { code: 2 }, // ERROR119});120121const events = translateSpan(span, state);122expect(events[0].data.success).toBe(false);123expect(events[0].data.error).toBeDefined();124});125126it('ignores non-relevant operation names', () => {127const state = createSessionTranslationState();128const span = makeSpan({129attributes: {130'gen_ai.operation.name': 'chat',131},132});133134const events = translateSpan(span, state);135expect(events).toHaveLength(0);136});137138it('truncates oversized user message content', () => {139const state = createSessionTranslationState();140const longMessage = 'x'.repeat(20_000);141const span = makeSpan({142attributes: {143'gen_ai.operation.name': 'invoke_agent',144'copilot_chat.user_request': longMessage,145},146});147148const events = translateSpan(span, state);149const userEvent = events.find(e => e.type === 'user.message');150expect(userEvent).toBeDefined();151expect((userEvent!.data.content as string).length).toBeLessThan(longMessage.length);152expect((userEvent!.data.content as string)).toContain('[truncated]');153});154155it('truncates oversized tool result content', () => {156const state = createSessionTranslationState();157state.started = true;158const longResult = 'x'.repeat(10_000);159160const span = makeSpan({161attributes: {162'gen_ai.operation.name': 'execute_tool',163'gen_ai.tool.name': 'read_file',164'gen_ai.tool.result': longResult,165},166});167168const events = translateSpan(span, state);169const result = events[0].data.result as { content: string };170expect(result.content.length).toBeLessThan(longResult.length);171expect(result.content).toContain('[truncated]');172});173174it('chains parentId across events', () => {175const state = createSessionTranslationState();176const span1 = makeSpan({177attributes: {178'gen_ai.operation.name': 'invoke_agent',179'copilot_chat.user_request': 'First',180},181});182const span2 = makeSpan({183attributes: {184'gen_ai.operation.name': 'invoke_agent',185'copilot_chat.user_request': 'Second',186},187});188189const events1 = translateSpan(span1, state);190const events2 = translateSpan(span2, state);191192// Second batch should chain from last event of first batch193const lastEvent1 = events1[events1.length - 1];194expect(events2[0].parentId).toBe(lastEvent1.id);195});196197it('includes context in session.start when provided', () => {198const state = createSessionTranslationState();199const context = { repository: 'microsoft/vscode', branch: 'main', headCommit: 'abc123' };200const span = makeSpan({201attributes: {202'gen_ai.operation.name': 'invoke_agent',203'copilot_chat.user_request': 'Hello',204},205});206207const events = translateSpan(span, state, context);208const ctx = events[0].data.context as Record<string, unknown>;209expect(ctx.repository).toBe('microsoft/vscode');210expect(ctx.branch).toBe('main');211expect(ctx.headCommit).toBe('abc123');212expect(ctx.hostType).toBe('github');213});214});215216describe('makeIdleEvent', () => {217it('creates an ephemeral session.idle event', () => {218const state = createSessionTranslationState();219const event = makeIdleEvent(state);220expect(event.type).toBe('session.idle');221expect(event.ephemeral).toBe(true);222});223});224225describe('makeShutdownEvent', () => {226it('creates a session.shutdown event', () => {227const state = createSessionTranslationState();228const event = makeShutdownEvent(state);229expect(event.type).toBe('session.shutdown');230expect(event.ephemeral).toBeUndefined();231});232233it('chains parentId from prior events', () => {234const state = createSessionTranslationState();235state.lastEventId = 'prev-event-id';236const event = makeShutdownEvent(state);237expect(event.parentId).toBe('prev-event-id');238});239});240241242