This commit is contained in:
145
benchmarks/harness/tests/streak-tracker.test.ts
Normal file
145
benchmarks/harness/tests/streak-tracker.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Task 2.5 Stage 1.5 §7.2 — StreakTracker tests.
|
||||
*
|
||||
* Verifies the consecutive-fetch-transport-failure counter, reset semantics,
|
||||
* and observability surface (getRecentWindow, summary).
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { StreakTracker, isFetchTransportFailure } from '../src/streak-tracker.js';
|
||||
|
||||
describe('isFetchTransportFailure', () => {
|
||||
it('matches fetch_error_* patterns', () => {
|
||||
expect(isFetchTransportFailure('fetch_error_TypeError')).toBe(true);
|
||||
expect(isFetchTransportFailure('fetch_error_RangeError')).toBe(true);
|
||||
expect(isFetchTransportFailure('fetch_error_SyntaxError')).toBe(true);
|
||||
});
|
||||
|
||||
it('does NOT match non-fetch patterns', () => {
|
||||
expect(isFetchTransportFailure(null)).toBe(false);
|
||||
expect(isFetchTransportFailure(undefined)).toBe(false);
|
||||
expect(isFetchTransportFailure('timeout')).toBe(false);
|
||||
expect(isFetchTransportFailure('http_500')).toBe(false);
|
||||
expect(isFetchTransportFailure('http_404')).toBe(false);
|
||||
expect(isFetchTransportFailure('')).toBe(false);
|
||||
expect(isFetchTransportFailure('FETCH_ERROR_TypeError')).toBe(false); // case-sensitive
|
||||
});
|
||||
});
|
||||
|
||||
describe('StreakTracker — halt trigger', () => {
|
||||
it('does not halt on 4 consecutive fetch_error_TypeError', () => {
|
||||
const t = new StreakTracker();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(t.record('fetch_error_TypeError')).toBe(false);
|
||||
}
|
||||
expect(t.getConsecutiveFailures()).toBe(4);
|
||||
});
|
||||
|
||||
it('halts on the 5th consecutive fetch_error_TypeError', () => {
|
||||
const t = new StreakTracker();
|
||||
for (let i = 0; i < 4; i++) t.record('fetch_error_TypeError');
|
||||
expect(t.record('fetch_error_TypeError')).toBe(true);
|
||||
expect(t.getConsecutiveFailures()).toBe(5);
|
||||
});
|
||||
|
||||
it('resets counter on a successful call (null failureMode)', () => {
|
||||
const t = new StreakTracker();
|
||||
for (let i = 0; i < 4; i++) t.record('fetch_error_TypeError');
|
||||
t.record(null);
|
||||
expect(t.getConsecutiveFailures()).toBe(0);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(t.record('fetch_error_TypeError')).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('resets counter on timeout (AbortError)', () => {
|
||||
const t = new StreakTracker();
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record('timeout');
|
||||
expect(t.getConsecutiveFailures()).toBe(0);
|
||||
});
|
||||
|
||||
it('resets counter on http_5xx', () => {
|
||||
const t = new StreakTracker();
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record('http_502');
|
||||
expect(t.getConsecutiveFailures()).toBe(0);
|
||||
});
|
||||
|
||||
it('mixes fetch_error_* subtypes and counts them all', () => {
|
||||
const t = new StreakTracker();
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record('fetch_error_RangeError');
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record('fetch_error_SyntaxError');
|
||||
expect(t.record('fetch_error_TypeError')).toBe(true); // 5th consecutive
|
||||
});
|
||||
});
|
||||
|
||||
describe('StreakTracker — configuration', () => {
|
||||
it('honours a custom threshold', () => {
|
||||
const t = new StreakTracker({ threshold: 3 });
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record('fetch_error_TypeError');
|
||||
expect(t.record('fetch_error_TypeError')).toBe(true);
|
||||
});
|
||||
|
||||
it('clamps threshold to minimum of 1', () => {
|
||||
const t = new StreakTracker({ threshold: 0 });
|
||||
expect(t.record('fetch_error_TypeError')).toBe(true); // threshold clamped to 1
|
||||
});
|
||||
|
||||
it('honours a custom window size', () => {
|
||||
const t = new StreakTracker({ windowSize: 3 });
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record(null);
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record(null); // oldest entry slides out
|
||||
const w = t.getRecentWindow();
|
||||
expect(w).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('StreakTracker — observability', () => {
|
||||
it('getRecentWindow returns snapshot of last N outcomes', () => {
|
||||
const t = new StreakTracker({ windowSize: 5 });
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record(null);
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record('timeout');
|
||||
t.record('fetch_error_TypeError');
|
||||
expect(t.getRecentWindow()).toEqual([true, false, true, false, true]);
|
||||
});
|
||||
|
||||
it('caps window at windowSize', () => {
|
||||
const t = new StreakTracker({ windowSize: 3 });
|
||||
for (let i = 0; i < 7; i++) {
|
||||
t.record(i % 2 === 0 ? 'fetch_error_TypeError' : null);
|
||||
}
|
||||
expect(t.getRecentWindow()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('summary() returns a human-readable status line', () => {
|
||||
const t = new StreakTracker({ threshold: 5, windowSize: 5 });
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record(null);
|
||||
const s = t.summary();
|
||||
expect(s).toContain('consecutive=0');
|
||||
expect(s).toContain('threshold=5');
|
||||
expect(s).toContain('[XX.]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('StreakTracker — reset', () => {
|
||||
it('reset clears both counter and window', () => {
|
||||
const t = new StreakTracker();
|
||||
t.record('fetch_error_TypeError');
|
||||
t.record('fetch_error_TypeError');
|
||||
t.reset();
|
||||
expect(t.getConsecutiveFailures()).toBe(0);
|
||||
expect(t.getRecentWindow()).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user