aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
492 lines (392 loc) • 15.1 kB
JavaScript
/**
* Unit tests for ExternalMultiLoopStateManager
*
* @tests @tools/ralph-external/external-multi-loop-state-manager.mjs
*/
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert';
import { existsSync, mkdirSync, rmSync, readFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { randomUUID } from 'crypto';
import { ExternalMultiLoopStateManager } from './external-multi-loop-state-manager.mjs';
describe('ExternalMultiLoopStateManager', () => {
let manager;
let testRoot;
beforeEach(() => {
// Create temporary test directory
testRoot = join(tmpdir(), `ralph-test-${randomUUID()}`);
mkdirSync(testRoot, { recursive: true });
manager = new ExternalMultiLoopStateManager(testRoot);
});
afterEach(() => {
// Cleanup test directory
if (existsSync(testRoot)) {
rmSync(testRoot, { recursive: true, force: true });
}
});
describe('initialization', () => {
it('should initialize base directories', () => {
manager.ensureBaseDir();
assert(existsSync(manager.baseDir), 'Base dir should exist');
assert(existsSync(manager.loopsDir), 'Loops dir should exist');
assert(existsSync(manager.archiveDir), 'Archive dir should exist');
assert(existsSync(manager.sharedDir), 'Shared dir should exist');
});
it('should create registry if missing', () => {
const registry = manager.loadRegistry();
assert.strictEqual(registry.version, '2.0.0');
assert.strictEqual(registry.max_concurrent_loops, 4);
assert.strictEqual(registry.active_loops.length, 0);
assert.strictEqual(registry.total_active, 0);
assert.strictEqual(registry.total_completed, 0);
assert.strictEqual(registry.total_aborted, 0);
});
});
describe('generateLoopId', () => {
it('should generate valid loop ID from task', () => {
const loopId = manager.generateLoopId('Fix all failing tests');
assert(loopId.startsWith('ralph-fix-all-failing-tests-'));
assert(loopId.match(/^ralph-[a-z0-9-]+-[a-f0-9]{8}$/));
});
it('should handle special characters', () => {
const loopId = manager.generateLoopId('Fix: tests & docs!');
assert(loopId.startsWith('ralph-fix-tests-docs-'));
assert(loopId.match(/^ralph-[a-z0-9-]+-[a-f0-9]{8}$/));
});
it('should truncate long task names', () => {
const longTask = 'a'.repeat(100);
const loopId = manager.generateLoopId(longTask);
// Slug is max 30 chars + prefix + uuid
assert(loopId.length <= 30 + 6 + 8 + 2); // ralph- + slug + - + uuid
});
});
describe('calculateCommunicationPaths', () => {
it('should calculate correct paths for various loop counts', () => {
assert.strictEqual(manager.calculateCommunicationPaths(2), 1);
assert.strictEqual(manager.calculateCommunicationPaths(3), 3);
assert.strictEqual(manager.calculateCommunicationPaths(4), 6);
assert.strictEqual(manager.calculateCommunicationPaths(5), 10);
assert.strictEqual(manager.calculateCommunicationPaths(7), 21);
});
});
describe('createLoop', () => {
it('should create new loop with generated ID', async () => {
const config = {
objective: 'Fix all tests',
completionCriteria: 'npm test passes',
maxIterations: 10,
};
const { loopId, state } = await manager.createLoop(config);
assert(loopId.startsWith('ralph-fix-all-tests-'));
assert.strictEqual(state.objective, 'Fix all tests');
assert.strictEqual(state.status, 'running');
assert.strictEqual(state.currentIteration, 0);
// Verify directory structure
const loopDir = manager.getLoopDir(loopId);
assert(existsSync(loopDir));
assert(existsSync(join(loopDir, 'iterations')));
assert(existsSync(join(loopDir, 'prompts')));
assert(existsSync(join(loopDir, 'outputs')));
assert(existsSync(join(loopDir, 'checkpoints')));
// Verify state file
const statePath = join(loopDir, 'session-state.json');
assert(existsSync(statePath));
// Verify registry
const registry = manager.loadRegistry();
assert.strictEqual(registry.active_loops.length, 1);
assert.strictEqual(registry.active_loops[0].loop_id, loopId);
});
it('should create new loop with explicit ID', async () => {
const config = {
loopId: 'ralph-explicit-loop-id',
objective: 'Fix all tests',
completionCriteria: 'npm test passes',
};
const { loopId, state } = await manager.createLoop(config);
assert.strictEqual(loopId, 'ralph-explicit-loop-id');
assert.strictEqual(state.loopId, 'ralph-explicit-loop-id');
assert(existsSync(manager.getLoopDir('ralph-explicit-loop-id')));
const registry = manager.loadRegistry();
assert.strictEqual(registry.active_loops[0].loop_id, 'ralph-explicit-loop-id');
});
it('should enforce MAX_CONCURRENT_LOOPS', async () => {
// Create 4 loops
for (let i = 0; i < 4; i++) {
await manager.createLoop({
objective: `Task ${i}`,
completionCriteria: 'done',
});
}
// 5th should fail
await assert.rejects(
async () => {
await manager.createLoop({
objective: 'Task 5',
completionCriteria: 'done',
});
},
/Cannot create loop: 4 loops already active/
);
});
it('should allow override with --force', async () => {
// Create 4 loops
for (let i = 0; i < 4; i++) {
await manager.createLoop({
objective: `Task ${i}`,
completionCriteria: 'done',
});
}
// 5th with --force should succeed
const { loopId } = await manager.createLoop(
{
objective: 'Task 5',
completionCriteria: 'done',
},
{ force: true }
);
assert(loopId);
const registry = manager.loadRegistry();
assert.strictEqual(registry.active_loops.length, 5);
});
it('should create legacy symlink for single loop', async () => {
const { loopId } = await manager.createLoop({
objective: 'Single task',
completionCriteria: 'done',
});
const legacyPath = join(manager.baseDir, 'session-state.json');
// Symlink may not be supported on all platforms
if (existsSync(legacyPath)) {
// Verify it points to loop state
const content = readFileSync(legacyPath, 'utf8');
const state = JSON.parse(content);
assert.strictEqual(state.loopId, loopId);
}
});
});
describe('getLoop', () => {
it('should retrieve loop state by ID', async () => {
const { loopId } = await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
const state = manager.getLoop(loopId);
assert.strictEqual(state.loopId, loopId);
assert.strictEqual(state.objective, 'Test task');
});
it('should throw error for non-existent loop', () => {
assert.throws(
() => {
manager.getLoop('ralph-nonexistent-12345678');
},
/Loop not found/
);
});
});
describe('updateLoop', () => {
it('should update loop state', async () => {
const { loopId } = await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
const updated = manager.updateLoop(loopId, {
status: 'paused',
currentIteration: 5,
});
assert.strictEqual(updated.status, 'paused');
assert.strictEqual(updated.currentIteration, 5);
assert(updated.lastUpdate); // Should be updated
// Verify persistence
const reloaded = manager.getLoop(loopId);
assert.strictEqual(reloaded.status, 'paused');
assert.strictEqual(reloaded.currentIteration, 5);
});
});
describe('listActiveLoops', () => {
it('should list all active loops', async () => {
await manager.createLoop({
objective: 'Task 1',
completionCriteria: 'done',
});
await manager.createLoop({
objective: 'Task 2',
completionCriteria: 'done',
});
const loops = manager.listActiveLoops();
assert.strictEqual(loops.length, 2);
assert(loops[0].task_summary.includes('Task 1'));
assert(loops[1].task_summary.includes('Task 2'));
});
it('should return empty array when no loops', () => {
const loops = manager.listActiveLoops();
assert.strictEqual(loops.length, 0);
});
});
describe('updateLoopPid', () => {
it('should update PID in both state and registry', async () => {
const { loopId } = await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
await manager.updateLoopPid(loopId, 99999);
// Check state
const state = manager.getLoop(loopId);
assert.strictEqual(state.currentPid, 99999);
// Check registry
const registry = manager.loadRegistry();
const loop = registry.active_loops.find((l) => l.loop_id === loopId);
assert.strictEqual(loop.pid, 99999);
});
});
describe('isLoopAlive', () => {
it('should return true for current process', async () => {
const { loopId } = await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
// Set to current process
await manager.updateLoopPid(loopId, process.pid);
assert.strictEqual(manager.isLoopAlive(loopId), true);
});
it('should return false for non-existent process', async () => {
const { loopId } = await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
// Set to unlikely PID
await manager.updateLoopPid(loopId, 99999);
assert.strictEqual(manager.isLoopAlive(loopId), false);
});
it('should return false for null PID', async () => {
const { loopId } = await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
await manager.updateLoopPid(loopId, null);
assert.strictEqual(manager.isLoopAlive(loopId), false);
});
});
describe('archiveLoop', () => {
it('should move loop to archive and update registry', async () => {
const { loopId } = await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
// Mark as completed
manager.updateLoop(loopId, { status: 'completed' });
await manager.archiveLoop(loopId);
// Verify moved to archive
const archiveDir = join(manager.archiveDir, loopId);
assert(existsSync(archiveDir));
// Verify removed from loops
const loopDir = join(manager.loopsDir, loopId);
assert(!existsSync(loopDir));
// Verify registry updated
const registry = manager.loadRegistry();
assert.strictEqual(registry.active_loops.length, 0);
assert.strictEqual(registry.total_completed, 1);
});
it('should increment aborted counter for aborted loops', async () => {
const { loopId } = await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
manager.updateLoop(loopId, { status: 'aborted' });
await manager.archiveLoop(loopId);
const registry = manager.loadRegistry();
assert.strictEqual(registry.total_aborted, 1);
});
});
describe('detectCrashedLoops', () => {
it('should detect loops with dead PIDs', async () => {
const { loopId } = await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
// Set to unlikely dead PID
await manager.updateLoopPid(loopId, 99999);
const crashed = manager.detectCrashedLoops();
assert.strictEqual(crashed.length, 1);
assert.strictEqual(crashed[0].loop_id, loopId);
});
it('should not detect paused loops', async () => {
const { loopId } = await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
manager.updateLoop(loopId, { status: 'paused' });
await manager.updateLoopPid(loopId, 99999);
const crashed = manager.detectCrashedLoops();
assert.strictEqual(crashed.length, 0);
});
it('should not detect loops with null PID', async () => {
await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
const crashed = manager.detectCrashedLoops();
// Current process is alive
assert.strictEqual(crashed.length, 0);
});
});
describe('file locking', () => {
it('should acquire and release lock', async () => {
const { acquired } = await manager.acquireLock('test-lock');
assert.strictEqual(acquired, true);
assert(existsSync(manager.lockPath));
manager.releaseLock('test-lock');
assert(!existsSync(manager.lockPath));
});
it('should detect stale locks', async () => {
// Ensure base directory exists before writing lock file
manager.ensureBaseDir();
// Create expired lock
const staleLock = {
pid: 99999,
loopId: 'stale',
timestamp: Date.now() - 60000,
leaseExpiry: Date.now() - 30000, // Expired
fencingToken: Date.now(),
};
const lockPath = manager.lockPath;
const fs = await import('fs');
fs.writeFileSync(lockPath, JSON.stringify(staleLock));
// Should acquire despite existing lock
const { acquired } = await manager.acquireLock('new-lock');
assert.strictEqual(acquired, true);
});
it('should timeout waiting for lock', async () => {
// Acquire lock
await manager.acquireLock('holder');
// Create new manager to try acquiring
const manager2 = new ExternalMultiLoopStateManager(testRoot);
// Override max attempts for faster test (instance property)
manager2.maxLockAttempts = 3;
await assert.rejects(
async () => {
await manager2.acquireLock('waiter');
},
/Failed to acquire lock/
);
// Cleanup
manager.releaseLock('holder');
});
});
describe('path helpers', () => {
it('should generate correct paths', async () => {
const { loopId } = await manager.createLoop({
objective: 'Test task',
completionCriteria: 'done',
});
const loopDir = manager.getLoopDir(loopId);
assert(loopDir.endsWith(loopId));
const iterDir = manager.getIterationDir(loopId, 5);
assert(iterDir.endsWith('iterations/005'));
const promptPath = manager.getPromptPath(loopId, 3);
assert(promptPath.endsWith('prompts/003-prompt.md'));
const { stdout, stderr } = manager.getOutputPaths(loopId, 2);
assert(stdout.endsWith('outputs/002-stdout.log'));
assert(stderr.endsWith('outputs/002-stderr.log'));
});
});
});