deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
285
packages/ai-chat/src/common/chat-service-deletion.spec.ts
Normal file
285
packages/ai-chat/src/common/chat-service-deletion.spec.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 EclipseSource GmbH.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { Container } from '@theia/core/shared/inversify';
|
||||
import { ChatServiceImpl } from './chat-service';
|
||||
import { ChatSessionStore, ChatSessionIndex, ChatModelWithMetadata } from './chat-session-store';
|
||||
import { ChatAgentService } from './chat-agent-service';
|
||||
import { ChatRequestParser } from './chat-request-parser';
|
||||
import { AIVariableService, ToolInvocationRegistry } from '@theia/ai-core';
|
||||
import { ILogger } from '@theia/core';
|
||||
import { ChatContentDeserializerRegistry, ChatContentDeserializerRegistryImpl, DefaultChatContentDeserializerContribution } from './chat-content-deserializer';
|
||||
import { ChangeSetElementDeserializerRegistry, ChangeSetElementDeserializerRegistryImpl } from './change-set-element-deserializer';
|
||||
import { ChatAgentLocation } from './chat-agents';
|
||||
import { ChatModel } from './chat-model';
|
||||
import { SerializedChatData } from './chat-model-serialization';
|
||||
|
||||
describe('ChatService Session Deletion', () => {
|
||||
let chatService: ChatServiceImpl;
|
||||
let sessionStore: MockChatSessionStore;
|
||||
let container: Container;
|
||||
|
||||
class MockChatSessionStore implements ChatSessionStore {
|
||||
public deletedSessions: string[] = [];
|
||||
public storedSessions: Array<ChatModel | ChatModelWithMetadata> = [];
|
||||
|
||||
async storeSessions(...sessions: Array<ChatModel | ChatModelWithMetadata>): Promise<void> {
|
||||
this.storedSessions = sessions;
|
||||
}
|
||||
|
||||
async readSession(sessionId: string): Promise<SerializedChatData | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async deleteSession(sessionId: string): Promise<void> {
|
||||
this.deletedSessions.push(sessionId);
|
||||
}
|
||||
|
||||
async clearAllSessions(): Promise<void> {
|
||||
this.deletedSessions = [];
|
||||
this.storedSessions = [];
|
||||
}
|
||||
|
||||
async getSessionIndex(): Promise<ChatSessionIndex> {
|
||||
return {};
|
||||
}
|
||||
|
||||
async hasPersistedSessions(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async setSessionTitle(sessionId: string, title: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
class MockChatAgentService {
|
||||
getAgent(): undefined {
|
||||
return undefined;
|
||||
}
|
||||
getAgents(): never[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
class MockChatRequestParser {
|
||||
parseChatRequest(): { parts: never[]; text: string } {
|
||||
return { parts: [], text: '' };
|
||||
}
|
||||
}
|
||||
|
||||
class MockAIVariableService {
|
||||
resolveVariables(): Promise<unknown[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
class MockLogger {
|
||||
error(): void { }
|
||||
warn(): void { }
|
||||
info(): void { }
|
||||
debug(): void { }
|
||||
}
|
||||
|
||||
const mockToolInvocationRegistry: ToolInvocationRegistry = {
|
||||
registerTool: () => { },
|
||||
getFunction: () => undefined,
|
||||
getFunctions: () => [],
|
||||
getAllFunctions: () => [],
|
||||
unregisterAllTools: () => { },
|
||||
onDidChange: () => ({ dispose: () => { } })
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
container = new Container();
|
||||
sessionStore = new MockChatSessionStore();
|
||||
|
||||
container.bind(ChatSessionStore).toConstantValue(sessionStore);
|
||||
container.bind(ChatAgentService).toConstantValue(new MockChatAgentService() as unknown as ChatAgentService);
|
||||
container.bind(ChatRequestParser).toConstantValue(new MockChatRequestParser() as unknown as ChatRequestParser);
|
||||
container.bind(AIVariableService).toConstantValue(new MockAIVariableService() as unknown as AIVariableService);
|
||||
container.bind(ILogger).toConstantValue(new MockLogger() as unknown as ILogger);
|
||||
container.bind(ToolInvocationRegistry).toConstantValue(mockToolInvocationRegistry);
|
||||
|
||||
// Bind deserializer registries
|
||||
const contentRegistry = new ChatContentDeserializerRegistryImpl();
|
||||
new DefaultChatContentDeserializerContribution().registerDeserializers(contentRegistry);
|
||||
container.bind(ChatContentDeserializerRegistry).toConstantValue(contentRegistry);
|
||||
container.bind(ChangeSetElementDeserializerRegistry).toConstantValue(new ChangeSetElementDeserializerRegistryImpl());
|
||||
container.bind(ChatServiceImpl).toSelf().inSingletonScope();
|
||||
|
||||
chatService = container.get(ChatServiceImpl);
|
||||
});
|
||||
|
||||
describe('deleteSession', () => {
|
||||
it('should delete session from memory and persistent storage', async () => {
|
||||
// Create a session
|
||||
const session = chatService.createSession(ChatAgentLocation.Panel);
|
||||
expect(chatService.getSessions()).to.have.lengthOf(1);
|
||||
|
||||
// Delete the session (now returns a Promise)
|
||||
await chatService.deleteSession(session.id);
|
||||
|
||||
// Verify it's removed from memory
|
||||
expect(chatService.getSessions()).to.have.lengthOf(0);
|
||||
|
||||
// Verify it's deleted from persistent storage
|
||||
expect(sessionStore.deletedSessions).to.include(session.id);
|
||||
});
|
||||
|
||||
it('should emit SessionDeletedEvent when session is deleted', done => {
|
||||
// Create a session
|
||||
const session = chatService.createSession(ChatAgentLocation.Panel);
|
||||
|
||||
// Listen for deletion event
|
||||
chatService.onSessionEvent(event => {
|
||||
if (event.type === 'deleted') {
|
||||
expect(event.sessionId).to.equal(session.id);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
// Delete the session
|
||||
chatService.deleteSession(session.id);
|
||||
});
|
||||
|
||||
it('should handle deletion when session store is not available', async () => {
|
||||
// Create a new service without session store
|
||||
const containerWithoutStore = new Container();
|
||||
containerWithoutStore.bind(ChatAgentService).toConstantValue(new MockChatAgentService() as unknown as ChatAgentService);
|
||||
containerWithoutStore.bind(ChatRequestParser).toConstantValue(new MockChatRequestParser() as unknown as ChatRequestParser);
|
||||
containerWithoutStore.bind(AIVariableService).toConstantValue(new MockAIVariableService() as unknown as AIVariableService);
|
||||
containerWithoutStore.bind(ILogger).toConstantValue(new MockLogger() as unknown as ILogger);
|
||||
containerWithoutStore.bind(ToolInvocationRegistry).toConstantValue(mockToolInvocationRegistry);
|
||||
|
||||
// Bind deserializer registries
|
||||
const contentRegistry = new ChatContentDeserializerRegistryImpl();
|
||||
new DefaultChatContentDeserializerContribution().registerDeserializers(contentRegistry);
|
||||
containerWithoutStore.bind(ChatContentDeserializerRegistry).toConstantValue(contentRegistry);
|
||||
containerWithoutStore.bind(ChangeSetElementDeserializerRegistry).toConstantValue(new ChangeSetElementDeserializerRegistryImpl());
|
||||
|
||||
containerWithoutStore.bind(ChatServiceImpl).toSelf().inSingletonScope();
|
||||
|
||||
const serviceWithoutStore = containerWithoutStore.get(ChatServiceImpl);
|
||||
|
||||
// Create and delete a session - should not throw
|
||||
const session = serviceWithoutStore.createSession(ChatAgentLocation.Panel);
|
||||
await serviceWithoutStore.deleteSession(session.id);
|
||||
|
||||
// Verify session is still removed from memory
|
||||
expect(serviceWithoutStore.getSessions()).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('should attempt storage deletion even for non-existent in-memory sessions', async () => {
|
||||
const initialDeletedCount = sessionStore.deletedSessions.length;
|
||||
const nonExistentId = 'non-existent-id';
|
||||
|
||||
// Try to delete non-existent session (could be a persisted-only session)
|
||||
await chatService.deleteSession(nonExistentId);
|
||||
|
||||
// Verify storage deletion was attempted even though session not in memory
|
||||
expect(sessionStore.deletedSessions).to.include(nonExistentId);
|
||||
expect(sessionStore.deletedSessions).to.have.lengthOf(initialDeletedCount + 1);
|
||||
});
|
||||
|
||||
it('should handle deleting active session', () => {
|
||||
// Create two sessions
|
||||
const session1 = chatService.createSession(ChatAgentLocation.Panel);
|
||||
const session2 = chatService.createSession(ChatAgentLocation.Panel);
|
||||
|
||||
// Ensure session2 is active (it should be by default as the latest)
|
||||
expect(chatService.getActiveSession()?.id).to.equal(session2.id);
|
||||
|
||||
// Delete session1 (not active)
|
||||
chatService.deleteSession(session1.id);
|
||||
|
||||
// Verify session2 is still active
|
||||
const activeSession = chatService.getActiveSession();
|
||||
expect(activeSession).to.not.be.undefined;
|
||||
expect(activeSession?.id).to.equal(session2.id);
|
||||
});
|
||||
|
||||
it('should handle storage deletion errors gracefully', async () => {
|
||||
// Create a session store that throws errors
|
||||
const errorStore = new MockChatSessionStore();
|
||||
errorStore.deleteSession = async () => {
|
||||
throw new Error('Storage error');
|
||||
};
|
||||
|
||||
const errorContainer = new Container();
|
||||
errorContainer.bind(ChatSessionStore).toConstantValue(errorStore);
|
||||
errorContainer.bind(ChatAgentService).toConstantValue(new MockChatAgentService() as unknown as ChatAgentService);
|
||||
errorContainer.bind(ChatRequestParser).toConstantValue(new MockChatRequestParser() as unknown as ChatRequestParser);
|
||||
errorContainer.bind(AIVariableService).toConstantValue(new MockAIVariableService() as unknown as AIVariableService);
|
||||
errorContainer.bind(ILogger).toConstantValue(new MockLogger() as unknown as ILogger);
|
||||
errorContainer.bind(ToolInvocationRegistry).toConstantValue(mockToolInvocationRegistry);
|
||||
|
||||
// Bind deserializer registries
|
||||
const contentRegistry = new ChatContentDeserializerRegistryImpl();
|
||||
new DefaultChatContentDeserializerContribution().registerDeserializers(contentRegistry);
|
||||
errorContainer.bind(ChatContentDeserializerRegistry).toConstantValue(contentRegistry);
|
||||
errorContainer.bind(ChangeSetElementDeserializerRegistry).toConstantValue(new ChangeSetElementDeserializerRegistryImpl());
|
||||
|
||||
errorContainer.bind(ChatServiceImpl).toSelf().inSingletonScope();
|
||||
|
||||
const errorService = errorContainer.get(ChatServiceImpl);
|
||||
|
||||
// Create and delete a session - should not throw even with storage error
|
||||
const session = errorService.createSession(ChatAgentLocation.Panel);
|
||||
await errorService.deleteSession(session.id);
|
||||
|
||||
// Verify session is still removed from memory despite storage error
|
||||
expect(errorService.getSessions()).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('should delete persisted-only sessions (not loaded in memory)', async () => {
|
||||
// This simulates deleting a session from the "Show Chats..." dialog
|
||||
// when the session is persisted but not currently loaded into memory
|
||||
const persistedSessionId = 'persisted-session-123';
|
||||
|
||||
// Verify session is not in memory
|
||||
expect(chatService.getSessions().find(s => s.id === persistedSessionId)).to.be.undefined;
|
||||
|
||||
// Delete the persisted-only session
|
||||
await chatService.deleteSession(persistedSessionId);
|
||||
|
||||
// Verify it was still deleted from storage (even though not in memory)
|
||||
expect(sessionStore.deletedSessions).to.include(persistedSessionId);
|
||||
});
|
||||
|
||||
it('should not fire SessionDeletedEvent for persisted-only sessions', async () => {
|
||||
// When deleting a persisted-only session (not in memory),
|
||||
// we shouldn't fire the event since no in-memory state changed
|
||||
const persistedSessionId = 'persisted-session-456';
|
||||
let eventFired = false;
|
||||
|
||||
// Listen for deletion event
|
||||
chatService.onSessionEvent(event => {
|
||||
if (event.type === 'deleted' && event.sessionId === persistedSessionId) {
|
||||
eventFired = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Delete the persisted-only session
|
||||
await chatService.deleteSession(persistedSessionId);
|
||||
|
||||
// Event should not have been fired since session wasn't in memory
|
||||
expect(eventFired).to.be.false;
|
||||
// But storage deletion should still have happened
|
||||
expect(sessionStore.deletedSessions).to.include(persistedSessionId);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user