103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
/**
|
|
* TDD — ResilienceSettings.comboCooldownWait (quota-share combo cooldown-aware
|
|
* retry, Variante A). Locks the defaults, the resolve/merge round-trip and the
|
|
* clamping/validation contract of normalizeComboCooldownWaitSettings.
|
|
*/
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
DEFAULT_RESILIENCE_SETTINGS,
|
|
mergeResilienceSettings,
|
|
resolveResilienceSettings,
|
|
type ResilienceSettings,
|
|
} from "../../src/lib/resilience/settings.ts";
|
|
|
|
function cloneDefaults(): ResilienceSettings {
|
|
return structuredClone(DEFAULT_RESILIENCE_SETTINGS);
|
|
}
|
|
|
|
test("default comboCooldownWait is conservative (on, 5s ceiling, 2 attempts, 8s budget)", () => {
|
|
const s = cloneDefaults().comboCooldownWait;
|
|
assert.equal(s.enabled, true);
|
|
assert.equal(s.maxWaitMs, 5000);
|
|
assert.equal(s.maxAttempts, 2);
|
|
assert.equal(s.budgetMs, 8000);
|
|
});
|
|
|
|
test("resolveResilienceSettings returns the default block when nothing is stored", () => {
|
|
const resolved = resolveResilienceSettings({});
|
|
assert.deepEqual(resolved.comboCooldownWait, DEFAULT_RESILIENCE_SETTINGS.comboCooldownWait);
|
|
});
|
|
|
|
test("resolveResilienceSettings reads stored comboCooldownWait overrides", () => {
|
|
const resolved = resolveResilienceSettings({
|
|
resilienceSettings: {
|
|
comboCooldownWait: { enabled: true, maxWaitMs: 3000, maxAttempts: 1, budgetMs: 4000 },
|
|
},
|
|
});
|
|
assert.deepEqual(resolved.comboCooldownWait, {
|
|
enabled: true,
|
|
maxWaitMs: 3000,
|
|
maxAttempts: 1,
|
|
budgetMs: 4000,
|
|
});
|
|
});
|
|
|
|
test("mergeResilienceSettings round-trips a partial patch", () => {
|
|
const merged = mergeResilienceSettings(cloneDefaults(), {
|
|
comboCooldownWait: { maxWaitMs: 2000 },
|
|
});
|
|
assert.equal(merged.comboCooldownWait.maxWaitMs, 2000);
|
|
// Unspecified fields keep the current value.
|
|
assert.equal(merged.comboCooldownWait.maxAttempts, 2);
|
|
assert.equal(merged.comboCooldownWait.budgetMs, 8000);
|
|
assert.equal(merged.comboCooldownWait.enabled, true);
|
|
});
|
|
|
|
test("enabled is forced false when maxWaitMs or maxAttempts is zero", () => {
|
|
const a = mergeResilienceSettings(cloneDefaults(), {
|
|
comboCooldownWait: { enabled: true, maxWaitMs: 0 },
|
|
});
|
|
assert.equal(a.comboCooldownWait.enabled, false);
|
|
|
|
const b = mergeResilienceSettings(cloneDefaults(), {
|
|
comboCooldownWait: { enabled: true, maxAttempts: 0 },
|
|
});
|
|
assert.equal(b.comboCooldownWait.enabled, false);
|
|
});
|
|
|
|
test("maxWaitMs is clamped to the 30s hard ceiling", () => {
|
|
const merged = mergeResilienceSettings(cloneDefaults(), {
|
|
comboCooldownWait: { maxWaitMs: 999_999 },
|
|
});
|
|
assert.equal(merged.comboCooldownWait.maxWaitMs, 30000);
|
|
});
|
|
|
|
test("budgetMs can never drop below a single maxWaitMs", () => {
|
|
const merged = mergeResilienceSettings(cloneDefaults(), {
|
|
comboCooldownWait: { maxWaitMs: 5000, budgetMs: 100 },
|
|
});
|
|
// budget floored at maxWaitMs so at least one wait can fire.
|
|
assert.equal(merged.comboCooldownWait.budgetMs, 5000);
|
|
});
|
|
|
|
test("explicit disable is honored", () => {
|
|
const merged = mergeResilienceSettings(cloneDefaults(), {
|
|
comboCooldownWait: { enabled: false },
|
|
});
|
|
assert.equal(merged.comboCooldownWait.enabled, false);
|
|
});
|
|
|
|
test("garbage values fall back to the current numbers", () => {
|
|
const merged = mergeResilienceSettings(cloneDefaults(), {
|
|
comboCooldownWait: {
|
|
maxWaitMs: "not-a-number" as never,
|
|
maxAttempts: null as never,
|
|
budgetMs: undefined,
|
|
},
|
|
});
|
|
assert.equal(merged.comboCooldownWait.maxWaitMs, 5000);
|
|
assert.equal(merged.comboCooldownWait.maxAttempts, 2);
|
|
assert.equal(merged.comboCooldownWait.budgetMs, 8000);
|
|
});
|