Files
Codeman/test/state-store.test.ts
T
arkonandClaude Opus 4.5 0d1ca37b6e refactor: rename inner-loop-tracker to ralph-tracker with API improvements
- Rename inner-loop-tracker.ts → ralph-tracker.ts throughout codebase
- Add ralph-config.ts for parsing .claude/ralph-loop.local.md config
- Standardize API error responses using createErrorResponse()
- Add input validation for auto-compact/auto-clear thresholds
- Update UI labels to "Ralph / Todo Tracker" consistently
- Add 46 integration tests for Ralph tracking functionality
- Update test badge to 438 total tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 22:36:45 +01:00

380 lines
11 KiB
TypeScript

/**
* @fileoverview Tests for StateStore
*
* Tests the persistent JSON state storage including
* debounced saves, state CRUD operations, and Ralph state management.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { existsSync, unlinkSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
// Import types before mocking
import type { AppState, SessionState, TaskState, RalphSessionState } from '../src/types.js';
// We need to import without mocking to test the actual implementation
import { StateStore, getStore } from '../src/state-store.js';
describe('StateStore', () => {
const testDir = join(tmpdir(), 'claudeman-test-' + Date.now());
const testFilePath = join(testDir, 'state.json');
const testInnerPath = join(testDir, 'state-inner.json');
beforeEach(() => {
// Create test directory
if (!existsSync(testDir)) {
mkdirSync(testDir, { recursive: true });
}
// Clean up any existing test files
try {
if (existsSync(testFilePath)) unlinkSync(testFilePath);
if (existsSync(testInnerPath)) unlinkSync(testInnerPath);
} catch (e) {
// Ignore cleanup errors
}
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
// Clean up test directory
try {
rmSync(testDir, { recursive: true, force: true });
} catch (e) {
// Ignore cleanup errors
}
});
describe('constructor', () => {
it('should create initial state when no file exists', () => {
const store = new StateStore(testFilePath);
const state = store.getState();
expect(state).toBeDefined();
expect(state.sessions).toBeDefined();
expect(state.tasks).toBeDefined();
expect(state.config).toBeDefined();
});
it('should set state file path in config', () => {
const store = new StateStore(testFilePath);
expect(store.getConfig().stateFilePath).toBe(testFilePath);
});
});
describe('save and saveNow', () => {
it('should debounce saves by default', () => {
const store = new StateStore(testFilePath);
store.setSession('test-1', createMockSessionState('test-1'));
store.setSession('test-2', createMockSessionState('test-2'));
// File should not exist yet (debounced)
expect(existsSync(testFilePath)).toBe(false);
// Advance past debounce time
vi.advanceTimersByTime(600);
// Now file should exist
expect(existsSync(testFilePath)).toBe(true);
});
it('should write immediately with saveNow', () => {
const store = new StateStore(testFilePath);
store.setSession('test-1', createMockSessionState('test-1'));
store.saveNow();
expect(existsSync(testFilePath)).toBe(true);
});
it('should flush pending saves', () => {
const store = new StateStore(testFilePath);
store.setSession('test-1', createMockSessionState('test-1'));
store.flush();
expect(existsSync(testFilePath)).toBe(true);
});
});
describe('session operations', () => {
it('should set and get sessions', () => {
const store = new StateStore(testFilePath);
const sessionState = createMockSessionState('session-1');
store.setSession('session-1', sessionState);
expect(store.getSession('session-1')).toEqual(sessionState);
});
it('should return null for non-existent session', () => {
const store = new StateStore(testFilePath);
expect(store.getSession('non-existent')).toBeNull();
});
it('should remove sessions', () => {
const store = new StateStore(testFilePath);
store.setSession('session-1', createMockSessionState('session-1'));
store.removeSession('session-1');
expect(store.getSession('session-1')).toBeNull();
});
it('should get all sessions', () => {
const store = new StateStore(testFilePath);
store.setSession('session-1', createMockSessionState('session-1'));
store.setSession('session-2', createMockSessionState('session-2'));
const sessions = store.getSessions();
expect(Object.keys(sessions)).toHaveLength(2);
expect(sessions['session-1']).toBeDefined();
expect(sessions['session-2']).toBeDefined();
});
});
describe('task operations', () => {
it('should set and get tasks', () => {
const store = new StateStore(testFilePath);
const taskState = createMockTaskState('task-1');
store.setTask('task-1', taskState);
expect(store.getTask('task-1')).toEqual(taskState);
});
it('should return null for non-existent task', () => {
const store = new StateStore(testFilePath);
expect(store.getTask('non-existent')).toBeNull();
});
it('should remove tasks', () => {
const store = new StateStore(testFilePath);
store.setTask('task-1', createMockTaskState('task-1'));
store.removeTask('task-1');
expect(store.getTask('task-1')).toBeNull();
});
it('should get all tasks', () => {
const store = new StateStore(testFilePath);
store.setTask('task-1', createMockTaskState('task-1'));
store.setTask('task-2', createMockTaskState('task-2'));
const tasks = store.getTasks();
expect(Object.keys(tasks)).toHaveLength(2);
});
});
describe('Ralph Loop state', () => {
it('should get and set Ralph Loop state', () => {
const store = new StateStore(testFilePath);
store.setRalphLoopState({ status: 'running', startedAt: 12345 });
const state = store.getRalphLoopState();
expect(state.status).toBe('running');
expect(state.startedAt).toBe(12345);
});
it('should merge partial updates', () => {
const store = new StateStore(testFilePath);
store.setRalphLoopState({ status: 'running' });
store.setRalphLoopState({ tasksCompleted: 5 });
const state = store.getRalphLoopState();
expect(state.status).toBe('running');
expect(state.tasksCompleted).toBe(5);
});
});
describe('config operations', () => {
it('should get and set config', () => {
const store = new StateStore(testFilePath);
store.setConfig({ maxConcurrentSessions: 10 });
expect(store.getConfig().maxConcurrentSessions).toBe(10);
});
it('should merge partial updates', () => {
const store = new StateStore(testFilePath);
const originalPollInterval = store.getConfig().pollIntervalMs;
store.setConfig({ maxConcurrentSessions: 10 });
expect(store.getConfig().maxConcurrentSessions).toBe(10);
expect(store.getConfig().pollIntervalMs).toBe(originalPollInterval);
});
});
describe('reset', () => {
it('should reset state to initial values', () => {
const store = new StateStore(testFilePath);
store.setSession('session-1', createMockSessionState('session-1'));
store.setTask('task-1', createMockTaskState('task-1'));
store.reset();
expect(store.getSession('session-1')).toBeNull();
expect(store.getTask('task-1')).toBeNull();
expect(store.getAllRalphStates().size).toBe(0);
});
it('should preserve state file path in config', () => {
const store = new StateStore(testFilePath);
store.reset();
expect(store.getConfig().stateFilePath).toBe(testFilePath);
});
});
describe('ralph state operations', () => {
it('should get and set ralph state', () => {
const store = new StateStore(testFilePath);
const ralphState = createMockRalphState('session-1');
store.setRalphState('session-1', ralphState);
expect(store.getRalphState('session-1')).toEqual(ralphState);
});
it('should return null for non-existent ralph state', () => {
const store = new StateStore(testFilePath);
expect(store.getRalphState('non-existent')).toBeNull();
});
it('should update ralph state with partial merge', () => {
const store = new StateStore(testFilePath);
store.setRalphState('session-1', createMockRalphState('session-1'));
const updated = store.updateRalphState('session-1', { totalTodos: 10 });
expect(updated.totalTodos).toBe(10);
expect(updated.sessionId).toBe('session-1');
});
it('should create initial state on update if none exists', () => {
const store = new StateStore(testFilePath);
const state = store.updateRalphState('new-session', { totalTodos: 5 });
expect(state.sessionId).toBe('new-session');
expect(state.totalTodos).toBe(5);
});
it('should remove ralph state', () => {
const store = new StateStore(testFilePath);
store.setRalphState('session-1', createMockRalphState('session-1'));
store.removeRalphState('session-1');
expect(store.getRalphState('session-1')).toBeNull();
});
it('should get all ralph states', () => {
const store = new StateStore(testFilePath);
store.setRalphState('session-1', createMockRalphState('session-1'));
store.setRalphState('session-2', createMockRalphState('session-2'));
const allStates = store.getAllRalphStates();
expect(allStates.size).toBe(2);
expect(allStates.has('session-1')).toBe(true);
expect(allStates.has('session-2')).toBe(true);
});
});
describe('flushAll', () => {
it('should flush both main and ralph state', () => {
const store = new StateStore(testFilePath);
store.setSession('session-1', createMockSessionState('session-1'));
store.setRalphState('session-1', createMockRalphState('session-1'));
store.flushAll();
expect(existsSync(testFilePath)).toBe(true);
expect(existsSync(testInnerPath)).toBe(true);
});
});
describe('persistence', () => {
it('should persist and restore state across instances', () => {
// Create and save state
const store1 = new StateStore(testFilePath);
store1.setSession('session-1', createMockSessionState('session-1'));
store1.setTask('task-1', createMockTaskState('task-1'));
store1.setRalphState('session-1', createMockRalphState('session-1'));
store1.flushAll();
// Create new instance and verify state is loaded
const store2 = new StateStore(testFilePath);
expect(store2.getSession('session-1')).toBeDefined();
expect(store2.getTask('task-1')).toBeDefined();
expect(store2.getRalphState('session-1')).toBeDefined();
});
});
});
// Helper functions to create mock state objects
function createMockSessionState(id: string): SessionState {
return {
id,
workingDir: '/tmp/test',
status: 'running',
pid: 12345,
createdAt: Date.now(),
caseName: 'test-case',
mode: 'claude',
};
}
function createMockTaskState(id: string): TaskState {
return {
id,
prompt: 'Test prompt',
workingDir: '/tmp/test',
priority: 0,
dependencies: [],
status: 'pending',
assignedSessionId: null,
createdAt: Date.now(),
startedAt: null,
completedAt: null,
output: '',
error: null,
};
}
function createMockRalphState(sessionId: string): RalphSessionState {
return {
sessionId,
enabled: true,
loopActive: false,
totalTodos: 0,
completedTodos: 0,
completionPhrase: null,
expectedPhrase: null,
todoItems: [],
lastUpdated: Date.now(),
messageId: null,
};
}