moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View 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([]);
});
});