Path: blob/main/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts
3296 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 assert from 'assert';6import { MarkdownString } from '../../../../../base/common/htmlContent.js';7import { URI } from '../../../../../base/common/uri.js';8import { assertSnapshot } from '../../../../../base/test/common/snapshot.js';9import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';10import { Range } from '../../../../../editor/common/core/range.js';11import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';12import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';13import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';14import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';15import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';16import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';17import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';18import { IStorageService } from '../../../../../platform/storage/common/storage.js';19import { IExtensionService } from '../../../../services/extensions/common/extensions.js';20import { TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';21import { ChatAgentService, IChatAgentService } from '../../common/chatAgents.js';22import { ChatModel, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, normalizeSerializableChatData, Response } from '../../common/chatModel.js';23import { ChatRequestTextPart } from '../../common/chatParserTypes.js';24import { ChatAgentLocation } from '../../common/constants.js';2526suite('ChatModel', () => {27const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();2829let instantiationService: TestInstantiationService;3031setup(async () => {32instantiationService = testDisposables.add(new TestInstantiationService());33instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService()));34instantiationService.stub(ILogService, new NullLogService());35instantiationService.stub(IExtensionService, new TestExtensionService());36instantiationService.stub(IContextKeyService, new MockContextKeyService());37instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService)));38instantiationService.stub(IConfigurationService, new TestConfigurationService());39});4041test('removeRequest', async () => {42const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel));4344const text = 'hello';45model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0);46const requests = model.getRequests();47assert.strictEqual(requests.length, 1);4849model.removeRequest(requests[0].id);50assert.strictEqual(model.getRequests().length, 0);51});5253test('adoptRequest', async function () {54const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor));55const model2 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel));5657const text = 'hello';58const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0);5960assert.strictEqual(model1.getRequests().length, 1);61assert.strictEqual(model2.getRequests().length, 0);62assert.ok(request1.session === model1);63assert.ok(request1.response?.session === model1);6465model2.adoptRequest(request1);6667assert.strictEqual(model1.getRequests().length, 0);68assert.strictEqual(model2.getRequests().length, 1);69assert.ok(request1.session === model2);70assert.ok(request1.response?.session === model2);7172model2.acceptResponseProgress(request1, { content: new MarkdownString('Hello'), kind: 'markdownContent' });7374assert.strictEqual(request1.response.response.toString(), 'Hello');75});7677test('addCompleteRequest', async function () {78const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel));7980const text = 'hello';81const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0, undefined, undefined, undefined, undefined, undefined, undefined, true);8283assert.strictEqual(request1.isCompleteAddedRequest, true);84assert.strictEqual(request1.response!.isCompleteAddedRequest, true);85assert.strictEqual(request1.shouldBeRemovedOnSend, undefined);86assert.strictEqual(request1.response!.shouldBeRemovedOnSend, undefined);87});88});8990suite('Response', () => {91const store = ensureNoDisposablesAreLeakedInTestSuite();9293test('mergeable markdown', async () => {94const response = store.add(new Response([]));95response.updateContent({ content: new MarkdownString('markdown1'), kind: 'markdownContent' });96response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' });97await assertSnapshot(response.value);9899assert.strictEqual(response.toString(), 'markdown1markdown2');100});101102test('not mergeable markdown', async () => {103const response = store.add(new Response([]));104const md1 = new MarkdownString('markdown1');105md1.supportHtml = true;106response.updateContent({ content: md1, kind: 'markdownContent' });107response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' });108await assertSnapshot(response.value);109});110111test('inline reference', async () => {112const response = store.add(new Response([]));113response.updateContent({ content: new MarkdownString('text before '), kind: 'markdownContent' });114response.updateContent({ inlineReference: URI.parse('https://microsoft.com/'), kind: 'inlineReference' });115response.updateContent({ content: new MarkdownString(' text after'), kind: 'markdownContent' });116await assertSnapshot(response.value);117118assert.strictEqual(response.toString(), 'text before https://microsoft.com/ text after');119120});121122test('consolidated edit summary', async () => {123const response = store.add(new Response([]));124response.updateContent({ content: new MarkdownString('Some content before edits'), kind: 'markdownContent' });125response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file1.ts'), edits: [], state: undefined, done: true });126response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file2.ts'), edits: [], state: undefined, done: true });127response.updateContent({ content: new MarkdownString('Some content after edits'), kind: 'markdownContent' });128129// Should have single "Made changes." at the end instead of multiple entries130const responseString = response.toString();131const madeChangesCount = (responseString.match(/Made changes\./g) || []).length;132assert.strictEqual(madeChangesCount, 1, 'Should have exactly one "Made changes." message');133assert.ok(responseString.includes('Some content before edits'), 'Should include content before edits');134assert.ok(responseString.includes('Some content after edits'), 'Should include content after edits');135assert.ok(responseString.endsWith('Made changes.'), 'Should end with "Made changes."');136});137138test('no edit summary when no edits', async () => {139const response = store.add(new Response([]));140response.updateContent({ content: new MarkdownString('Some content'), kind: 'markdownContent' });141response.updateContent({ content: new MarkdownString('More content'), kind: 'markdownContent' });142143// Should not have "Made changes." when there are no edit groups144const responseString = response.toString();145assert.ok(!responseString.includes('Made changes.'), 'Should not include "Made changes." when no edits present');146assert.strictEqual(responseString, 'Some contentMore content');147});148149test('consolidated edit summary with clear operation', async () => {150const response = store.add(new Response([]));151response.updateContent({ content: new MarkdownString('Initial content'), kind: 'markdownContent' });152response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file1.ts'), edits: [], state: undefined, done: true });153response.updateContent({ kind: 'clearToPreviousToolInvocation', reason: 1 });154response.updateContent({ content: new MarkdownString('Content after clear'), kind: 'markdownContent' });155response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file2.ts'), edits: [], state: undefined, done: true });156157// Should only show "Made changes." for edits after the clear operation158const responseString = response.toString();159const madeChangesCount = (responseString.match(/Made changes\./g) || []).length;160assert.strictEqual(madeChangesCount, 1, 'Should have exactly one "Made changes." message after clear');161assert.ok(responseString.includes('Content after clear'), 'Should include content after clear');162assert.ok(!responseString.includes('Initial content'), 'Should not include content before clear');163assert.ok(responseString.endsWith('Made changes.'), 'Should end with "Made changes."');164});165});166167suite('normalizeSerializableChatData', () => {168ensureNoDisposablesAreLeakedInTestSuite();169170test('v1', () => {171const v1Data: ISerializableChatData1 = {172creationDate: Date.now(),173initialLocation: undefined,174isImported: false,175requesterAvatarIconUri: undefined,176requesterUsername: 'me',177requests: [],178responderAvatarIconUri: undefined,179responderUsername: 'bot',180sessionId: 'session1',181};182183const newData = normalizeSerializableChatData(v1Data);184assert.strictEqual(newData.creationDate, v1Data.creationDate);185assert.strictEqual(newData.lastMessageDate, v1Data.creationDate);186assert.strictEqual(newData.version, 3);187assert.ok('customTitle' in newData);188});189190test('v2', () => {191const v2Data: ISerializableChatData2 = {192version: 2,193creationDate: 100,194lastMessageDate: Date.now(),195initialLocation: undefined,196isImported: false,197requesterAvatarIconUri: undefined,198requesterUsername: 'me',199requests: [],200responderAvatarIconUri: undefined,201responderUsername: 'bot',202sessionId: 'session1',203computedTitle: 'computed title'204};205206const newData = normalizeSerializableChatData(v2Data);207assert.strictEqual(newData.version, 3);208assert.strictEqual(newData.creationDate, v2Data.creationDate);209assert.strictEqual(newData.lastMessageDate, v2Data.lastMessageDate);210assert.strictEqual(newData.customTitle, v2Data.computedTitle);211});212213test('old bad data', () => {214const v1Data: ISerializableChatData1 = {215// Testing the scenario where these are missing216sessionId: undefined!,217creationDate: undefined!,218219initialLocation: undefined,220isImported: false,221requesterAvatarIconUri: undefined,222requesterUsername: 'me',223requests: [],224responderAvatarIconUri: undefined,225responderUsername: 'bot',226};227228const newData = normalizeSerializableChatData(v1Data);229assert.strictEqual(newData.version, 3);230assert.ok(newData.creationDate > 0);231assert.ok(newData.lastMessageDate > 0);232assert.ok(newData.sessionId);233});234235test('v3 with bug', () => {236const v3Data: ISerializableChatData3 = {237// Test case where old data was wrongly normalized and these fields were missing238creationDate: undefined!,239lastMessageDate: undefined!,240241version: 3,242initialLocation: undefined,243isImported: false,244requesterAvatarIconUri: undefined,245requesterUsername: 'me',246requests: [],247responderAvatarIconUri: undefined,248responderUsername: 'bot',249sessionId: 'session1',250customTitle: 'computed title'251};252253const newData = normalizeSerializableChatData(v3Data);254assert.strictEqual(newData.version, 3);255assert.ok(newData.creationDate > 0);256assert.ok(newData.lastMessageDate > 0);257assert.ok(newData.sessionId);258});259});260261262