Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/__tests__/renderer/components/SessionList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import { SessionList } from '../../../renderer/components/SessionList';
import type { Session, Group, Theme } from '../../../renderer/types';
import { useUIStore } from '../../../renderer/stores/uiStore';
import { useSessionStore } from '../../../renderer/stores/sessionStore';
import { useSettingsStore, DEFAULT_AUTO_RUN_STATS } from '../../../renderer/stores/settingsStore';
import { useSettingsStore } from '../../../renderer/stores/settingsStore';

// Deep-cloned default autoRunStats captured from a fresh store (no longer exported).
const DEFAULT_AUTO_RUN_STATS = JSON.parse(JSON.stringify(useSettingsStore.getState().autoRunStats));
import { useBatchStore } from '../../../renderer/stores/batchStore';
import { useModalStore } from '../../../renderer/stores/modalStore';
import type { BatchRunState } from '../../../renderer/types';
Expand Down
24 changes: 15 additions & 9 deletions src/__tests__/renderer/fonts-and-sizing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,21 @@ import { renderHook, act, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { useSettings } from '../../renderer/hooks';
import React from 'react';
import {
useSettingsStore,
DEFAULT_CONTEXT_MANAGEMENT_SETTINGS,
DEFAULT_AUTO_RUN_STATS,
DEFAULT_USAGE_STATS,
DEFAULT_KEYBOARD_MASTERY_STATS,
DEFAULT_ONBOARDING_STATS,
DEFAULT_AI_COMMANDS,
} from '../../renderer/stores/settingsStore';
import { useSettingsStore } from '../../renderer/stores/settingsStore';

// Deep-cloned defaults captured from a fresh store so mutations in tests can't
// leak back into the reference. The store no longer exports these defaults.
const _INITIAL_STATE = useSettingsStore.getState();
const DEFAULT_CONTEXT_MANAGEMENT_SETTINGS = JSON.parse(
JSON.stringify(_INITIAL_STATE.contextManagementSettings)
);
const DEFAULT_AUTO_RUN_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.autoRunStats));
const DEFAULT_USAGE_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.usageStats));
const DEFAULT_KEYBOARD_MASTERY_STATS = JSON.parse(
JSON.stringify(_INITIAL_STATE.keyboardMasteryStats)
);
const DEFAULT_ONBOARDING_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.onboardingStats));
const DEFAULT_AI_COMMANDS = JSON.parse(JSON.stringify(_INITIAL_STATE.customAICommands));
import { DEFAULT_SHORTCUTS, TAB_SHORTCUTS } from '../../renderer/constants/shortcuts';
import { DEFAULT_CUSTOM_THEME_COLORS } from '../../renderer/constants/themes';

Expand Down
24 changes: 15 additions & 9 deletions src/__tests__/renderer/hooks/useSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ import { renderHook, act, waitFor } from '@testing-library/react';
import { useSettings } from '../../../renderer/hooks';
import type { AutoRunStats, OnboardingStats, CustomAICommand } from '../../../renderer/types';
import { DEFAULT_SHORTCUTS } from '../../../renderer/constants/shortcuts';
import {
useSettingsStore,
DEFAULT_CONTEXT_MANAGEMENT_SETTINGS,
DEFAULT_AUTO_RUN_STATS,
DEFAULT_USAGE_STATS,
DEFAULT_KEYBOARD_MASTERY_STATS,
DEFAULT_ONBOARDING_STATS,
DEFAULT_AI_COMMANDS,
} from '../../../renderer/stores/settingsStore';
import { useSettingsStore } from '../../../renderer/stores/settingsStore';

// Deep-cloned defaults captured from a fresh store so mutations in tests can't
// leak back into the reference. The store no longer exports these defaults.
const _INITIAL_STATE = useSettingsStore.getState();
const DEFAULT_CONTEXT_MANAGEMENT_SETTINGS = JSON.parse(
JSON.stringify(_INITIAL_STATE.contextManagementSettings)
);
const DEFAULT_AUTO_RUN_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.autoRunStats));
const DEFAULT_USAGE_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.usageStats));
const DEFAULT_KEYBOARD_MASTERY_STATS = JSON.parse(
JSON.stringify(_INITIAL_STATE.keyboardMasteryStats)
);
const DEFAULT_ONBOARDING_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.onboardingStats));
const DEFAULT_AI_COMMANDS = JSON.parse(JSON.stringify(_INITIAL_STATE.customAICommands));
import { TAB_SHORTCUTS } from '../../../renderer/constants/shortcuts';
import { DEFAULT_CUSTOM_THEME_COLORS } from '../../../renderer/constants/themes';

Expand Down
71 changes: 30 additions & 41 deletions src/__tests__/renderer/stores/agentStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import {
useAgentStore,
selectAvailableAgents,
selectAgentsDetected,
getAgentState,
getAgentActions,
} from '../../../renderer/stores/agentStore';
import { useAgentStore } from '../../../renderer/stores/agentStore';
import type { ProcessQueuedItemDeps } from '../../../renderer/stores/agentStore';
import { useSessionStore } from '../../../renderer/stores/sessionStore';
import type { Session, AgentConfig, QueuedItem } from '../../../renderer/types';
Expand Down Expand Up @@ -919,85 +913,80 @@ describe('agentStore', () => {
});
});

describe('selectors', () => {
it('selectAvailableAgents returns the agents list', () => {
describe('store state access', () => {
it('availableAgents reflects setState updates', () => {
const agents = [createMockAgentConfig({ id: 'claude-code' })];
useAgentStore.setState({ availableAgents: agents });

expect(selectAvailableAgents(useAgentStore.getState())).toEqual(agents);
expect(useAgentStore.getState().availableAgents).toEqual(agents);
});

it('selectAgentsDetected returns detection status', () => {
expect(selectAgentsDetected(useAgentStore.getState())).toBe(false);
it('agentsDetected reflects setState updates', () => {
expect(useAgentStore.getState().agentsDetected).toBe(false);

useAgentStore.setState({ agentsDetected: true });

expect(selectAgentsDetected(useAgentStore.getState())).toBe(true);
expect(useAgentStore.getState().agentsDetected).toBe(true);
});
});

describe('non-React access', () => {
it('getAgentState returns current snapshot', () => {
it('getState returns current snapshot', () => {
const agents = [createMockAgentConfig()];
useAgentStore.setState({ availableAgents: agents, agentsDetected: true });

const state = getAgentState();
const state = useAgentStore.getState();
expect(state.availableAgents).toEqual(agents);
expect(state.agentsDetected).toBe(true);
});

it('getAgentState reflects latest mutations', () => {
expect(getAgentState().agentsDetected).toBe(false);
it('getState reflects latest mutations', () => {
expect(useAgentStore.getState().agentsDetected).toBe(false);

useAgentStore.setState({ agentsDetected: true });

expect(getAgentState().agentsDetected).toBe(true);
expect(useAgentStore.getState().agentsDetected).toBe(true);
});

it('getAgentActions returns all 10 action functions', () => {
const actions = getAgentActions();

expect(typeof actions.refreshAgents).toBe('function');
expect(typeof actions.getAgentConfig).toBe('function');
expect(typeof actions.processQueuedItem).toBe('function');
expect(typeof actions.clearAgentError).toBe('function');
expect(typeof actions.startNewSessionAfterError).toBe('function');
expect(typeof actions.retryAfterError).toBe('function');
expect(typeof actions.restartAgentAfterError).toBe('function');
expect(typeof actions.authenticateAfterError).toBe('function');
expect(typeof actions.killAgent).toBe('function');
expect(typeof actions.interruptAgent).toBe('function');
it('getState exposes all 10 action functions', () => {
const state = useAgentStore.getState();

// Verify exactly 10 actions (no extras, no missing)
expect(Object.keys(actions)).toHaveLength(10);
expect(typeof state.refreshAgents).toBe('function');
expect(typeof state.getAgentConfig).toBe('function');
expect(typeof state.processQueuedItem).toBe('function');
expect(typeof state.clearAgentError).toBe('function');
expect(typeof state.startNewSessionAfterError).toBe('function');
expect(typeof state.retryAfterError).toBe('function');
expect(typeof state.restartAgentAfterError).toBe('function');
expect(typeof state.authenticateAfterError).toBe('function');
expect(typeof state.killAgent).toBe('function');
expect(typeof state.interruptAgent).toBe('function');
});

it('getAgentActions clearAgentError works end-to-end', () => {
it('clearAgentError works end-to-end', () => {
const session = createMockSession({
id: 'session-1',
state: 'error',
agentError: { type: 'agent_crashed', message: 'crash' } as any,
});
useSessionStore.getState().setSessions([session]);

const { clearAgentError } = getAgentActions();
clearAgentError('session-1');
useAgentStore.getState().clearAgentError('session-1');

expect(useSessionStore.getState().sessions[0].state).toBe('idle');
expect(mockClearError).toHaveBeenCalledWith('session-1');
});

it('getAgentActions killAgent works end-to-end', async () => {
const { killAgent } = getAgentActions();
await killAgent('session-1', 'terminal');
it('killAgent works end-to-end', async () => {
await useAgentStore.getState().killAgent('session-1', 'terminal');

expect(mockKill).toHaveBeenCalledWith('session-1-terminal');
});
});

describe('React hook integration', () => {
it('useAgentStore with selector re-renders on agent detection', async () => {
const { result } = renderHook(() => useAgentStore(selectAgentsDetected));
const { result } = renderHook(() => useAgentStore((s) => s.agentsDetected));

expect(result.current).toBe(false);

Expand All @@ -1012,7 +1001,7 @@ describe('agentStore', () => {
});

it('useAgentStore with availableAgents selector updates on refresh', async () => {
const { result } = renderHook(() => useAgentStore(selectAvailableAgents));
const { result } = renderHook(() => useAgentStore((s) => s.availableAgents));

expect(result.current).toEqual([]);

Expand Down
47 changes: 4 additions & 43 deletions src/__tests__/renderer/stores/batchStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@ import { describe, it, expect, beforeEach } from 'vitest';
import {
useBatchStore,
getBatchState,
getBatchActions,
selectHasAnyActiveBatch,
selectActiveBatchSessionIds,
selectStoppingBatchSessionIds,
selectBatchRunState,
} from '../../../renderer/stores/batchStore';
import type { TaskCountEntry } from '../../../renderer/stores/batchStore';
import type { AutoRunTreeNode } from '../../../renderer/hooks/batch/useAutoRunHandlers';
Expand Down Expand Up @@ -565,40 +562,6 @@ describe('batchStore', () => {
expect(ids).toContain('sess-3');
});
});

describe('selectStoppingBatchSessionIds', () => {
it('returns empty array when no stopping batches', () => {
expect(selectStoppingBatchSessionIds(useBatchStore.getState())).toEqual([]);
});

it('returns only stopping session IDs', () => {
useBatchStore.getState().setBatchRunStates({
'sess-1': { ...DEFAULT_BATCH_STATE, isRunning: true, isStopping: true },
'sess-2': { ...DEFAULT_BATCH_STATE, isRunning: true, isStopping: false },
'sess-3': { ...DEFAULT_BATCH_STATE, isRunning: false, isStopping: true },
});
const ids = selectStoppingBatchSessionIds(useBatchStore.getState());
// Only sess-1: isRunning=true AND isStopping=true
expect(ids).toEqual(['sess-1']);
});
});

describe('selectBatchRunState', () => {
it('returns undefined for non-existent session', () => {
expect(selectBatchRunState(useBatchStore.getState(), 'nope')).toBeUndefined();
});

it('returns batch state for existing session', () => {
useBatchStore.getState().dispatchBatch({
type: 'START_BATCH',
sessionId: 'sess-1',
payload: createStartBatchPayload({ documents: ['x.md'] }),
});
const state = selectBatchRunState(useBatchStore.getState(), 'sess-1');
expect(state).toBeDefined();
expect(state!.documents).toEqual(['x.md']);
});
});
});

// ==========================================================================
Expand All @@ -612,15 +575,13 @@ describe('batchStore', () => {
expect(state.documentList).toEqual(['test.md']);
});

it('getBatchActions returns working action references', () => {
const actions = getBatchActions();
actions.setDocumentList(['via-actions.md']);
it('useBatchStore.getState exposes working action references', () => {
useBatchStore.getState().setDocumentList(['via-actions.md']);
expect(useBatchStore.getState().documentList).toEqual(['via-actions.md']);
});

it('getBatchActions.dispatchBatch works', () => {
const actions = getBatchActions();
actions.dispatchBatch({
it('useBatchStore.getState().dispatchBatch works', () => {
useBatchStore.getState().dispatchBatch({
type: 'START_BATCH',
sessionId: 'sess-1',
payload: createStartBatchPayload(),
Expand Down
38 changes: 17 additions & 21 deletions src/__tests__/renderer/stores/fileExplorerStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
*/

import { describe, it, expect, beforeEach } from 'vitest';
import {
useFileExplorerStore,
getFileExplorerState,
getFileExplorerActions,
} from '../../../renderer/stores/fileExplorerStore';
import { useFileExplorerStore } from '../../../renderer/stores/fileExplorerStore';
import type { FlatTreeNode } from '../../../renderer/utils/fileExplorer';

// ============================================================================
Expand Down Expand Up @@ -244,32 +240,32 @@ describe('fileExplorerStore', () => {
});

describe('non-React access', () => {
it('getFileExplorerState returns current state', () => {
it('useFileExplorerStore.getState() returns current state', () => {
useFileExplorerStore.getState().setFileTreeFilter('search');
const state = getFileExplorerState();
const state = useFileExplorerStore.getState();
expect(state.fileTreeFilter).toBe('search');
});

it('getFileExplorerActions returns action functions', () => {
const actions = getFileExplorerActions();
expect(typeof actions.setSelectedFileIndex).toBe('function');
expect(typeof actions.setFileTreeFilter).toBe('function');
expect(typeof actions.setFileTreeFilterOpen).toBe('function');
expect(typeof actions.setFilePreviewLoading).toBe('function');
expect(typeof actions.setFlatFileList).toBe('function');
expect(typeof actions.focusFileInGraph).toBe('function');
expect(typeof actions.openLastDocumentGraph).toBe('function');
expect(typeof actions.closeGraphView).toBe('function');
expect(typeof actions.setIsGraphViewOpen).toBe('function');
it('useFileExplorerStore.getState() exposes action functions', () => {
const state = useFileExplorerStore.getState();
expect(typeof state.setSelectedFileIndex).toBe('function');
expect(typeof state.setFileTreeFilter).toBe('function');
expect(typeof state.setFileTreeFilterOpen).toBe('function');
expect(typeof state.setFilePreviewLoading).toBe('function');
expect(typeof state.setFlatFileList).toBe('function');
expect(typeof state.focusFileInGraph).toBe('function');
expect(typeof state.openLastDocumentGraph).toBe('function');
expect(typeof state.closeGraphView).toBe('function');
expect(typeof state.setIsGraphViewOpen).toBe('function');
});

it('actions from getFileExplorerActions update state', () => {
const actions = getFileExplorerActions();
it('actions from useFileExplorerStore.getState() update state', () => {
const actions = useFileExplorerStore.getState();
actions.setSelectedFileIndex(10);
actions.setFileTreeFilter('test');
actions.focusFileInGraph('via-actions.ts');

const state = getFileExplorerState();
const state = useFileExplorerStore.getState();
expect(state.selectedFileIndex).toBe(10);
expect(state.fileTreeFilter).toBe('test');
expect(state.graphFocusFilePath).toBe('via-actions.ts');
Expand Down
Loading