Add comprehensive tests for CLI functionality and CodexLens compatibility

- Introduced tests for stale running fallback in CLI watch functionality to ensure proper handling of saved conversations.
- Added compatibility tests for CodexLens CLI to verify index initialization despite compatibility conflicts.
- Implemented tests for Smart Search MCP usage to validate default settings and path handling.
- Created tests for UV Manager to ensure Python preference handling works as expected.
- Added a detailed guide for CCW/Codex commands and skills, covering core commands, execution modes, and templates.
This commit is contained in:
catlog22
2026-03-08 17:30:39 +08:00
parent 4254eeeaa7
commit 9aa07e8d01
32 changed files with 2954 additions and 154 deletions

View File

@@ -0,0 +1,63 @@
// ========================================
// CLI Stream Store Tests
// ========================================
import { describe, it, expect, beforeEach } from 'vitest';
import { useCliStreamStore, selectActiveExecutionCount } from './cliStreamStore';
describe('cliStreamStore', () => {
beforeEach(() => {
useCliStreamStore.getState().resetState();
});
it('removeExecution clears outputs and execution state together', () => {
const store = useCliStreamStore.getState();
store.upsertExecution('exec-1', {
tool: 'codex',
mode: 'analysis',
status: 'running',
output: [],
startTime: 1_741_400_000_000,
});
store.addOutput('exec-1', {
type: 'stdout',
content: 'hello',
timestamp: 1_741_400_000_100,
});
expect(useCliStreamStore.getState().outputs['exec-1']).toHaveLength(1);
expect(useCliStreamStore.getState().executions['exec-1']).toBeDefined();
store.removeExecution('exec-1');
expect(useCliStreamStore.getState().outputs['exec-1']).toBeUndefined();
expect(useCliStreamStore.getState().executions['exec-1']).toBeUndefined();
});
it('resetState clears execution badge state for workspace switches', () => {
const store = useCliStreamStore.getState();
store.upsertExecution('exec-running', {
tool: 'codex',
mode: 'analysis',
status: 'running',
output: [],
startTime: 1_741_401_000_000,
});
store.setCurrentExecution('exec-running');
store.markExecutionClosedByUser('exec-running');
expect(selectActiveExecutionCount(useCliStreamStore.getState() as any)).toBe(1);
expect(useCliStreamStore.getState().currentExecutionId).toBe('exec-running');
store.resetState();
const nextState = useCliStreamStore.getState();
expect(selectActiveExecutionCount(nextState as any)).toBe(0);
expect(nextState.currentExecutionId).toBeNull();
expect(Object.keys(nextState.executions)).toEqual([]);
expect(Object.keys(nextState.outputs)).toEqual([]);
expect(nextState.userClosedExecutions.size).toBe(0);
});
});

View File

@@ -93,6 +93,7 @@ interface CliStreamState extends BlockCacheState {
isExecutionClosedByUser: (executionId: string) => boolean;
cleanupUserClosedExecutions: (serverIds: Set<string>) => void;
setCurrentExecution: (executionId: string | null) => void;
resetState: () => void;
// Block cache methods
getBlocks: (executionId: string) => LogBlockData[];
@@ -462,15 +463,18 @@ export const useCliStreamStore = create<CliStreamState>()(
removeExecution: (executionId: string) => {
set((state) => {
const newOutputs = { ...state.outputs };
const newExecutions = { ...state.executions };
const newBlocks = { ...state.blocks };
const newLastUpdate = { ...state.lastUpdate };
const newDeduplicationWindows = { ...state.deduplicationWindows };
delete newOutputs[executionId];
delete newExecutions[executionId];
delete newBlocks[executionId];
delete newLastUpdate[executionId];
delete newDeduplicationWindows[executionId];
return {
outputs: newOutputs,
executions: newExecutions,
blocks: newBlocks,
lastUpdate: newLastUpdate,
@@ -513,6 +517,18 @@ export const useCliStreamStore = create<CliStreamState>()(
set({ currentExecutionId: executionId }, false, 'cliStream/setCurrentExecution');
},
resetState: () => {
set({
outputs: {},
executions: {},
currentExecutionId: null,
userClosedExecutions: new Set<string>(),
deduplicationWindows: {},
blocks: {},
lastUpdate: {},
}, false, 'cliStream/resetState');
},
// Block cache methods
getBlocks: (executionId: string) => {
const state = get();

View File

@@ -0,0 +1,46 @@
// ========================================
// Execution Monitor Store Tests
// ========================================
import { beforeEach, describe, expect, it } from 'vitest';
import {
useExecutionMonitorStore,
selectActiveExecutionCount,
type ExecutionWSMessage,
} from './executionMonitorStore';
describe('executionMonitorStore', () => {
beforeEach(() => {
useExecutionMonitorStore.getState().resetState();
});
it('resetState clears workspace-scoped execution monitor state', () => {
const store = useExecutionMonitorStore.getState();
const startMessage: ExecutionWSMessage = {
type: 'EXECUTION_STARTED',
payload: {
executionId: 'exec-running',
flowId: 'flow-1',
sessionKey: 'session-1',
stepName: 'Workspace Flow',
totalSteps: 3,
timestamp: '2026-03-08T12:00:00.000Z',
},
};
store.handleExecutionMessage(startMessage);
const activeState = useExecutionMonitorStore.getState();
expect(selectActiveExecutionCount(activeState as any)).toBe(1);
expect(activeState.currentExecutionId).toBe('exec-running');
expect(activeState.isPanelOpen).toBe(true);
store.resetState();
const nextState = useExecutionMonitorStore.getState();
expect(selectActiveExecutionCount(nextState as any)).toBe(0);
expect(nextState.activeExecutions).toEqual({});
expect(nextState.currentExecutionId).toBeNull();
expect(nextState.isPanelOpen).toBe(false);
});
});

View File

@@ -81,6 +81,7 @@ interface ExecutionMonitorActions {
setPanelOpen: (open: boolean) => void;
clearExecution: (executionId: string) => void;
clearAllExecutions: () => void;
resetState: () => void;
}
type ExecutionMonitorStore = ExecutionMonitorState & ExecutionMonitorActions;
@@ -318,6 +319,10 @@ export const useExecutionMonitorStore = create<ExecutionMonitorStore>()(
clearAllExecutions: () => {
set({ activeExecutions: {}, currentExecutionId: null }, false, 'clearAllExecutions');
},
resetState: () => {
set({ ...initialState }, false, 'resetState');
},
}),
{ name: 'ExecutionMonitorStore' }
)

View File

@@ -0,0 +1,35 @@
// ========================================
// Terminal Panel Store Tests
// ========================================
import { beforeEach, describe, expect, it } from 'vitest';
import { useTerminalPanelStore, selectTerminalCount } from './terminalPanelStore';
describe('terminalPanelStore', () => {
beforeEach(() => {
useTerminalPanelStore.getState().resetState();
});
it('resetState clears workspace-scoped terminal tabs and selection', () => {
const store = useTerminalPanelStore.getState();
store.openTerminal('session-a');
store.addTerminal('session-b');
store.setPanelView('queue');
const activeState = useTerminalPanelStore.getState();
expect(selectTerminalCount(activeState as any)).toBe(2);
expect(activeState.activeTerminalId).toBe('session-a');
expect(activeState.panelView).toBe('queue');
expect(activeState.isPanelOpen).toBe(true);
store.resetState();
const nextState = useTerminalPanelStore.getState();
expect(selectTerminalCount(nextState as any)).toBe(0);
expect(nextState.terminalOrder).toEqual([]);
expect(nextState.activeTerminalId).toBeNull();
expect(nextState.panelView).toBe('terminal');
expect(nextState.isPanelOpen).toBe(false);
});
});

View File

@@ -38,6 +38,8 @@ export interface TerminalPanelActions {
addTerminal: (sessionKey: string) => void;
/** Remove a terminal from the order list and adjust active if needed */
removeTerminal: (sessionKey: string) => void;
/** Reset workspace-scoped terminal panel UI state */
resetState: () => void;
}
export type TerminalPanelStore = TerminalPanelState & TerminalPanelActions;
@@ -153,6 +155,10 @@ export const useTerminalPanelStore = create<TerminalPanelStore>()(
'removeTerminal'
);
},
resetState: () => {
set({ ...initialState }, false, 'resetState');
},
}),
{ name: 'TerminalPanelStore' }
)

View File

@@ -112,8 +112,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
},
sessionDataStore,
},
false,
'setSessions'
false
);
},
@@ -131,8 +130,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
[key]: session,
},
}),
false,
'addSession'
false
);
},
@@ -140,7 +138,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
const key = sessionKey(sessionId);
set(
(state) => {
(state: WorkflowState) => {
const session = state.sessionDataStore[key];
if (!session) return state;
@@ -163,8 +161,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
},
};
},
false,
'updateSession'
false
);
},
@@ -172,7 +169,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
const key = sessionKey(sessionId);
set(
(state) => {
(state: WorkflowState) => {
const { [key]: removed, ...remainingStore } = state.sessionDataStore;
return {
@@ -187,8 +184,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
},
};
},
false,
'removeSession'
false
);
},
@@ -196,7 +192,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
const key = sessionKey(sessionId);
set(
(state) => {
(state: WorkflowState) => {
const session = state.sessionDataStore[key];
if (!session || session.location === 'archived') return state;
@@ -220,8 +216,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
},
};
},
false,
'archiveSession'
false
);
},
@@ -231,7 +226,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
const key = sessionKey(sessionId);
set(
(state) => {
(state: WorkflowState) => {
const session = state.sessionDataStore[key];
if (!session) return state;
@@ -252,8 +247,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
},
};
},
false,
'addTask'
false
);
},
@@ -261,7 +255,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
const key = sessionKey(sessionId);
set(
(state) => {
(state: WorkflowState) => {
const session = state.sessionDataStore[key];
if (!session?.tasks) return state;
@@ -284,8 +278,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
},
};
},
false,
'updateTask'
false
);
},
@@ -293,7 +286,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
const key = sessionKey(sessionId);
set(
(state) => {
(state: WorkflowState) => {
const session = state.sessionDataStore[key];
if (!session?.tasks) return state;
@@ -310,8 +303,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
},
};
},
false,
'removeTask'
false
);
},
@@ -325,8 +317,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
[key]: session,
},
}),
false,
'setLiteTaskSession'
false
);
},
@@ -336,8 +327,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
const { [key]: removed, ...remaining } = state.liteTaskDataStore;
return { liteTaskDataStore: remaining };
},
false,
'removeLiteTaskSession'
false
);
},
@@ -351,8 +341,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
[key]: data,
},
}),
false,
'setTaskJson'
false
);
},
@@ -362,38 +351,36 @@ export const useWorkflowStore = create<WorkflowStore>()(
const { [key]: removed, ...remaining } = state.taskJsonStore;
return { taskJsonStore: remaining };
},
false,
'removeTaskJson'
false
);
},
// ========== Active Session ==========
setActiveSessionId: (sessionId: string | null) => {
set({ activeSessionId: sessionId }, false, 'setActiveSessionId');
set({ activeSessionId: sessionId }, false);
},
// ========== Project Path ==========
setProjectPath: (path: string) => {
set({ projectPath: path }, false, 'setProjectPath');
set({ projectPath: path }, false);
},
addRecentPath: (path: string) => {
set(
(state) => {
(state: WorkflowState) => {
// Remove if exists, add to front
const filtered = state.recentPaths.filter((p) => p !== path);
const updated = [path, ...filtered].slice(0, 10); // Keep max 10
return { recentPaths: updated };
},
false,
'addRecentPath'
false
);
},
setServerPlatform: (platform: 'win32' | 'darwin' | 'linux') => {
set({ serverPlatform: platform }, false, 'setServerPlatform');
set({ serverPlatform: platform }, false);
},
// ========== Workspace Actions ==========
@@ -418,8 +405,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
},
sessionDataStore,
},
false,
'switchWorkspace'
false
);
// Persist projectPath to localStorage manually
@@ -434,16 +420,16 @@ export const useWorkflowStore = create<WorkflowStore>()(
removeRecentPath: async (path: string) => {
const updatedPaths = await apiRemoveRecentPath(path);
set({ recentPaths: updatedPaths }, false, 'removeRecentPath');
set({ recentPaths: updatedPaths }, false);
},
refreshRecentPaths: async () => {
const paths = await fetchRecentPaths();
set({ recentPaths: paths }, false, 'refreshRecentPaths');
set({ recentPaths: paths }, false);
},
registerQueryInvalidator: (callback: () => void) => {
set({ _invalidateQueriesCallback: callback }, false, 'registerQueryInvalidator');
set({ _invalidateQueriesCallback: callback }, false);
},
// ========== Filters and Sorting ==========
@@ -453,8 +439,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
(state) => ({
filters: { ...state.filters, ...filters },
}),
false,
'setFilters'
false
);
},
@@ -463,13 +448,12 @@ export const useWorkflowStore = create<WorkflowStore>()(
(state) => ({
sorting: { ...state.sorting, ...sorting },
}),
false,
'setSorting'
false
);
},
resetFilters: () => {
set({ filters: defaultFilters, sorting: defaultSorting }, false, 'resetFilters');
set({ filters: defaultFilters, sorting: defaultSorting }, false);
},
// ========== Computed Selectors ==========