Files
waggle-os/packages/agent/tests/loop-guard-window.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

124 lines
4.9 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { LoopGuard } from '../src/loop-guard.js';
describe('LoopGuard Rolling Window Detection (11B-7)', () => {
it('detects A/B oscillation pattern after 8 calls (4 of each)', () => {
// windowSize=10, windowThreshold=4 (defaults)
const guard = new LoopGuard({ maxRepeats: 10 }); // high maxRepeats so consecutive check doesn't trigger
const argA = { command: 'echo A' };
const argB = { command: 'echo B' };
// A, B, A, B, A, B — 3 of each, still allowed
expect(guard.check('bash', argA)).toBe(true); // window: [A]
expect(guard.check('bash', argB)).toBe(true); // window: [A, B]
expect(guard.check('bash', argA)).toBe(true); // window: [A, B, A]
expect(guard.check('bash', argB)).toBe(true); // window: [A, B, A, B]
expect(guard.check('bash', argA)).toBe(true); // window: [A, B, A, B, A] — A=3
expect(guard.check('bash', argB)).toBe(true); // window: [A, B, A, B, A, B] — B=3
// 7th call: A again — A count = 4 in window => blocked
expect(guard.check('bash', argA)).toBe(false);
});
it('alternating A, B, A, B, A, B, A, B — detects loop after 8th call', () => {
const guard = new LoopGuard({ maxRepeats: 10, windowSize: 10, windowThreshold: 4 });
const results: boolean[] = [];
for (let i = 0; i < 8; i++) {
const args = i % 2 === 0 ? { cmd: 'A' } : { cmd: 'B' };
results.push(guard.check('tool', args));
}
// First 6 should be true (3 of each), 7th triggers A=4
// Call 1: A(1) -> true
// Call 2: B(1) -> true
// Call 3: A(2) -> true
// Call 4: B(2) -> true
// Call 5: A(3) -> true
// Call 6: B(3) -> true
// Call 7: A(4) -> false (4 A's in window)
// Call 8: B(4) -> would also be false but we already stopped at 7
// The 7th call (index 6) should be false
expect(results.slice(0, 6).every(r => r)).toBe(true);
expect(results[6]).toBe(false);
});
it('does not false-positive with diverse calls in the window', () => {
const guard = new LoopGuard({ maxRepeats: 10, windowSize: 10, windowThreshold: 4 });
// 10 different calls
for (let i = 0; i < 10; i++) {
expect(guard.check('tool', { idx: i })).toBe(true);
}
});
it('window slides — old entries drop out', () => {
const guard = new LoopGuard({ maxRepeats: 10, windowSize: 5, windowThreshold: 3 });
// Fill window: A, A, B, C, D — A count is 2
expect(guard.check('t', { v: 'A' })).toBe(true);
expect(guard.check('t', { v: 'A' })).toBe(true);
expect(guard.check('t', { v: 'B' })).toBe(true);
expect(guard.check('t', { v: 'C' })).toBe(true);
expect(guard.check('t', { v: 'D' })).toBe(true);
// Window is now [A, A, B, C, D] — if we add A, window becomes [A, B, C, D, A]
// A count = 2 (within threshold of 3)
expect(guard.check('t', { v: 'A' })).toBe(true);
// Now add E to push out old A: window [B, C, D, A, E]
expect(guard.check('t', { v: 'E' })).toBe(true);
// Add A again: window [C, D, A, E, A] — A count = 2
expect(guard.check('t', { v: 'A' })).toBe(true);
});
it('respects custom windowSize and windowThreshold', () => {
const guard = new LoopGuard({ maxRepeats: 10, windowSize: 4, windowThreshold: 2 });
// With threshold=2 in window of 4:
expect(guard.check('t', { v: 'X' })).toBe(true); // window: [X], count=1
expect(guard.check('t', { v: 'Y' })).toBe(true); // window: [X, Y]
expect(guard.check('t', { v: 'X' })).toBe(false); // window: [X, Y, X], X count=2 => blocked
});
it('reset clears window state', () => {
const guard = new LoopGuard({ maxRepeats: 10, windowSize: 5, windowThreshold: 3 });
// Build up some history
guard.check('t', { v: 'A' });
guard.check('t', { v: 'A' });
guard.reset();
// After reset, A count should be fresh
expect(guard.check('t', { v: 'A' })).toBe(true); // A=1 in fresh window
expect(guard.check('t', { v: 'A' })).toBe(true); // A=2
});
it('consecutive detection still works alongside window detection', () => {
const guard = new LoopGuard({ maxRepeats: 3 }); // consecutive limit
// 3 consecutive identical calls — the 4th is blocked by consecutive check
expect(guard.check('bash', { cmd: 'x' })).toBe(true);
expect(guard.check('bash', { cmd: 'x' })).toBe(true);
expect(guard.check('bash', { cmd: 'x' })).toBe(true);
expect(guard.check('bash', { cmd: 'x' })).toBe(false); // consecutive > 3
});
it('three-way oscillation A, B, C detected when threshold met', () => {
const guard = new LoopGuard({ maxRepeats: 20, windowSize: 12, windowThreshold: 4 });
// A, B, C, A, B, C, A, B, C — 3 of each, still fine
for (let cycle = 0; cycle < 3; cycle++) {
expect(guard.check('t', { v: 'A' })).toBe(true);
expect(guard.check('t', { v: 'B' })).toBe(true);
expect(guard.check('t', { v: 'C' })).toBe(true);
}
// 10th call: A — A count becomes 4 in window => blocked
expect(guard.check('t', { v: 'A' })).toBe(false);
});
});