Path: blob/main/extensions/copilot/src/extension/chronicle/vscode-node/test/sessionStoreTracker.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 { GenAiAttr } from '../../../../platform/otel/common/genAiAttributes';7import type { ICompletedSpanData } from '../../../../platform/otel/common/otelService';8import { extractFilePath, extractToolArgs } from '../../common/sessionStoreTracking';910/**11* These tests verify the span data processing logic used by SessionStoreTracker.12*13* The tests focus on:14* 1. Tool argument extraction from OTel span attributes using the real extractToolArgs helper15* 2. File path extraction using the real extractFilePath helper16*17* Note: Full integration tests of SessionStoreTracker require mocking multiple18* services (ISessionStore, IOTelService, IChatSessionService, etc.) and are19* covered by manual testing and telemetry validation.20*/2122// Create a minimal mock span for testing23function makeSpan(overrides: Partial<ICompletedSpanData> = {}): ICompletedSpanData {24return {25name: 'test',26traceId: 'trace-1',27spanId: 'span-1',28startTime: 0,29endTime: 1,30attributes: {},31events: [],32status: { code: 0 },33...overrides,34};35}3637describe('SessionStoreTracker span processing', () => {38describe('tool argument extraction from OTel attributes', () => {39it('uses gen_ai.tool.call.arguments attribute (not gen_ai.tool.input)', () => {40// This test documents the fix for using the correct OTel attribute41const span = makeSpan({42attributes: {43// The correct attribute that OTel uses44[GenAiAttr.TOOL_CALL_ARGUMENTS]: JSON.stringify({45filePath: '/src/file.ts',46content: 'test',47}),48// This was incorrectly used before - should be ignored49'gen_ai.tool.input': JSON.stringify({ wrong: 'data' }),50},51});5253const args = extractToolArgs(span);5455expect(args).toEqual({56filePath: '/src/file.ts',57content: 'test',58});59// Verify we're not reading from the wrong attribute60expect(args).not.toHaveProperty('wrong');61});6263it('returns empty object when attribute is missing', () => {64const span = makeSpan({ attributes: {} });65expect(extractToolArgs(span)).toEqual({});66});6768it('returns empty object for malformed JSON', () => {69const span = makeSpan({70attributes: {71[GenAiAttr.TOOL_CALL_ARGUMENTS]: 'not valid json {',72},73});74expect(extractToolArgs(span)).toEqual({});75});7677it('returns empty object for non-string attribute', () => {78const span = makeSpan({79attributes: {80[GenAiAttr.TOOL_CALL_ARGUMENTS]: 12345 as unknown as string,81},82});83expect(extractToolArgs(span)).toEqual({});84});85});8687describe('file path extraction pipeline', () => {88// These tests verify the full pipeline: span -> extractToolArgs -> extractFilePath8990it('extracts file from replace_string_in_file span', () => {91const span = makeSpan({92attributes: {93[GenAiAttr.TOOL_CALL_ARGUMENTS]: JSON.stringify({94filePath: '/workspace/src/utils.ts',95oldString: 'old',96newString: 'new',97}),98},99});100101const args = extractToolArgs(span);102const filePath = extractFilePath('replace_string_in_file', args);103104expect(filePath).toBe('/workspace/src/utils.ts');105});106107it('extracts file from create_file span', () => {108const span = makeSpan({109attributes: {110[GenAiAttr.TOOL_CALL_ARGUMENTS]: JSON.stringify({111filePath: '/new/module.ts',112content: 'export {}',113}),114},115});116117const args = extractToolArgs(span);118const filePath = extractFilePath('create_file', args);119120expect(filePath).toBe('/new/module.ts');121});122123it('extracts file from apply_patch span using input field', () => {124const patchInput = '*** Begin Patch\n*** Update File: /lib/helpers.ts\n@@export\n-old\n+new\n*** End Patch';125const span = makeSpan({126attributes: {127[GenAiAttr.TOOL_CALL_ARGUMENTS]: JSON.stringify({ input: patchInput }),128},129});130131const args = extractToolArgs(span);132const filePath = extractFilePath('apply_patch', args);133134expect(filePath).toBe('/lib/helpers.ts');135});136137it('extracts file from multi_replace_string_in_file span', () => {138const span = makeSpan({139attributes: {140[GenAiAttr.TOOL_CALL_ARGUMENTS]: JSON.stringify({141explanation: 'fix imports',142replacements: [143{ filePath: '/src/a.ts', oldString: 'x', newString: 'y' },144{ filePath: '/src/b.ts', oldString: 'x', newString: 'y' },145],146}),147},148});149150const args = extractToolArgs(span);151const filePath = extractFilePath('multi_replace_string_in_file', args);152153// extractFilePath returns first file from replacements array154expect(filePath).toBe('/src/a.ts');155});156157it('returns undefined for non-file tools', () => {158const span = makeSpan({159attributes: {160[GenAiAttr.TOOL_CALL_ARGUMENTS]: JSON.stringify({ command: 'ls -la' }),161},162});163164const args = extractToolArgs(span);165const filePath = extractFilePath('run_in_terminal', args);166167expect(filePath).toBeUndefined();168});169170it('returns undefined when args are missing filePath', () => {171const span = makeSpan({172attributes: {173[GenAiAttr.TOOL_CALL_ARGUMENTS]: JSON.stringify({ content: 'no path' }),174},175});176177const args = extractToolArgs(span);178const filePath = extractFilePath('create_file', args);179180expect(filePath).toBeUndefined();181});182});183});184185186