takt/src/__tests__/cycle-detector.test.ts

219 lines
7.0 KiB
TypeScript

/**
* CycleDetector unit tests
*
* Tests cycle detection logic for loop_monitors.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { CycleDetector } from '../core/piece/engine/cycle-detector.js';
import type { LoopMonitorConfig } from '../core/models/index.js';
function makeMonitor(
cycle: string[],
threshold: number,
rules = [
{ condition: 'healthy', next: 'ai_review' },
{ condition: 'unproductive', next: 'reviewers' },
],
): LoopMonitorConfig {
return {
cycle,
threshold,
judge: { rules },
};
}
describe('CycleDetector', () => {
describe('2-step cycle detection', () => {
let detector: CycleDetector;
const monitor = makeMonitor(['ai_review', 'ai_fix'], 3);
beforeEach(() => {
detector = new CycleDetector([monitor]);
});
it('should not trigger before threshold is reached', () => {
// 2 complete cycles (4 movements)
expect(detector.recordAndCheck('ai_review').triggered).toBe(false);
expect(detector.recordAndCheck('ai_fix').triggered).toBe(false);
expect(detector.recordAndCheck('ai_review').triggered).toBe(false);
expect(detector.recordAndCheck('ai_fix').triggered).toBe(false);
});
it('should trigger when threshold (3 cycles) is reached', () => {
// 3 complete cycles (6 movements)
detector.recordAndCheck('ai_review');
detector.recordAndCheck('ai_fix');
detector.recordAndCheck('ai_review');
detector.recordAndCheck('ai_fix');
detector.recordAndCheck('ai_review');
const result = detector.recordAndCheck('ai_fix');
expect(result.triggered).toBe(true);
expect(result.cycleCount).toBe(3);
expect(result.monitor).toBe(monitor);
});
it('should not trigger when cycle is interrupted by another movement', () => {
detector.recordAndCheck('ai_review');
detector.recordAndCheck('ai_fix');
detector.recordAndCheck('ai_review');
detector.recordAndCheck('ai_fix');
// Interrupt the cycle with a different movement
detector.recordAndCheck('plan');
detector.recordAndCheck('ai_review');
const result = detector.recordAndCheck('ai_fix');
// Only 1 complete cycle since the interruption
expect(result.triggered).toBe(false);
});
it('should not trigger when only the cycle end matches', () => {
// History doesn't form a valid cycle pattern
detector.recordAndCheck('plan');
detector.recordAndCheck('implement');
detector.recordAndCheck('ai_fix');
expect(detector.recordAndCheck('ai_fix').triggered).toBe(false);
});
it('should reset correctly and not trigger after reset', () => {
// Build up 2 cycles
detector.recordAndCheck('ai_review');
detector.recordAndCheck('ai_fix');
detector.recordAndCheck('ai_review');
detector.recordAndCheck('ai_fix');
// Reset
detector.reset();
// Now only 1 cycle after reset
detector.recordAndCheck('ai_review');
const result = detector.recordAndCheck('ai_fix');
expect(result.triggered).toBe(false);
expect(result.cycleCount).toBe(0); // Less than threshold
});
it('should trigger exactly at threshold, not before', () => {
// Threshold is 3
// Cycle 1
expect(detector.recordAndCheck('ai_review').triggered).toBe(false);
expect(detector.recordAndCheck('ai_fix').triggered).toBe(false);
// Cycle 2
expect(detector.recordAndCheck('ai_review').triggered).toBe(false);
expect(detector.recordAndCheck('ai_fix').triggered).toBe(false);
// Cycle 3 (threshold reached)
expect(detector.recordAndCheck('ai_review').triggered).toBe(false);
expect(detector.recordAndCheck('ai_fix').triggered).toBe(true);
});
});
describe('3-step cycle detection', () => {
it('should detect 3-step cycles', () => {
const monitor = makeMonitor(['A', 'B', 'C'], 2);
const detector = new CycleDetector([monitor]);
// Cycle 1
detector.recordAndCheck('A');
detector.recordAndCheck('B');
detector.recordAndCheck('C');
// Cycle 2
detector.recordAndCheck('A');
detector.recordAndCheck('B');
const result = detector.recordAndCheck('C');
expect(result.triggered).toBe(true);
expect(result.cycleCount).toBe(2);
});
});
describe('multiple monitors', () => {
it('should check all monitors and trigger the first matching one', () => {
const monitor1 = makeMonitor(['A', 'B'], 3);
const monitor2 = makeMonitor(['X', 'Y'], 2);
const detector = new CycleDetector([monitor1, monitor2]);
// 2 cycles of X → Y (threshold for monitor2 is 2)
detector.recordAndCheck('X');
detector.recordAndCheck('Y');
detector.recordAndCheck('X');
const result = detector.recordAndCheck('Y');
expect(result.triggered).toBe(true);
expect(result.cycleCount).toBe(2);
expect(result.monitor).toBe(monitor2);
});
});
describe('no monitors', () => {
it('should never trigger with empty monitors', () => {
const detector = new CycleDetector([]);
detector.recordAndCheck('ai_review');
detector.recordAndCheck('ai_fix');
detector.recordAndCheck('ai_review');
const result = detector.recordAndCheck('ai_fix');
expect(result.triggered).toBe(false);
});
});
describe('getHistory', () => {
it('should return the full movement history', () => {
const detector = new CycleDetector([]);
detector.recordAndCheck('plan');
detector.recordAndCheck('implement');
detector.recordAndCheck('ai_review');
expect(detector.getHistory()).toEqual(['plan', 'implement', 'ai_review']);
});
it('should return empty after reset', () => {
const detector = new CycleDetector([]);
detector.recordAndCheck('plan');
detector.reset();
expect(detector.getHistory()).toEqual([]);
});
});
describe('threshold of 1', () => {
it('should trigger after first complete cycle', () => {
const monitor = makeMonitor(['A', 'B'], 1);
const detector = new CycleDetector([monitor]);
detector.recordAndCheck('A');
const result = detector.recordAndCheck('B');
expect(result.triggered).toBe(true);
expect(result.cycleCount).toBe(1);
});
});
describe('beyond threshold', () => {
it('should also trigger at threshold + N (consecutive cycles)', () => {
const monitor = makeMonitor(['A', 'B'], 2);
const detector = new CycleDetector([monitor]);
// 2 cycles → threshold met
detector.recordAndCheck('A');
detector.recordAndCheck('B');
detector.recordAndCheck('A');
const result1 = detector.recordAndCheck('B');
expect(result1.triggered).toBe(true);
expect(result1.cycleCount).toBe(2);
// After reset + 3 more cycles → triggers at 2 again
detector.reset();
detector.recordAndCheck('A');
detector.recordAndCheck('B');
detector.recordAndCheck('A');
const result2 = detector.recordAndCheck('B');
expect(result2.triggered).toBe(true);
expect(result2.cycleCount).toBe(2);
});
});
});