feat(booterm): structured pty_exited WS notifications. Plan-validated, impl-validated, code-reviewed green (contracts build clean, contracts test 29/29, booterm + web typecheck clean). wip: in-progress inference/provider refactor (agents.ts, provider.ts, new llama-providers.ts, removed llama-args-validator), plus arena, dispatcher, compaction, schema changes. openspec: pty-exit-notifications complete; x-agent-flags planned (not yet implemented).
83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { addJitter, reconnectDecision, DEFAULT_RECONNECT_POLICY } from '../fleet-connector.js';
|
|
|
|
describe('addJitter', () => {
|
|
it('returns a value >= the input delay', () => {
|
|
const jittered = addJitter(1000);
|
|
expect(jittered).toBeGreaterThanOrEqual(1000);
|
|
});
|
|
|
|
it('returns a value <= 1.5x the input delay', () => {
|
|
const jittered = addJitter(1000);
|
|
expect(jittered).toBeLessThanOrEqual(1500);
|
|
});
|
|
|
|
it('0ms delay stays 0ms', () => {
|
|
expect(addJitter(0)).toBe(0);
|
|
});
|
|
|
|
it('returns different values on repeated calls (stochastic)', () => {
|
|
const results = new Set<number>();
|
|
for (let i = 0; i < 20; i++) {
|
|
results.add(addJitter(1000));
|
|
}
|
|
expect(results.size).toBeGreaterThan(1);
|
|
});
|
|
});
|
|
|
|
describe('reconnectDecision', () => {
|
|
it('first failure returns baseMs with jitter', () => {
|
|
const decision = reconnectDecision(1);
|
|
expect(decision.action).toBe('reconnect');
|
|
expect(decision.delayMs).toBeGreaterThanOrEqual(DEFAULT_RECONNECT_POLICY.baseMs);
|
|
expect(decision.delayMs).toBeLessThanOrEqual(DEFAULT_RECONNECT_POLICY.baseMs * 1.5);
|
|
});
|
|
|
|
it('exponential growth: failure 2 returns 2x baseMs with jitter', () => {
|
|
const decision = reconnectDecision(2);
|
|
expect(decision.action).toBe('reconnect');
|
|
expect(decision.delayMs).toBeGreaterThanOrEqual(DEFAULT_RECONNECT_POLICY.baseMs * 2);
|
|
expect(decision.delayMs).toBeLessThanOrEqual(DEFAULT_RECONNECT_POLICY.baseMs * 3);
|
|
});
|
|
|
|
it('exponential growth: failure 3 returns 4x baseMs with jitter', () => {
|
|
const decision = reconnectDecision(3);
|
|
expect(decision.action).toBe('reconnect');
|
|
expect(decision.delayMs).toBeGreaterThanOrEqual(DEFAULT_RECONNECT_POLICY.baseMs * 4);
|
|
expect(decision.delayMs).toBeLessThanOrEqual(DEFAULT_RECONNECT_POLICY.baseMs * 6);
|
|
});
|
|
|
|
it('capped at maxMs with jitter', () => {
|
|
const decision = reconnectDecision(6);
|
|
expect(decision.action).toBe('reconnect');
|
|
expect(decision.delayMs).toBeGreaterThanOrEqual(DEFAULT_RECONNECT_POLICY.maxMs);
|
|
expect(decision.delayMs).toBeLessThanOrEqual(DEFAULT_RECONNECT_POLICY.maxMs * 1.5);
|
|
});
|
|
|
|
it('gives up after maxAttempts', () => {
|
|
const decision = reconnectDecision(DEFAULT_RECONNECT_POLICY.maxAttempts + 1);
|
|
expect(decision).toEqual({ action: 'give-up' });
|
|
});
|
|
|
|
it('custom policy works with jitter', () => {
|
|
const policy = { baseMs: 500, maxMs: 5000, maxAttempts: 3 };
|
|
const d1 = reconnectDecision(1, policy);
|
|
expect(d1.action).toBe('reconnect');
|
|
expect(d1.delayMs).toBeGreaterThanOrEqual(500);
|
|
expect(d1.delayMs).toBeLessThanOrEqual(750);
|
|
|
|
const d2 = reconnectDecision(2, policy);
|
|
expect(d2.action).toBe('reconnect');
|
|
expect(d2.delayMs).toBeGreaterThanOrEqual(1000);
|
|
expect(d2.delayMs).toBeLessThanOrEqual(1500);
|
|
|
|
const d3 = reconnectDecision(3, policy);
|
|
expect(d3.action).toBe('reconnect');
|
|
expect(d3.delayMs).toBeGreaterThanOrEqual(2000);
|
|
expect(d3.delayMs).toBeLessThanOrEqual(3000);
|
|
|
|
const d4 = reconnectDecision(4, policy);
|
|
expect(d4).toEqual({ action: 'give-up' });
|
|
});
|
|
});
|