mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 06:29:42 +02:00
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>
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* @fileoverview Tests for Ralph Wiggum configuration parser
|
||||
*
|
||||
* Tests parsing of .claude/ralph-loop.local.md and CLAUDE.md
|
||||
* for Ralph loop configuration.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
parseRalphLoopConfigFromContent,
|
||||
extractCompletionPhraseFromContent,
|
||||
} from '../src/ralph-config.js';
|
||||
|
||||
describe('parseRalphLoopConfigFromContent', () => {
|
||||
describe('valid YAML frontmatter', () => {
|
||||
it('should parse complete config', () => {
|
||||
const content = `---
|
||||
enabled: true
|
||||
iteration: 5
|
||||
max-iterations: 50
|
||||
completion-promise: "COMPLETE"
|
||||
---
|
||||
# Original Prompt
|
||||
|
||||
Build a REST API...
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.enabled).toBe(true);
|
||||
expect(config!.iteration).toBe(5);
|
||||
expect(config!.maxIterations).toBe(50);
|
||||
expect(config!.completionPromise).toBe('COMPLETE');
|
||||
});
|
||||
|
||||
it('should parse config without quotes around values', () => {
|
||||
const content = `---
|
||||
enabled: true
|
||||
iteration: 10
|
||||
max-iterations: 100
|
||||
completion-promise: TESTS_PASS
|
||||
---
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.completionPromise).toBe('TESTS_PASS');
|
||||
});
|
||||
|
||||
it('should parse config with hyphenated completion promise', () => {
|
||||
const content = `---
|
||||
enabled: true
|
||||
completion-promise: "TESTS-PASS"
|
||||
---
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.completionPromise).toBe('TESTS-PASS');
|
||||
});
|
||||
|
||||
it('should handle partial config (only enabled)', () => {
|
||||
const content = `---
|
||||
enabled: true
|
||||
---
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.enabled).toBe(true);
|
||||
expect(config!.iteration).toBe(0);
|
||||
expect(config!.maxIterations).toBeNull();
|
||||
expect(config!.completionPromise).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle disabled state', () => {
|
||||
const content = `---
|
||||
enabled: false
|
||||
iteration: 25
|
||||
max-iterations: 50
|
||||
completion-promise: "DONE"
|
||||
---
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.enabled).toBe(false);
|
||||
expect(config!.iteration).toBe(25);
|
||||
});
|
||||
|
||||
it('should convert completion promise to uppercase', () => {
|
||||
const content = `---
|
||||
completion-promise: "lower_case_phrase"
|
||||
---
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.completionPromise).toBe('LOWER_CASE_PHRASE');
|
||||
});
|
||||
|
||||
it('should handle whitespace in values', () => {
|
||||
const content = `---
|
||||
enabled: true
|
||||
completion-promise: "COMPLETE"
|
||||
iteration: 5
|
||||
---
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.completionPromise).toBe('COMPLETE');
|
||||
expect(config!.iteration).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid content', () => {
|
||||
it('should return null for content without frontmatter', () => {
|
||||
const content = `# Just a markdown file
|
||||
|
||||
No YAML frontmatter here.
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for malformed frontmatter (missing closing)', () => {
|
||||
const content = `---
|
||||
enabled: true
|
||||
# No closing ---
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for empty content', () => {
|
||||
const config = parseRalphLoopConfigFromContent('');
|
||||
|
||||
expect(config).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for empty frontmatter', () => {
|
||||
const content = `---
|
||||
---
|
||||
Content here.
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
// Empty frontmatter (nothing between ---) returns null
|
||||
// because there's nothing useful to extract
|
||||
expect(config).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle zero iteration', () => {
|
||||
const content = `---
|
||||
iteration: 0
|
||||
---
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.iteration).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle invalid iteration as zero', () => {
|
||||
const content = `---
|
||||
iteration: not-a-number
|
||||
---
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.iteration).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle invalid max-iterations as null', () => {
|
||||
const content = `---
|
||||
max-iterations: infinite
|
||||
---
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.maxIterations).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle mixed valid and invalid values', () => {
|
||||
const content = `---
|
||||
enabled: true
|
||||
iteration: abc
|
||||
max-iterations: 50
|
||||
completion-promise: "VALID-PHRASE"
|
||||
---
|
||||
`;
|
||||
|
||||
const config = parseRalphLoopConfigFromContent(content);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.enabled).toBe(true);
|
||||
expect(config!.iteration).toBe(0); // Invalid falls back to 0
|
||||
expect(config!.maxIterations).toBe(50);
|
||||
expect(config!.completionPromise).toBe('VALID-PHRASE');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractCompletionPhraseFromContent', () => {
|
||||
describe('standard patterns', () => {
|
||||
it('should extract simple completion phrase', () => {
|
||||
const content = `Output <promise>COMPLETE</promise> when done.`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('COMPLETE');
|
||||
});
|
||||
|
||||
it('should extract phrase with underscores', () => {
|
||||
const content = `<promise>ALL_TASKS_DONE</promise>`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('ALL_TASKS_DONE');
|
||||
});
|
||||
|
||||
it('should extract phrase with hyphens', () => {
|
||||
const content = `Output <promise>TESTS-PASS</promise> when all tests green.`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('TESTS-PASS');
|
||||
});
|
||||
|
||||
it('should extract phrase with numbers', () => {
|
||||
const content = `<promise>TASK_123</promise>`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('TASK_123');
|
||||
});
|
||||
|
||||
it('should extract phrase with mixed characters', () => {
|
||||
const content = `<promise>TASK-123_COMPLETE</promise>`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('TASK-123_COMPLETE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('whitespace handling', () => {
|
||||
it('should handle whitespace inside tags', () => {
|
||||
const content = `<promise> COMPLETE </promise>`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('COMPLETE');
|
||||
});
|
||||
|
||||
it('should handle newlines around phrase', () => {
|
||||
const content = `<promise>
|
||||
DONE
|
||||
</promise>`;
|
||||
|
||||
// The pattern does match across lines since \s* includes newlines
|
||||
// This is acceptable - multi-line promises are unusual but still valid
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('DONE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('case handling', () => {
|
||||
it('should convert lowercase to uppercase', () => {
|
||||
const content = `<promise>complete</promise>`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('COMPLETE');
|
||||
});
|
||||
|
||||
it('should convert mixed case to uppercase', () => {
|
||||
const content = `<promise>TestsPass</promise>`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('TESTSPASS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple occurrences', () => {
|
||||
it('should return first occurrence', () => {
|
||||
const content = `
|
||||
First: <promise>FIRST_PHRASE</promise>
|
||||
Second: <promise>SECOND_PHRASE</promise>
|
||||
Third: <promise>THIRD_PHRASE</promise>
|
||||
`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('FIRST_PHRASE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('in backticks', () => {
|
||||
it('should extract phrase from within backticks', () => {
|
||||
const content = 'Output `<promise>COMPLETE</promise>` when done.';
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('COMPLETE');
|
||||
});
|
||||
|
||||
it('should extract phrase from code block', () => {
|
||||
const content = `
|
||||
\`\`\`
|
||||
<promise>DONE</promise>
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('DONE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('no match cases', () => {
|
||||
it('should return null when no pattern found', () => {
|
||||
const content = `Just some regular text without any promise tags.`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for empty content', () => {
|
||||
const phrase = extractCompletionPhraseFromContent('');
|
||||
|
||||
expect(phrase).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for malformed tags', () => {
|
||||
const content = `<promise>INCOMPLETE`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for empty promise', () => {
|
||||
const content = `<promise></promise>`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for promise with only whitespace', () => {
|
||||
const content = `<promise> </promise>`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('real-world CLAUDE.md examples', () => {
|
||||
it('should extract from typical CLAUDE.md content', () => {
|
||||
const content = `
|
||||
# CLAUDE.md
|
||||
|
||||
## Task
|
||||
|
||||
Build a REST API for user management.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- All endpoints working
|
||||
- Tests passing
|
||||
- Documentation complete
|
||||
|
||||
Output <promise>COMPLETE</promise> when all criteria met.
|
||||
`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('COMPLETE');
|
||||
});
|
||||
|
||||
it('should extract from Ralph Loop section', () => {
|
||||
const content = `
|
||||
## Ralph Wiggum Loop
|
||||
|
||||
Completion phrase: <promise>TIME_COMPLETE</promise>
|
||||
|
||||
Keep working until minimum duration reached.
|
||||
`;
|
||||
|
||||
const phrase = extractCompletionPhraseFromContent(content);
|
||||
|
||||
expect(phrase).toBe('TIME_COMPLETE');
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,26 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { InnerLoopTracker } from '../src/inner-loop-tracker.js';
|
||||
import { InnerLoopState, InnerTodoItem } from '../src/types.js';
|
||||
import { RalphTracker } from '../src/ralph-tracker.js';
|
||||
import { RalphTrackerState, RalphTodoItem } from '../src/types.js';
|
||||
|
||||
/**
|
||||
* InnerLoopTracker Tests
|
||||
* RalphTracker Tests
|
||||
*
|
||||
* Tests the detection of Ralph Wiggum loops and todo lists from terminal output
|
||||
* running inside Claude Code sessions.
|
||||
*/
|
||||
|
||||
describe('InnerLoopTracker', () => {
|
||||
let tracker: InnerLoopTracker;
|
||||
describe('RalphTracker', () => {
|
||||
let tracker: RalphTracker;
|
||||
|
||||
beforeEach(() => {
|
||||
tracker = new InnerLoopTracker();
|
||||
tracker = new RalphTracker();
|
||||
// Enable tracker by default for most tests (testing detection logic)
|
||||
tracker.enable();
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should start with inactive loop state', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const freshTracker = new RalphTracker();
|
||||
const state = freshTracker.loopState;
|
||||
expect(state.active).toBe(false);
|
||||
expect(state.completionPhrase).toBeNull();
|
||||
@@ -33,7 +33,7 @@ describe('InnerLoopTracker', () => {
|
||||
});
|
||||
|
||||
it('should start disabled by default', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const freshTracker = new RalphTracker();
|
||||
expect(freshTracker.enabled).toBe(false);
|
||||
expect(freshTracker.loopState.enabled).toBe(false);
|
||||
});
|
||||
@@ -41,7 +41,7 @@ describe('InnerLoopTracker', () => {
|
||||
|
||||
describe('Auto-Enable Behavior', () => {
|
||||
it('should not process data when disabled', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const freshTracker = new RalphTracker();
|
||||
// This pattern doesn't trigger auto-enable
|
||||
freshTracker.processTerminalData('Elapsed: 2.5 hours\n');
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('InnerLoopTracker', () => {
|
||||
});
|
||||
|
||||
it('should auto-enable on /ralph-loop:ralph-loop command', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const freshTracker = new RalphTracker();
|
||||
const enableHandler = vi.fn();
|
||||
freshTracker.on('enabled', enableHandler);
|
||||
|
||||
@@ -60,21 +60,21 @@ describe('InnerLoopTracker', () => {
|
||||
});
|
||||
|
||||
it('should auto-enable on completion phrase', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const freshTracker = new RalphTracker();
|
||||
freshTracker.processTerminalData('<promise>COMPLETE</promise>\n');
|
||||
|
||||
expect(freshTracker.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should auto-enable on TodoWrite detection', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const freshTracker = new RalphTracker();
|
||||
freshTracker.processTerminalData('TodoWrite: Todos have been modified\n');
|
||||
|
||||
expect(freshTracker.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should auto-enable on todo checkboxes', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const freshTracker = new RalphTracker();
|
||||
freshTracker.processTerminalData('- [ ] New task\n');
|
||||
|
||||
expect(freshTracker.enabled).toBe(true);
|
||||
@@ -82,21 +82,21 @@ describe('InnerLoopTracker', () => {
|
||||
});
|
||||
|
||||
it('should auto-enable on iteration patterns', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const freshTracker = new RalphTracker();
|
||||
freshTracker.processTerminalData('Iteration 5/50\n');
|
||||
|
||||
expect(freshTracker.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should auto-enable on loop start patterns', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const freshTracker = new RalphTracker();
|
||||
freshTracker.processTerminalData('Loop started at 2024-01-15\n');
|
||||
|
||||
expect(freshTracker.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow manual enable/disable', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const freshTracker = new RalphTracker();
|
||||
expect(freshTracker.enabled).toBe(false);
|
||||
|
||||
freshTracker.enable();
|
||||
@@ -151,6 +151,28 @@ describe('InnerLoopTracker', () => {
|
||||
expect(tracker.loopState.completionPhrase).toBe('MY_CUSTOM_PHRASE_123');
|
||||
});
|
||||
|
||||
it('should detect completion phrases with hyphens', () => {
|
||||
const completionHandler = vi.fn();
|
||||
tracker.on('completionDetected', completionHandler);
|
||||
|
||||
tracker.startLoop();
|
||||
tracker.processTerminalData('<promise>TESTS-PASS</promise>\n');
|
||||
|
||||
expect(completionHandler).toHaveBeenCalledWith('TESTS-PASS');
|
||||
expect(tracker.loopState.completionPhrase).toBe('TESTS-PASS');
|
||||
});
|
||||
|
||||
it('should detect completion phrases with mixed characters', () => {
|
||||
const completionHandler = vi.fn();
|
||||
tracker.on('completionDetected', completionHandler);
|
||||
|
||||
tracker.startLoop();
|
||||
tracker.processTerminalData('<promise>TASK-123_COMPLETE</promise>\n');
|
||||
|
||||
expect(completionHandler).toHaveBeenCalledWith('TASK-123_COMPLETE');
|
||||
expect(tracker.loopState.completionPhrase).toBe('TASK-123_COMPLETE');
|
||||
});
|
||||
|
||||
it('should mark loop as inactive when completion detected', () => {
|
||||
// Start a loop first
|
||||
tracker.startLoop('TEST_PHRASE');
|
||||
@@ -357,7 +379,7 @@ describe('InnerLoopTracker', () => {
|
||||
});
|
||||
|
||||
it('should auto-enable on native todo pattern', () => {
|
||||
const freshTracker = new InnerLoopTracker();
|
||||
const freshTracker = new RalphTracker();
|
||||
freshTracker.processTerminalData('☐ New task\n');
|
||||
|
||||
expect(freshTracker.enabled).toBe(true);
|
||||
@@ -427,8 +449,10 @@ describe('InnerLoopTracker', () => {
|
||||
});
|
||||
|
||||
it('should clear all state', () => {
|
||||
tracker.startLoop('TEST');
|
||||
tracker.processTerminalData('- [ ] Test task to clear\n');
|
||||
// Use unique phrase that won't appear in the todo content
|
||||
// (bare phrase detection would trigger on common words like 'TEST')
|
||||
tracker.startLoop('XYZZY_COMPLETE');
|
||||
tracker.processTerminalData('- [ ] Sample task to clear\n');
|
||||
|
||||
expect(tracker.loopState.active).toBe(true);
|
||||
expect(tracker.todos).toHaveLength(1);
|
||||
@@ -443,7 +467,7 @@ describe('InnerLoopTracker', () => {
|
||||
|
||||
describe('State Restoration', () => {
|
||||
it('should restore state from persisted data', () => {
|
||||
const loopState: InnerLoopState = {
|
||||
const loopState: RalphTrackerState = {
|
||||
enabled: true,
|
||||
active: true,
|
||||
completionPhrase: 'RESTORED',
|
||||
@@ -454,7 +478,7 @@ describe('InnerLoopTracker', () => {
|
||||
elapsedHours: 1.5,
|
||||
};
|
||||
|
||||
const todos: InnerTodoItem[] = [
|
||||
const todos: RalphTodoItem[] = [
|
||||
{ id: 'todo-1', content: 'Task 1', status: 'completed', detectedAt: Date.now() },
|
||||
{ id: 'todo-2', content: 'Task 2', status: 'in_progress', detectedAt: Date.now() },
|
||||
];
|
||||
@@ -479,7 +503,7 @@ describe('InnerLoopTracker', () => {
|
||||
maxIterations: null,
|
||||
lastActivity: Date.now(),
|
||||
elapsedHours: null,
|
||||
} as InnerLoopState;
|
||||
} as RalphTrackerState;
|
||||
|
||||
tracker.restoreState(loopState, []);
|
||||
|
||||
@@ -125,9 +125,10 @@ describe('Interactive Session Lifecycle', () => {
|
||||
const response = await fetch(`${baseUrl}/api/sessions/${sessionId}/output`);
|
||||
const data = await response.json();
|
||||
|
||||
expect(data).toHaveProperty('textOutput');
|
||||
expect(data).toHaveProperty('messages');
|
||||
expect(data).toHaveProperty('errorBuffer');
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.data).toHaveProperty('textOutput');
|
||||
expect(data.data).toHaveProperty('messages');
|
||||
expect(data.data).toHaveProperty('errorBuffer');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+27
-27
@@ -2,7 +2,7 @@
|
||||
* @fileoverview Tests for StateStore
|
||||
*
|
||||
* Tests the persistent JSON state storage including
|
||||
* debounced saves, state CRUD operations, and inner state management.
|
||||
* debounced saves, state CRUD operations, and Ralph state management.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
@@ -11,7 +11,7 @@ import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
// Import types before mocking
|
||||
import type { AppState, SessionState, TaskState, InnerSessionState } from '../src/types.js';
|
||||
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';
|
||||
@@ -231,7 +231,7 @@ describe('StateStore', () => {
|
||||
|
||||
expect(store.getSession('session-1')).toBeNull();
|
||||
expect(store.getTask('task-1')).toBeNull();
|
||||
expect(store.getAllInnerStates().size).toBe(0);
|
||||
expect(store.getAllRalphStates().size).toBe(0);
|
||||
});
|
||||
|
||||
it('should preserve state file path in config', () => {
|
||||
@@ -243,27 +243,27 @@ describe('StateStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('inner state operations', () => {
|
||||
it('should get and set inner state', () => {
|
||||
describe('ralph state operations', () => {
|
||||
it('should get and set ralph state', () => {
|
||||
const store = new StateStore(testFilePath);
|
||||
const innerState = createMockInnerState('session-1');
|
||||
const ralphState = createMockRalphState('session-1');
|
||||
|
||||
store.setInnerState('session-1', innerState);
|
||||
store.setRalphState('session-1', ralphState);
|
||||
|
||||
expect(store.getInnerState('session-1')).toEqual(innerState);
|
||||
expect(store.getRalphState('session-1')).toEqual(ralphState);
|
||||
});
|
||||
|
||||
it('should return null for non-existent inner state', () => {
|
||||
it('should return null for non-existent ralph state', () => {
|
||||
const store = new StateStore(testFilePath);
|
||||
|
||||
expect(store.getInnerState('non-existent')).toBeNull();
|
||||
expect(store.getRalphState('non-existent')).toBeNull();
|
||||
});
|
||||
|
||||
it('should update inner state with partial merge', () => {
|
||||
it('should update ralph state with partial merge', () => {
|
||||
const store = new StateStore(testFilePath);
|
||||
|
||||
store.setInnerState('session-1', createMockInnerState('session-1'));
|
||||
const updated = store.updateInnerState('session-1', { totalTodos: 10 });
|
||||
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');
|
||||
@@ -272,27 +272,27 @@ describe('StateStore', () => {
|
||||
it('should create initial state on update if none exists', () => {
|
||||
const store = new StateStore(testFilePath);
|
||||
|
||||
const state = store.updateInnerState('new-session', { totalTodos: 5 });
|
||||
const state = store.updateRalphState('new-session', { totalTodos: 5 });
|
||||
|
||||
expect(state.sessionId).toBe('new-session');
|
||||
expect(state.totalTodos).toBe(5);
|
||||
});
|
||||
|
||||
it('should remove inner state', () => {
|
||||
it('should remove ralph state', () => {
|
||||
const store = new StateStore(testFilePath);
|
||||
store.setInnerState('session-1', createMockInnerState('session-1'));
|
||||
store.setRalphState('session-1', createMockRalphState('session-1'));
|
||||
|
||||
store.removeInnerState('session-1');
|
||||
store.removeRalphState('session-1');
|
||||
|
||||
expect(store.getInnerState('session-1')).toBeNull();
|
||||
expect(store.getRalphState('session-1')).toBeNull();
|
||||
});
|
||||
|
||||
it('should get all inner states', () => {
|
||||
it('should get all ralph states', () => {
|
||||
const store = new StateStore(testFilePath);
|
||||
store.setInnerState('session-1', createMockInnerState('session-1'));
|
||||
store.setInnerState('session-2', createMockInnerState('session-2'));
|
||||
store.setRalphState('session-1', createMockRalphState('session-1'));
|
||||
store.setRalphState('session-2', createMockRalphState('session-2'));
|
||||
|
||||
const allStates = store.getAllInnerStates();
|
||||
const allStates = store.getAllRalphStates();
|
||||
|
||||
expect(allStates.size).toBe(2);
|
||||
expect(allStates.has('session-1')).toBe(true);
|
||||
@@ -301,11 +301,11 @@ describe('StateStore', () => {
|
||||
});
|
||||
|
||||
describe('flushAll', () => {
|
||||
it('should flush both main and inner state', () => {
|
||||
it('should flush both main and ralph state', () => {
|
||||
const store = new StateStore(testFilePath);
|
||||
|
||||
store.setSession('session-1', createMockSessionState('session-1'));
|
||||
store.setInnerState('session-1', createMockInnerState('session-1'));
|
||||
store.setRalphState('session-1', createMockRalphState('session-1'));
|
||||
|
||||
store.flushAll();
|
||||
|
||||
@@ -320,7 +320,7 @@ describe('StateStore', () => {
|
||||
const store1 = new StateStore(testFilePath);
|
||||
store1.setSession('session-1', createMockSessionState('session-1'));
|
||||
store1.setTask('task-1', createMockTaskState('task-1'));
|
||||
store1.setInnerState('session-1', createMockInnerState('session-1'));
|
||||
store1.setRalphState('session-1', createMockRalphState('session-1'));
|
||||
store1.flushAll();
|
||||
|
||||
// Create new instance and verify state is loaded
|
||||
@@ -328,7 +328,7 @@ describe('StateStore', () => {
|
||||
|
||||
expect(store2.getSession('session-1')).toBeDefined();
|
||||
expect(store2.getTask('task-1')).toBeDefined();
|
||||
expect(store2.getInnerState('session-1')).toBeDefined();
|
||||
expect(store2.getRalphState('session-1')).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -363,7 +363,7 @@ function createMockTaskState(id: string): TaskState {
|
||||
};
|
||||
}
|
||||
|
||||
function createMockInnerState(sessionId: string): InnerSessionState {
|
||||
function createMockRalphState(sessionId: string): RalphSessionState {
|
||||
return {
|
||||
sessionId,
|
||||
enabled: true,
|
||||
|
||||
+8
-8
@@ -9,8 +9,8 @@ import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
createErrorResponse,
|
||||
createSuccessResponse,
|
||||
createInitialInnerLoopState,
|
||||
createInitialInnerSessionState,
|
||||
createInitialRalphTrackerState,
|
||||
createInitialRalphSessionState,
|
||||
createInitialState,
|
||||
ErrorMessages,
|
||||
} from '../src/types.js';
|
||||
@@ -69,9 +69,9 @@ describe('types utility functions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createInitialInnerLoopState', () => {
|
||||
it('should create initial inner loop state', () => {
|
||||
const state = createInitialInnerLoopState();
|
||||
describe('createInitialRalphTrackerState', () => {
|
||||
it('should create initial Ralph tracker state', () => {
|
||||
const state = createInitialRalphTrackerState();
|
||||
|
||||
expect(state.enabled).toBe(false);
|
||||
expect(state.active).toBe(false);
|
||||
@@ -84,9 +84,9 @@ describe('types utility functions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createInitialInnerSessionState', () => {
|
||||
it('should create initial inner session state with session ID', () => {
|
||||
const state = createInitialInnerSessionState('session-123');
|
||||
describe('createInitialRalphSessionState', () => {
|
||||
it('should create initial Ralph session state with session ID', () => {
|
||||
const state = createInitialRalphSessionState('session-123');
|
||||
|
||||
expect(state.sessionId).toBe('session-123');
|
||||
expect(state.loop).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user