Files
vscode/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts
T

454 lines
21 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { mock } from 'vs/base/test/common/mock';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService';
import { Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/model';
import { instantiateTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/common/progress';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService';
import { HunkState, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService';
import { InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl';
import { EditMode, IInlineChatService, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
import { CancellationToken } from 'vs/base/common/cancellation';
import { assertType } from 'vs/base/common/types';
import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position } from 'vs/editor/common/core/position';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { TestWorkerService } from './testWorkerService';
import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl';
import { IChatSlashCommandService, ChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { IChatWidgetHistoryService, ChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService';
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
import { TestExtensionService, TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
import { IChatAgentService, ChatAgentService, ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables';
suite('InlineChatSession', function () {
const store = new DisposableStore();
let editor: IActiveCodeEditor;
let model: ITextModel;
let instaService: TestInstantiationService;
let inlineChatService: InlineChatServiceImpl;
let inlineChatSessionService: IInlineChatSessionService;
setup(function () {
const contextKeyService = new MockContextKeyService();
const serviceCollection = new ServiceCollection(
[IConfigurationService, new TestConfigurationService()],
[IChatVariablesService, new SyncDescriptor(ChatVariablesService)],
[ILogService, new NullLogService()],
[ITelemetryService, NullTelemetryService],
[IExtensionService, new TestExtensionService()],
[IContextKeyService, new MockContextKeyService()],
[IViewsService, new TestExtensionService()],
[IWorkspaceContextService, new TestContextService()],
[IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)],
[IChatWidgetService, new SyncDescriptor(ChatWidgetService)],
[IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)],
[IChatService, new SyncDescriptor(ChatService)],
[IEditorWorkerService, new SyncDescriptor(TestWorkerService)],
[IChatAgentService, new SyncDescriptor(ChatAgentService)],
[IInlineChatService, new SyncDescriptor(InlineChatServiceImpl)],
[IContextKeyService, contextKeyService],
[IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)],
[IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)],
[IInlineChatSavingService, new class extends mock<IInlineChatSavingService>() {
override markChanged(session: Session): void {
// noop
}
}],
[IEditorProgressService, new class extends mock<IEditorProgressService>() {
override show(total: unknown, delay?: unknown): IProgressRunner {
return {
total() { },
worked(value) { },
done() { },
};
}
}],
[IChatAccessibilityService, new class extends mock<IChatAccessibilityService>() {
override acceptResponse(response: IChatResponseViewModel | undefined, requestId: number): void { }
override acceptRequest(): number { return -1; }
}],
[IAccessibleViewService, new class extends mock<IAccessibleViewService>() {
override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {
return null;
}
}],
[IConfigurationService, new TestConfigurationService()],
[IViewDescriptorService, new class extends mock<IViewDescriptorService>() {
override onDidChangeLocation = Event.None;
}]
);
instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection));
inlineChatService = instaService.get(IInlineChatService) as InlineChatServiceImpl;
inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService));
instaService.get(IChatAgentService).registerDynamicAgent({
extensionId: nullExtensionDescription.identifier,
id: 'testAgent',
name: 'testAgent',
isDefault: true,
locations: [ChatAgentLocation.Panel],
metadata: {},
slashCommands: []
}, {
async invoke() {
return {};
}
});
store.add(inlineChatService.addProvider({
extensionId: nullExtensionDescription.identifier,
label: 'Unit Test',
prepareInlineChatSession() {
return {
id: Math.random()
};
},
provideResponse(session, request) {
return {
type: InlineChatResponseType.EditorEdit,
id: Math.random(),
edits: [{
range: new Range(1, 1, 1, 1),
text: request.prompt
}]
};
}
}));
model = store.add(instaService.get(IModelService).createModel('one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven', null));
editor = store.add(instantiateTestCodeEditor(instaService, model));
});
teardown(function () {
store.clear();
});
ensureNoDisposablesAreLeakedInTestSuite();
async function makeEditAsAi(edit: EditOperation | EditOperation[]) {
const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri);
assertType(session);
session.hunkData.ignoreTextModelNChanges = true;
try {
editor.executeEdits('test', Array.isArray(edit) ? edit : [edit]);
} finally {
session.hunkData.ignoreTextModelNChanges = false;
}
await session.hunkData.recompute();
}
function makeEdit(edit: EditOperation | EditOperation[]) {
editor.executeEdits('test', Array.isArray(edit) ? edit : [edit]);
}
test('Create, release', async function () {
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
inlineChatSessionService.releaseSession(session);
});
test('HunkData, info', async function () {
const decorationCountThen = model.getAllDecorations().length;
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
assert.ok(session.textModelN === model);
await makeEditAsAi(EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'));
assert.strictEqual(session.hunkData.size, 1);
let [hunk] = session.hunkData.getInfo();
assertType(hunk);
assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer()));
assert.strictEqual(hunk.getState(), HunkState.Pending);
assert.ok(hunk.getRangesN()[0].equalsRange({ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 7 }));
await makeEditAsAi(EditOperation.insert(new Position(1, 3), 'foobar'));
[hunk] = session.hunkData.getInfo();
assert.ok(hunk.getRangesN()[0].equalsRange({ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 13 }));
inlineChatSessionService.releaseSession(session);
assert.strictEqual(model.getAllDecorations().length, decorationCountThen); // no leaked decorations!
});
test('HunkData, accept', async function () {
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]);
assert.strictEqual(session.hunkData.size, 2);
assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer()));
for (const hunk of session.hunkData.getInfo()) {
assertType(hunk);
assert.strictEqual(hunk.getState(), HunkState.Pending);
hunk.acceptChanges();
assert.strictEqual(hunk.getState(), HunkState.Accepted);
}
assert.strictEqual(session.textModel0.getValue(), session.textModelN.getValue());
inlineChatSessionService.releaseSession(session);
});
test('HunkData, reject', async function () {
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]);
assert.strictEqual(session.hunkData.size, 2);
assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer()));
for (const hunk of session.hunkData.getInfo()) {
assertType(hunk);
assert.strictEqual(hunk.getState(), HunkState.Pending);
hunk.discardChanges();
assert.strictEqual(hunk.getState(), HunkState.Rejected);
}
assert.strictEqual(session.textModel0.getValue(), session.textModelN.getValue());
inlineChatSessionService.releaseSession(session);
});
test('HunkData, N rounds', async function () {
model.setValue('one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven\ntwelwe\nthirteen\nfourteen\nfifteen\nsixteen\nseventeen\neighteen\nnineteen\n');
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
assert.ok(session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer()));
assert.strictEqual(session.hunkData.size, 0);
// ROUND #1
await makeEditAsAi([
EditOperation.insert(new Position(1, 1), 'AI1'),
EditOperation.insert(new Position(4, 1), 'AI2'),
EditOperation.insert(new Position(19, 1), 'AI3')
]);
assert.strictEqual(session.hunkData.size, 2); // AI1, AI2 are merged into one hunk, AI3 is a separate hunk
let [first, second] = session.hunkData.getInfo();
assert.ok(model.getValueInRange(first.getRangesN()[0]).includes('AI1'));
assert.ok(model.getValueInRange(first.getRangesN()[0]).includes('AI2'));
assert.ok(model.getValueInRange(second.getRangesN()[0]).includes('AI3'));
assert.ok(!session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI1'));
assert.ok(!session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI2'));
assert.ok(!session.textModel0.getValueInRange(second.getRangesN()[0]).includes('AI3'));
first.acceptChanges();
assert.ok(session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI1'));
assert.ok(session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI2'));
assert.ok(!session.textModel0.getValueInRange(second.getRangesN()[0]).includes('AI3'));
// ROUND #2
await makeEditAsAi([
EditOperation.insert(new Position(7, 1), 'AI4'),
]);
assert.strictEqual(session.hunkData.size, 2);
[first, second] = session.hunkData.getInfo();
assert.ok(model.getValueInRange(first.getRangesN()[0]).includes('AI4')); // the new hunk (in line-order)
assert.ok(model.getValueInRange(second.getRangesN()[0]).includes('AI3')); // the previous hunk remains
inlineChatSessionService.releaseSession(session);
});
test('HunkData, (mirror) edit before', async function () {
const lines = ['one', 'two', 'three'];
model.setValue(lines.join('\n'));
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]);
assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI WAS HERE', 'three'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), lines.join('\n'));
makeEdit([EditOperation.replace(new Range(1, 1, 1, 4), 'ONE')]);
assert.strictEqual(session.textModelN.getValue(), ['ONE', 'two', 'AI WAS HERE', 'three'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['ONE', 'two', 'three'].join('\n'));
});
test('HunkData, (mirror) edit after', async function () {
const lines = ['one', 'two', 'three', 'four', 'five'];
model.setValue(lines.join('\n'));
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]);
assert.strictEqual(session.hunkData.size, 1);
const [hunk] = session.hunkData.getInfo();
makeEdit([EditOperation.insert(new Position(1, 1), 'USER1')]);
assert.strictEqual(session.textModelN.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'four', 'five'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['USER1one', 'two', 'three', 'four', 'five'].join('\n'));
makeEdit([EditOperation.insert(new Position(5, 1), 'USER2')]);
assert.strictEqual(session.textModelN.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'USER2four', 'five'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['USER1one', 'two', 'three', 'USER2four', 'five'].join('\n'));
hunk.acceptChanges();
assert.strictEqual(session.textModelN.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'USER2four', 'five'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'USER2four', 'five'].join('\n'));
});
test('HunkData, (mirror) edit inside ', async function () {
const lines = ['one', 'two', 'three'];
model.setValue(lines.join('\n'));
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]);
assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI WAS HERE', 'three'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), lines.join('\n'));
makeEdit([EditOperation.replace(new Range(3, 4, 3, 7), 'wwaaassss')]);
assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI wwaaassss HERE', 'three'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three'].join('\n'));
});
test('HunkData, (mirror) edit after dicard ', async function () {
const lines = ['one', 'two', 'three'];
model.setValue(lines.join('\n'));
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]);
assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI WAS HERE', 'three'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), lines.join('\n'));
assert.strictEqual(session.hunkData.size, 1);
const [hunk] = session.hunkData.getInfo();
hunk.discardChanges();
assert.strictEqual(session.textModelN.getValue(), lines.join('\n'));
assert.strictEqual(session.textModel0.getValue(), lines.join('\n'));
makeEdit([EditOperation.replace(new Range(3, 4, 3, 6), '3333')]);
assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'thr3333'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'thr3333'].join('\n'));
});
test('HunkData, (mirror) edit after, multi turn', async function () {
const lines = ['one', 'two', 'three', 'four', 'five'];
model.setValue(lines.join('\n'));
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]);
assert.strictEqual(session.hunkData.size, 1);
makeEdit([EditOperation.insert(new Position(5, 1), 'FOO')]);
assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n'));
await makeEditAsAi([EditOperation.insert(new Position(2, 4), ' zwei')]);
assert.strictEqual(session.hunkData.size, 1);
assert.strictEqual(session.textModelN.getValue(), ['one', 'two zwei', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n'));
makeEdit([EditOperation.replace(new Range(6, 3, 6, 5), 'vefivefi')]);
assert.strictEqual(session.textModelN.getValue(), ['one', 'two zwei', 'AI_EDIT', 'three', 'FOOfour', 'fivefivefi'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'fivefivefi'].join('\n'));
});
test('HunkData, (mirror) edit after, multi turn 2', async function () {
const lines = ['one', 'two', 'three', 'four', 'five'];
model.setValue(lines.join('\n'));
const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None);
assertType(session);
await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]);
assert.strictEqual(session.hunkData.size, 1);
makeEdit([EditOperation.insert(new Position(5, 1), 'FOO')]);
assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n'));
await makeEditAsAi([EditOperation.insert(new Position(2, 4), 'zwei')]);
assert.strictEqual(session.hunkData.size, 1);
assert.strictEqual(session.textModelN.getValue(), ['one', 'twozwei', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n'));
makeEdit([EditOperation.replace(new Range(6, 3, 6, 5), 'vefivefi')]);
assert.strictEqual(session.textModelN.getValue(), ['one', 'twozwei', 'AI_EDIT', 'three', 'FOOfour', 'fivefivefi'].join('\n'));
assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'fivefivefi'].join('\n'));
session.hunkData.getInfo()[0].acceptChanges();
assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue());
makeEdit([EditOperation.replace(new Range(1, 1, 1, 1), 'done')]);
assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue());
});
});