Path: blob/main/src/vs/workbench/contrib/mcp/test/common/mcpSamplingLog.test.ts
5338 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 * as assert from 'assert';6import * as sinon from 'sinon';7import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';8import {9StorageScope10} from '../../../../../platform/storage/common/storage.js';11import { TestStorageService } from '../../../../test/common/workbenchTestServices.js';12import { ISamplingStoredData, McpSamplingLog } from '../../common/mcpSamplingLog.js';13import { IMcpServer } from '../../common/mcpTypes.js';14import { asArray } from '../../../../../base/common/arrays.js';1516suite('MCP - Sampling Log', () => {17const ds = ensureNoDisposablesAreLeakedInTestSuite();18const fakeServer: IMcpServer = {19definition: { id: 'testServer' },20readDefinitions: () => ({21get: () => ({ collection: { scope: StorageScope.APPLICATION } }),22}),23} as IMcpServer;2425let log: McpSamplingLog;26let storage: TestStorageService;27let clock: sinon.SinonFakeTimers;2829setup(() => {30storage = ds.add(new TestStorageService());31log = ds.add(new McpSamplingLog(storage));32clock = sinon.useFakeTimers();33clock.setSystemTime(new Date('2023-10-01T00:00:00Z').getTime());34});3536teardown(() => {37clock.restore();38});3940test('logs a single request', async () => {41log.add(42fakeServer,43[{ role: 'user', content: { type: 'text', text: 'test request' } }],44'test response here',45'foobar9000',46);4748// storage.testEmitWillSaveState(WillSaveStateReason.NONE);49await storage.flush();50assert.deepStrictEqual(51(storage.getObject('mcp.sampling.logs', StorageScope.APPLICATION) as unknown),52[53[54'testServer',55{56head: 19631,57bins: [1, 0, 0, 0, 0, 0, 0],58lastReqs: [59{60request: [{ role: 'user', content: { type: 'text', text: 'test request' } }],61response: 'test response here',62at: 1696118400000,63model: 'foobar9000',64},65],66},67],68],69);70});7172test('logs multiple requests on the same day', async () => {73// First request74log.add(75fakeServer,76[{ role: 'user', content: { type: 'text', text: 'first request' } }],77'first response',78'foobar9000',79);8081// Advance time by a few hours but stay on the same day82clock.tick(5 * 60 * 60 * 1000); // 5 hours8384// Second request85log.add(86fakeServer,87[{ role: 'user', content: { type: 'text', text: 'second request' } }],88'second response',89'foobar9000',90);9192await storage.flush();93const data = (storage.getObject('mcp.sampling.logs', StorageScope.APPLICATION) as [string, any][])[0][1];9495// Verify the bin for the current day has 2 requests96assert.strictEqual(data.bins[0], 2);9798// Verify both requests are in the lastReqs array, with the most recent first99assert.strictEqual(data.lastReqs.length, 2);100assert.strictEqual(data.lastReqs[0].request[0].content.text, 'second request');101assert.strictEqual(data.lastReqs[1].request[0].content.text, 'first request');102});103104test('shifts bins when adding requests on different days', async () => {105// First request on day 1106log.add(107fakeServer,108[{ role: 'user', content: { type: 'text', text: 'day 1 request' } }],109'day 1 response',110'foobar9000',111);112113// Advance time to the next day114clock.tick(24 * 60 * 60 * 1000);115116// Second request on day 2117log.add(118fakeServer,119[{ role: 'user', content: { type: 'text', text: 'day 2 request' } }],120'day 2 response',121'foobar9000',122);123124await storage.flush();125const data = (storage.getObject('mcp.sampling.logs', StorageScope.APPLICATION) as [string, ISamplingStoredData][])[0][1];126127// Verify the bins: day 2 should have 1 request, day 1 should have 1 request128assert.strictEqual(data.bins[0], 1); // day 2129assert.strictEqual(data.bins[1], 1); // day 1130131// Advance time by 5 more days132clock.tick(5 * 24 * 60 * 60 * 1000);133134// Request on day 7135log.add(136fakeServer,137[{ role: 'user', content: { type: 'text', text: 'day 7 request' } }],138'day 7 response',139'foobar9000',140);141142await storage.flush();143const updatedData = (storage.getObject('mcp.sampling.logs', StorageScope.APPLICATION) as [string, ISamplingStoredData][])[0][1];144145// Verify the bins have shifted correctly146assert.strictEqual(updatedData.bins[0], 1); // day 7147assert.strictEqual(updatedData.bins[5], 1); // day 2148assert.strictEqual(updatedData.bins[6], 1); // day 1149});150151test('limits the number of stored requests', async () => {152// Add more than the maximum number of requests (Constants.SamplingLastNMessage = 30)153for (let i = 0; i < 35; i++) {154log.add(155fakeServer,156[{ role: 'user', content: { type: 'text', text: `request ${i}` } }],157`response ${i}`,158'foobar9000',159);160}161162await storage.flush();163const data = (storage.getObject('mcp.sampling.logs', StorageScope.APPLICATION) as [string, ISamplingStoredData][])[0][1];164165// Verify only the last 30 requests are kept166assert.strictEqual(data.lastReqs.length, 30);167assert.strictEqual((data.lastReqs[0].request[0].content as { type: 'text'; text: string }).text, 'request 34');168assert.strictEqual((data.lastReqs[29].request[0].content as { type: 'text'; text: string }).text, 'request 5');169});170171test('handles different content types', async () => {172// Add a request with text content173log.add(174fakeServer,175[{ role: 'user', content: { type: 'text', text: 'text request' } }],176'text response',177'foobar9000',178);179180// Add a request with image content181log.add(182fakeServer,183[{184role: 'user',185content: {186type: 'image',187data: 'base64data',188mimeType: 'image/png'189}190}],191'image response',192'foobar9000',193);194195// Add a request with mixed content196log.add(197fakeServer,198[199{ role: 'user', content: { type: 'text', text: 'text and image' } },200{201role: 'assistant',202content: {203type: 'image',204data: 'base64data',205mimeType: 'image/jpeg'206}207}208],209'mixed response',210'foobar9000',211);212213await storage.flush();214const data = (storage.getObject('mcp.sampling.logs', StorageScope.APPLICATION) as [string, ISamplingStoredData][])[0][1];215216// Verify all requests are stored correctly217assert.strictEqual(data.lastReqs.length, 3);218assert.strictEqual(data.lastReqs[0].request.length, 2); // Mixed content request has 2 messages219assert.strictEqual(asArray(data.lastReqs[1].request[0].content)[0].type, 'image');220assert.strictEqual(asArray(data.lastReqs[2].request[0].content)[0].type, 'text');221});222223test('handles multiple servers', async () => {224const fakeServer2: IMcpServer = {225definition: { id: 'testServer2' },226readDefinitions: () => ({227get: () => ({ collection: { scope: StorageScope.APPLICATION } }),228}),229} as IMcpServer;230231log.add(232fakeServer,233[{ role: 'user', content: { type: 'text', text: 'server1 request' } }],234'server1 response',235'foobar9000',236);237238log.add(239fakeServer2,240[{ role: 'user', content: { type: 'text', text: 'server2 request' } }],241'server2 response',242'foobar9000',243);244245await storage.flush();246const storageData = (storage.getObject('mcp.sampling.logs', StorageScope.APPLICATION) as [string, ISamplingStoredData][]);247248// Verify both servers have their data stored249assert.strictEqual(storageData.length, 2);250assert.strictEqual(storageData[0][0], 'testServer');251assert.strictEqual(storageData[1][0], 'testServer2');252});253});254255256