Associate persisted interactive session data with providerID, add unit test (#176188)

This commit is contained in:
Rob Lourens
2023-03-05 22:01:43 -05:00
committed by GitHub
parent adc51fb212
commit 0cab3cc3cb
4 changed files with 110 additions and 17 deletions
@@ -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>;
}
@@ -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);
@@ -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' });
});
});