mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
- Remove unused validateTokenCounts/validateTokensAndCost exports and PlanPhase type alias - Add execPattern() helper to eliminate 8 repetitive .lastIndex=0 + exec() loops - Centralize 11 magic number constants into config/ai-defaults.ts and config/server-timing.ts - Remove stale src/tui from tsconfig.json exclude - Fix CLAUDE.md inaccuracies (session helpers, app.js line count, module count) - Add 316 new tests: LRUMap (38), StaleExpirationMap (42), BufferAccumulator (33), respawn helpers (142), system-routes expansion (11→61) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
249 lines
8.4 KiB
TypeScript
249 lines
8.4 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { RespawnAdaptiveTiming, AdaptiveTimingConfig } from '../src/respawn-adaptive-timing.js';
|
|
|
|
describe('RespawnAdaptiveTiming', () => {
|
|
let timing: RespawnAdaptiveTiming;
|
|
const defaultConfig: AdaptiveTimingConfig = {
|
|
adaptiveMinConfirmMs: 5000,
|
|
adaptiveMaxConfirmMs: 30000,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
timing = new RespawnAdaptiveTiming(defaultConfig);
|
|
});
|
|
|
|
describe('initial state', () => {
|
|
it('should start with default 10000ms completion confirm', () => {
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(10000);
|
|
});
|
|
|
|
it('should start with empty timing history', () => {
|
|
const history = timing.getTimingHistory();
|
|
expect(history.recentIdleDetectionMs).toEqual([]);
|
|
expect(history.recentCycleDurationMs).toEqual([]);
|
|
expect(history.sampleCount).toBe(0);
|
|
expect(history.maxSamples).toBe(20);
|
|
});
|
|
});
|
|
|
|
describe('recordTimingData', () => {
|
|
it('should add samples to rolling windows', () => {
|
|
timing.recordTimingData(1000, 5000);
|
|
|
|
const history = timing.getTimingHistory();
|
|
expect(history.recentIdleDetectionMs).toEqual([1000]);
|
|
expect(history.recentCycleDurationMs).toEqual([5000]);
|
|
expect(history.sampleCount).toBe(1);
|
|
});
|
|
|
|
it('should accumulate multiple samples', () => {
|
|
timing.recordTimingData(1000, 5000);
|
|
timing.recordTimingData(2000, 6000);
|
|
timing.recordTimingData(3000, 7000);
|
|
|
|
const history = timing.getTimingHistory();
|
|
expect(history.recentIdleDetectionMs).toEqual([1000, 2000, 3000]);
|
|
expect(history.recentCycleDurationMs).toEqual([5000, 6000, 7000]);
|
|
expect(history.sampleCount).toBe(3);
|
|
});
|
|
|
|
it('should trim to maxSamples when exceeded', () => {
|
|
// Add 22 samples (exceeds maxSamples of 20)
|
|
for (let i = 1; i <= 22; i++) {
|
|
timing.recordTimingData(i * 100, i * 500);
|
|
}
|
|
|
|
const history = timing.getTimingHistory();
|
|
expect(history.recentIdleDetectionMs.length).toBe(20);
|
|
expect(history.recentCycleDurationMs.length).toBe(20);
|
|
// First two should have been shifted off
|
|
expect(history.recentIdleDetectionMs[0]).toBe(300); // 3rd sample
|
|
expect(history.recentCycleDurationMs[0]).toBe(1500);
|
|
expect(history.sampleCount).toBe(20);
|
|
});
|
|
|
|
it('should update lastUpdatedAt timestamp', () => {
|
|
const before = Date.now();
|
|
timing.recordTimingData(1000, 5000);
|
|
const after = Date.now();
|
|
|
|
const history = timing.getTimingHistory();
|
|
expect(history.lastUpdatedAt).toBeGreaterThanOrEqual(before);
|
|
expect(history.lastUpdatedAt).toBeLessThanOrEqual(after);
|
|
});
|
|
});
|
|
|
|
describe('adaptive timing calculation', () => {
|
|
it('should not recalculate with fewer than 5 samples', () => {
|
|
// Add 4 samples -- below the threshold
|
|
for (let i = 0; i < 4; i++) {
|
|
timing.recordTimingData(8000, 20000);
|
|
}
|
|
|
|
// Should still be the default
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(10000);
|
|
});
|
|
|
|
it('should recalculate at exactly 5 samples', () => {
|
|
// All the same value = easy to predict
|
|
for (let i = 0; i < 5; i++) {
|
|
timing.recordTimingData(10000, 20000);
|
|
}
|
|
|
|
// P75 of [10000, 10000, 10000, 10000, 10000] = 10000
|
|
// With 20% buffer = 12000, clamped to [5000, 30000] = 12000
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(12000);
|
|
});
|
|
|
|
it('should use 75th percentile with 20% buffer', () => {
|
|
// 5 sorted values: 1000, 2000, 3000, 4000, 5000
|
|
timing.recordTimingData(3000, 10000);
|
|
timing.recordTimingData(1000, 10000);
|
|
timing.recordTimingData(5000, 10000);
|
|
timing.recordTimingData(2000, 10000);
|
|
timing.recordTimingData(4000, 10000);
|
|
|
|
// Sorted: [1000, 2000, 3000, 4000, 5000]
|
|
// P75 index = floor(5 * 0.75) = 3 -> value = 4000
|
|
// With 20% buffer: 4000 * 1.2 = 4800
|
|
// Clamped to min 5000
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(5000);
|
|
});
|
|
|
|
it('should clamp to minimum configured value', () => {
|
|
// Very small idle detection times
|
|
for (let i = 0; i < 5; i++) {
|
|
timing.recordTimingData(1000, 5000);
|
|
}
|
|
|
|
// P75 = 1000, with buffer = 1200, clamped to min 5000
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(5000);
|
|
});
|
|
|
|
it('should clamp to maximum configured value', () => {
|
|
// Very large idle detection times
|
|
for (let i = 0; i < 5; i++) {
|
|
timing.recordTimingData(50000, 100000);
|
|
}
|
|
|
|
// P75 = 50000, with buffer = 60000, clamped to max 30000
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(30000);
|
|
});
|
|
|
|
it('should handle varying values correctly', () => {
|
|
// 10 values, spread out
|
|
const values = [2000, 4000, 6000, 8000, 10000, 12000, 14000, 16000, 18000, 20000];
|
|
for (const v of values) {
|
|
timing.recordTimingData(v, v * 2);
|
|
}
|
|
|
|
// Sorted: [2000, 4000, 6000, 8000, 10000, 12000, 14000, 16000, 18000, 20000]
|
|
// P75 index = floor(10 * 0.75) = 7 -> value = 16000
|
|
// With 20% buffer: 16000 * 1.2 = 19200
|
|
// Clamped to [5000, 30000] = 19200
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(19200);
|
|
});
|
|
|
|
it('should adapt as new data arrives', () => {
|
|
// Start with small values
|
|
for (let i = 0; i < 5; i++) {
|
|
timing.recordTimingData(3000, 10000);
|
|
}
|
|
// P75 = 3000, buffer = 3600, clamped to min 5000
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(5000);
|
|
|
|
// Now add larger values that shift the P75 up
|
|
for (let i = 0; i < 10; i++) {
|
|
timing.recordTimingData(20000, 40000);
|
|
}
|
|
// P75 of mostly-20000 values = 20000, buffer = 24000
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(24000);
|
|
});
|
|
});
|
|
|
|
describe('getTimingHistory', () => {
|
|
it('should return a new object each call (shallow copy)', () => {
|
|
timing.recordTimingData(1000, 5000);
|
|
const history1 = timing.getTimingHistory();
|
|
const history2 = timing.getTimingHistory();
|
|
|
|
// Different object references each time
|
|
expect(history1).not.toBe(history2);
|
|
// But equal content
|
|
expect(history1).toEqual(history2);
|
|
});
|
|
|
|
it('should allow overwriting top-level fields on the copy without affecting source', () => {
|
|
timing.recordTimingData(1000, 5000);
|
|
const copy = timing.getTimingHistory();
|
|
copy.sampleCount = 999;
|
|
|
|
expect(timing.getTimingHistory().sampleCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('reset', () => {
|
|
it('should clear all timing history', () => {
|
|
// Add some data
|
|
for (let i = 0; i < 10; i++) {
|
|
timing.recordTimingData(i * 1000, i * 5000);
|
|
}
|
|
|
|
timing.reset();
|
|
|
|
const history = timing.getTimingHistory();
|
|
expect(history.recentIdleDetectionMs).toEqual([]);
|
|
expect(history.recentCycleDurationMs).toEqual([]);
|
|
expect(history.adaptiveCompletionConfirmMs).toBe(10000);
|
|
expect(history.sampleCount).toBe(0);
|
|
expect(history.maxSamples).toBe(20);
|
|
});
|
|
|
|
it('should allow re-recording after reset', () => {
|
|
for (let i = 0; i < 5; i++) {
|
|
timing.recordTimingData(10000, 20000);
|
|
}
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(12000);
|
|
|
|
timing.reset();
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(10000);
|
|
|
|
// Record new data -- below 5 samples, should stay at default
|
|
timing.recordTimingData(5000, 10000);
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(10000);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle zero idle detection times', () => {
|
|
for (let i = 0; i < 5; i++) {
|
|
timing.recordTimingData(0, 5000);
|
|
}
|
|
// P75 = 0, buffer = 0, clamped to min 5000
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(5000);
|
|
});
|
|
|
|
it('should handle identical values', () => {
|
|
for (let i = 0; i < 20; i++) {
|
|
timing.recordTimingData(15000, 30000);
|
|
}
|
|
// P75 = 15000, buffer = 18000
|
|
expect(timing.getAdaptiveCompletionConfirmMs()).toBe(18000);
|
|
});
|
|
|
|
it('should handle config with very narrow bounds', () => {
|
|
const narrowTiming = new RespawnAdaptiveTiming({
|
|
adaptiveMinConfirmMs: 10000,
|
|
adaptiveMaxConfirmMs: 10000,
|
|
});
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
narrowTiming.recordTimingData(50000, 100000);
|
|
}
|
|
|
|
// Always clamped to 10000 regardless of data
|
|
expect(narrowTiming.getAdaptiveCompletionConfirmMs()).toBe(10000);
|
|
});
|
|
});
|
|
});
|