agent-ecosystem/test/renderer/store/sessionSlice.test.ts
iliya 0c2f70b2b2 feat: implement in-app project editor with CodeMirror integration
- Added architectural plan and iteration plan for the in-app project editor.
- Introduced new components for the editor, including CodeEditorOverlay, FileTreePanel, and EditorTabsPanel.
- Established state management using Zustand for editor state persistence.
- Implemented IPC channels for file operations and editor functionality.
- Enhanced TeamDetailView with a button to open the editor overlay.
- Conducted reuse analysis for existing components to optimize codebase integration.
2026-02-27 22:36:06 +02:00

316 lines
9.3 KiB
TypeScript

/**
* Session slice unit tests.
* Tests session state management including fetching, pagination, and selection.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { installMockElectronAPI, type MockElectronAPI } from '../../mocks/electronAPI';
import { createTestStore, type TestStore } from './storeTestUtils';
describe('sessionSlice', () => {
let store: TestStore;
let mockAPI: MockElectronAPI;
beforeEach(() => {
mockAPI = installMockElectronAPI();
store = createTestStore();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('fetchSessionsInitial', () => {
it('should fetch first page of sessions', async () => {
const mockSessions = [
{ id: 'session-1', createdAt: '2024-01-15T10:00:00Z' },
{ id: 'session-2', createdAt: '2024-01-14T10:00:00Z' },
];
mockAPI.getSessionsPaginated.mockResolvedValue({
sessions: mockSessions as never[],
nextCursor: 'cursor-1',
hasMore: true,
totalCount: 50,
});
await store.getState().fetchSessionsInitial('project-1');
expect(mockAPI.getSessionsPaginated).toHaveBeenCalledWith('project-1', null, 20, {
includeTotalCount: false,
prefilterAll: false,
metadataLevel: 'deep',
});
expect(store.getState().sessions).toHaveLength(2);
expect(store.getState().sessionsCursor).toBe('cursor-1');
expect(store.getState().sessionsHasMore).toBe(true);
expect(store.getState().sessionsTotalCount).toBe(50);
expect(store.getState().sessionsLoading).toBe(false);
});
it('should set loading state during fetch', async () => {
mockAPI.getSessionsPaginated.mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(
() =>
resolve({
sessions: [],
nextCursor: null,
hasMore: false,
totalCount: 0,
}),
100
);
})
);
const fetchPromise = store.getState().fetchSessionsInitial('project-1');
expect(store.getState().sessionsLoading).toBe(true);
vi.useFakeTimers();
vi.advanceTimersByTime(100);
await fetchPromise;
vi.useRealTimers();
expect(store.getState().sessionsLoading).toBe(false);
});
it('should handle fetch error', async () => {
mockAPI.getSessionsPaginated.mockRejectedValue(new Error('Network error'));
await store.getState().fetchSessionsInitial('project-1');
expect(store.getState().sessionsError).toBe('Network error');
expect(store.getState().sessionsLoading).toBe(false);
});
});
describe('fetchSessionsMore', () => {
it('should append sessions to existing list', async () => {
// Setup initial state
store.setState({
selectedProjectId: 'project-1',
sessions: [{ id: 'session-1' }] as never[],
sessionsCursor: 'cursor-1',
sessionsHasMore: true,
sessionsLoadingMore: false,
});
mockAPI.getSessionsPaginated.mockResolvedValue({
sessions: [{ id: 'session-2' }] as never[],
nextCursor: 'cursor-2',
hasMore: true,
totalCount: 50,
});
await store.getState().fetchSessionsMore();
expect(store.getState().sessions).toHaveLength(2);
expect(store.getState().sessionsCursor).toBe('cursor-2');
});
it('should not fetch if no more pages', async () => {
store.setState({
selectedProjectId: 'project-1',
sessionsHasMore: false,
sessionsCursor: null,
});
await store.getState().fetchSessionsMore();
expect(mockAPI.getSessionsPaginated).not.toHaveBeenCalled();
});
it('should not fetch if already loading', async () => {
store.setState({
selectedProjectId: 'project-1',
sessionsHasMore: true,
sessionsCursor: 'cursor-1',
sessionsLoadingMore: true,
});
await store.getState().fetchSessionsMore();
expect(mockAPI.getSessionsPaginated).not.toHaveBeenCalled();
});
});
describe('selectSession', () => {
it('should update selected session ID', () => {
store.setState({
selectedProjectId: 'project-1',
});
mockAPI.getSessionDetail.mockResolvedValue({
session: { id: 'session-1' },
chunks: [],
} as never);
store.getState().selectSession('session-1');
expect(store.getState().selectedSessionId).toBe('session-1');
});
it('should clear previous session detail', () => {
store.setState({
selectedProjectId: 'project-1',
sessionDetail: { session: { id: 'old-session' } } as never,
sessionContextStats: new Map() as never,
});
mockAPI.getSessionDetail.mockResolvedValue({
session: { id: 'session-2' },
chunks: [],
} as never);
store.getState().selectSession('session-2');
expect(store.getState().sessionDetail).toBeNull();
expect(store.getState().sessionContextStats).toBeNull();
});
});
describe('clearSelection', () => {
it('should clear all selection state', () => {
store.setState({
selectedProjectId: 'project-1',
selectedSessionId: 'session-1',
sessions: [{ id: 'session-1' }] as never[],
sessionDetail: { session: { id: 'session-1' } } as never,
});
store.getState().clearSelection();
expect(store.getState().selectedProjectId).toBeNull();
expect(store.getState().selectedSessionId).toBeNull();
expect(store.getState().sessions).toHaveLength(0);
expect(store.getState().sessionDetail).toBeNull();
});
});
describe('refreshSessionsInPlace', () => {
it('should refresh sessions without loading state', async () => {
store.setState({
selectedProjectId: 'project-1',
sessions: [{ id: 'session-1' }] as never[],
sessionsLoading: false,
});
mockAPI.getSessionsPaginated.mockResolvedValue({
sessions: [{ id: 'session-1' }, { id: 'session-2' }] as never[],
nextCursor: null,
hasMore: false,
totalCount: 2,
});
await store.getState().refreshSessionsInPlace('project-1');
expect(store.getState().sessions).toHaveLength(2);
expect(mockAPI.getSessionsPaginated).toHaveBeenCalledWith('project-1', null, 20, {
includeTotalCount: false,
prefilterAll: false,
metadataLevel: 'deep',
});
// Should not have set loading state
expect(store.getState().sessionsLoading).toBe(false);
});
it('should skip refresh if different project selected', async () => {
store.setState({
selectedProjectId: 'project-1',
});
await store.getState().refreshSessionsInPlace('project-2');
expect(mockAPI.getSessionsPaginated).not.toHaveBeenCalled();
});
it('should ignore stale refresh responses and keep latest result', async () => {
store.setState({
selectedProjectId: 'project-1',
sessions: [{ id: 'seed' }] as never[],
});
let resolveFirst: ((value: unknown) => void) | undefined;
let resolveSecond: ((value: unknown) => void) | undefined;
mockAPI.getSessionsPaginated
.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveFirst = resolve as (value: unknown) => void;
})
)
.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveSecond = resolve as (value: unknown) => void;
})
);
const first = store.getState().refreshSessionsInPlace('project-1');
const second = store.getState().refreshSessionsInPlace('project-1');
resolveSecond?.({
sessions: [{ id: 'newest' }] as never[],
nextCursor: null,
hasMore: false,
totalCount: 1,
});
resolveFirst?.({
sessions: [{ id: 'stale' }] as never[],
nextCursor: null,
hasMore: false,
totalCount: 1,
});
await Promise.all([first, second]);
expect(store.getState().sessions[0]?.id).toBe('newest');
});
});
describe('fetchSessionDetail', () => {
it('should ignore stale responses and keep the latest session detail', async () => {
store.setState({
selectedSessionId: 'session-2',
});
let resolveFirst: ((value: unknown) => void) | undefined;
let resolveSecond: ((value: unknown) => void) | undefined;
mockAPI.getSessionDetail
.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveFirst = resolve as (value: unknown) => void;
})
)
.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveSecond = resolve as (value: unknown) => void;
})
);
const first = store.getState().fetchSessionDetail('project-1', 'session-1');
const second = store.getState().fetchSessionDetail('project-1', 'session-2');
resolveSecond?.({
session: { id: 'session-2' },
chunks: [],
processes: [],
});
resolveFirst?.({
session: { id: 'session-1' },
chunks: [],
processes: [],
});
await Promise.all([first, second]);
expect(store.getState().sessionDetail?.session.id).toBe('session-2');
});
});
});