mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 17:19:48 +01:00
Associate persisted interactive session data with providerID, add unit test (#176188)
This commit is contained in:
@@ -92,13 +92,19 @@ export interface IInteractiveSessionModel {
|
||||
getRequests(): IInteractiveRequestModel[];
|
||||
}
|
||||
|
||||
export interface IDeserializedInteractiveSessionData {
|
||||
export interface IDeserializedInteractiveSessionItem {
|
||||
requests: InteractiveRequestModel[];
|
||||
providerId: string;
|
||||
providerState: any;
|
||||
}
|
||||
|
||||
export interface IDeserializedInteractiveSessionsData {
|
||||
[providerId: string]: IDeserializedInteractiveSessionItem[];
|
||||
}
|
||||
|
||||
export interface ISerializableInteractiveSessionData {
|
||||
requests: { message: string; response: string | undefined }[];
|
||||
providerId: string;
|
||||
providerState: any;
|
||||
}
|
||||
|
||||
@@ -128,7 +134,7 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS
|
||||
private _requests: InteractiveRequestModel[];
|
||||
private _providerState: any;
|
||||
|
||||
static deserialize(obj: ISerializableInteractiveSessionData): IDeserializedInteractiveSessionData {
|
||||
static deserialize(obj: ISerializableInteractiveSessionData): IDeserializedInteractiveSessionItem {
|
||||
const requests = obj.requests;
|
||||
if (!Array.isArray(requests)) {
|
||||
throw new Error(`Malformed session data: ${obj}`);
|
||||
@@ -141,14 +147,14 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS
|
||||
}
|
||||
return request;
|
||||
});
|
||||
return { requests: requestModels, providerState: obj.providerState };
|
||||
return { requests: requestModels, providerState: obj.providerState, providerId: obj.providerId };
|
||||
}
|
||||
|
||||
get sessionId(): number {
|
||||
return this.session.id;
|
||||
}
|
||||
|
||||
constructor(public readonly session: IInteractiveSession, public readonly providerId: string, initialData?: IDeserializedInteractiveSessionData) {
|
||||
constructor(public readonly session: IInteractiveSession, public readonly providerId: string, initialData?: IDeserializedInteractiveSessionItem) {
|
||||
super();
|
||||
this._requests = initialData ? initialData.requests : [];
|
||||
this._providerState = initialData ? initialData.providerState : undefined;
|
||||
@@ -202,6 +208,7 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS
|
||||
response: r.response ? r.response.response.value : undefined,
|
||||
};
|
||||
}),
|
||||
providerId: this.providerId,
|
||||
providerState: this._providerState
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface IInteractiveProvider {
|
||||
id: string;
|
||||
prepareSession(initialState: IPersistedInteractiveState | undefined, token: CancellationToken): ProviderResult<IInteractiveSession | undefined>;
|
||||
resolveRequest?(session: IInteractiveSession, context: any, token: CancellationToken): ProviderResult<IInteractiveRequest>;
|
||||
provideSuggestions(token: CancellationToken): ProviderResult<string[] | undefined>;
|
||||
provideSuggestions?(token: CancellationToken): ProviderResult<string[] | undefined>;
|
||||
provideReply(request: IInteractiveRequest, progress: (progress: IInteractiveProgress) => void, token: CancellationToken): ProviderResult<IInteractiveResponse>;
|
||||
}
|
||||
|
||||
|
||||
+21
-12
@@ -4,12 +4,13 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { groupBy } from 'vs/base/common/collections';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { InteractiveRequestModel, InteractiveSessionModel, IDeserializedInteractiveSessionData } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel';
|
||||
import { IDeserializedInteractiveSessionsData, InteractiveRequestModel, InteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel';
|
||||
import { IInteractiveProgress, IInteractiveProvider, IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
@@ -21,7 +22,7 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
|
||||
private readonly _providers = new Map<string, IInteractiveProvider>();
|
||||
private readonly _sessionModels = new Map<number, InteractiveSessionModel>();
|
||||
private readonly _pendingRequestSessions = new Set<number>();
|
||||
private readonly _unprocessedPersistedSessions: IDeserializedInteractiveSessionData[];
|
||||
private readonly _unprocessedPersistedSessions: IDeserializedInteractiveSessionsData;
|
||||
|
||||
constructor(
|
||||
@IStorageService storageService: IStorageService,
|
||||
@@ -31,10 +32,11 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
|
||||
super();
|
||||
const sessionData = storageService.get(serializedInteractiveSessionKey, StorageScope.WORKSPACE, '');
|
||||
if (sessionData) {
|
||||
this._unprocessedPersistedSessions = this.restoreInteractiveSessions(sessionData);
|
||||
this.trace('constructor', `Restored ${this._unprocessedPersistedSessions.length} persisted sessions`);
|
||||
this._unprocessedPersistedSessions = this.deserializeInteractiveSessions(sessionData);
|
||||
const countsForLog = Object.keys(this._unprocessedPersistedSessions).map(key => `${key}: ${this._unprocessedPersistedSessions[key].length}`).join(', ');
|
||||
this.trace('constructor', `Restored persisted sessions: ${countsForLog}`);
|
||||
} else {
|
||||
this._unprocessedPersistedSessions = [];
|
||||
this._unprocessedPersistedSessions = {};
|
||||
this.trace('constructor', 'No persisted sessions');
|
||||
}
|
||||
|
||||
@@ -53,17 +55,18 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
|
||||
this.logService.error(`[InteractiveSessionService#${method}] ${message}`);
|
||||
}
|
||||
|
||||
private restoreInteractiveSessions(sessionData: string): IDeserializedInteractiveSessionData[] {
|
||||
private deserializeInteractiveSessions(sessionData: string): IDeserializedInteractiveSessionsData {
|
||||
try {
|
||||
const obj = JSON.parse(sessionData);
|
||||
if (!Array.isArray(obj)) {
|
||||
throw new Error('Expected array');
|
||||
}
|
||||
|
||||
return obj.map(item => InteractiveSessionModel.deserialize(item));
|
||||
const items = obj.map(item => InteractiveSessionModel.deserialize(item));
|
||||
return groupBy(items, item => item.providerId);
|
||||
} catch (err) {
|
||||
this.error('restoreInteractiveSessions', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}...]`);
|
||||
return [];
|
||||
this.error('deserializeInteractiveSessions', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}${sessionData.length > 20 ? '...' : ''}]`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,12 +79,13 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
|
||||
throw new Error(`Unknown provider: ${providerId}`);
|
||||
}
|
||||
|
||||
const someSessionHistory = allowRestoringSession ? this._unprocessedPersistedSessions.shift() : undefined;
|
||||
const providerData = this._unprocessedPersistedSessions[providerId] ?? [];
|
||||
const someSessionHistory = allowRestoringSession ? providerData.shift() : undefined;
|
||||
this.trace('startSession', `Has history: ${!!someSessionHistory}. Including provider state: ${!!someSessionHistory?.providerState}`);
|
||||
const session = await provider.prepareSession(someSessionHistory?.providerState, token);
|
||||
if (!session) {
|
||||
if (someSessionHistory) {
|
||||
this._unprocessedPersistedSessions.unshift(someSessionHistory);
|
||||
providerData.unshift(someSessionHistory);
|
||||
}
|
||||
|
||||
this.trace('startSession', 'Provider returned no session');
|
||||
@@ -95,7 +99,7 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
|
||||
}
|
||||
|
||||
sendRequest(sessionId: number, message: string, token: CancellationToken): boolean {
|
||||
this.trace('sendRequest', `sessionId: ${sessionId}, message: ${message.substring(0, 20)}[...]`);
|
||||
this.trace('sendRequest', `sessionId: ${sessionId}, message: ${message.substring(0, 20)}${message.length > 20 ? '[...]' : ''}}`);
|
||||
if (!message.trim()) {
|
||||
this.trace('sendRequest', 'Rejected empty message');
|
||||
return false;
|
||||
@@ -208,6 +212,7 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
|
||||
}
|
||||
|
||||
async provideSuggestions(providerId: string, token: CancellationToken): Promise<string[] | undefined> {
|
||||
this.trace('provideSuggestions', `Called for provider ${providerId}`);
|
||||
await this.extensionService.activateByEvent(`onInteractiveSession:${providerId}`);
|
||||
|
||||
const provider = this._providers.get(providerId);
|
||||
@@ -215,6 +220,10 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
|
||||
throw new Error(`Unknown provider: ${providerId}`);
|
||||
}
|
||||
|
||||
if (!provider.provideSuggestions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const suggestions = await provider.provideSuggestions(token);
|
||||
this.trace('provideSuggestions', `Provider returned ${suggestions?.length} suggestions`);
|
||||
return withNullAsUndefined(suggestions);
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IInteractiveProvider, IInteractiveRequest } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
|
||||
import { InteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
|
||||
suite('InteractiveSession', () => {
|
||||
const testDisposables = new DisposableStore();
|
||||
|
||||
let storageService: IStorageService;
|
||||
let instantiationService: TestInstantiationService;
|
||||
|
||||
suiteSetup(async () => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
instantiationService.stub(IStorageService, storageService = new TestStorageService());
|
||||
instantiationService.stub(ILogService, new NullLogService());
|
||||
instantiationService.stub(IExtensionService, new TestExtensionService());
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
testDisposables.clear();
|
||||
});
|
||||
|
||||
test('Restores state for the correct provider', async () => {
|
||||
let sessionId = 0;
|
||||
function getTestProvider(providerId: string) {
|
||||
return new class implements IInteractiveProvider {
|
||||
readonly id = providerId;
|
||||
|
||||
lastInitialState = undefined;
|
||||
|
||||
prepareSession(initialState: any) {
|
||||
this.lastInitialState = initialState;
|
||||
return Promise.resolve({ id: sessionId++ });
|
||||
}
|
||||
|
||||
async provideReply(request: IInteractiveRequest) {
|
||||
return { session: request.session, followups: [] };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const testService = instantiationService.createInstance(InteractiveSessionService);
|
||||
const provider1 = getTestProvider('provider1');
|
||||
const provider2 = getTestProvider('provider2');
|
||||
testService.registerProvider(provider1);
|
||||
testService.registerProvider(provider2);
|
||||
|
||||
let session1 = await testService.startSession('provider1', true, CancellationToken.None);
|
||||
let session2 = await testService.startSession('provider2', true, CancellationToken.None);
|
||||
assert.strictEqual(provider1.lastInitialState, undefined);
|
||||
assert.strictEqual(provider2.lastInitialState, undefined);
|
||||
testService.acceptNewSessionState(session1!.sessionId, { state: 'provider1_state' });
|
||||
testService.acceptNewSessionState(session2!.sessionId, { state: 'provider2_state' });
|
||||
storageService.flush();
|
||||
|
||||
const testService2 = instantiationService.createInstance(InteractiveSessionService);
|
||||
testService2.registerProvider(provider1);
|
||||
testService2.registerProvider(provider2);
|
||||
session1 = await testService2.startSession('provider1', true, CancellationToken.None);
|
||||
session2 = await testService2.startSession('provider2', true, CancellationToken.None);
|
||||
assert.deepStrictEqual(provider1.lastInitialState, { state: 'provider1_state' });
|
||||
assert.deepStrictEqual(provider2.lastInitialState, { state: 'provider2_state' });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user