using ClaudeDo.Worker.Usage; namespace ClaudeDo.Worker.Tests.Usage; public sealed class UsageThrottleTests { private static readonly UsageThresholds FiveHour = new(SoftPct: 50, HardPct: 65, GatePct: 80); private static readonly UsageThresholds SevenDay = new(SoftPct: 50, HardPct: 65, GatePct: 90); private static int Effective(double? five, double? seven, int configured = 3) => UsageThrottle.EffectiveSlots(configured, five, FiveHour, seven, SevenDay); [Fact] public void BelowSoftThreshold_ReturnsFullConfiguredSlots() { Assert.Equal(3, Effective(30, 40)); } [Fact] public void AtSoftThreshold_CapsAtTwo() { Assert.Equal(2, Effective(50, 10)); } [Fact] public void BetweenSoftAndHard_CapsAtTwo() { Assert.Equal(2, Effective(60, 0)); } [Fact] public void AtHardThreshold_CapsAtOne() { Assert.Equal(1, Effective(65, 0)); } [Fact] public void BetweenHardAndGate_CapsAtOne() { Assert.Equal(1, Effective(70, 0)); } [Fact] public void AtGateThreshold_ReturnsZero() { Assert.Equal(0, Effective(80, 0)); } [Fact] public void AboveGateThreshold_ReturnsZero() { Assert.Equal(0, Effective(95, 0)); } [Fact] public void FiveHourAloneDecisive_SevenDayLow() { Assert.Equal(1, Effective(70, 5)); } [Fact] public void SevenDayAloneDecisive_FiveHourLow() { Assert.Equal(1, Effective(5, 70)); } [Fact] public void SevenDayAloneAtGate_ReturnsZero() { Assert.Equal(0, Effective(10, 90)); } [Fact] public void FiveHourAloneAtGate_ReturnsZero() { Assert.Equal(0, Effective(80, 10)); } [Fact] public void ConfiguredSlotsSmallerThanCap_NeverIncreased() { // Soft tier caps at 2, but configured is only 1 — throttle never raises parallelism. Assert.Equal(1, Effective(55, 0, configured: 1)); } [Fact] public void NoUtilizationAtAll_ReturnsFullConfiguredSlots() { Assert.Equal(3, Effective(null, null)); } [Fact] public void ZeroSoftAndHardThresholds_NeverThrottleBelowGate() { var five = new UsageThresholds(0, 0, 80); var seven = new UsageThresholds(0, 0, 90); Assert.Equal(3, UsageThrottle.EffectiveSlots(3, 79, five, 89, seven)); } [Fact] public void ZeroGateThresholds_NeverHardBlock() { var five = new UsageThresholds(50, 65, 0); var seven = new UsageThresholds(50, 65, 0); Assert.Equal(1, UsageThrottle.EffectiveSlots(3, 99, five, 99, seven)); } [Fact] public void ConfiguredSlotsBelowOne_ClampedToOneBeforeThrottling() { Assert.Equal(1, Effective(10, 10, configured: 0)); } // ── Per-bucket thresholds are independent ─────────────────────────────── [Fact] public void PerBucket_SameUtilization_DifferentStagesPerBucket() { // 60% is past the 5h soft (50) but still under the 7d soft (70): the 5h bucket decides. var five = new UsageThresholds(50, 65, 80); var seven = new UsageThresholds(70, 85, 90); Assert.Equal(2, UsageThrottle.EffectiveSlots(3, 60, five, 60, seven)); } [Fact] public void PerBucket_LessUtilizedBucketCanStillBeTheStricterOne() { // 7d sits lower (40%) but has the tighter thresholds, so it — not the busier 5h — throttles. var five = new UsageThresholds(90, 95, 99); var seven = new UsageThresholds(20, 35, 90); Assert.Equal(1, UsageThrottle.EffectiveSlots(3, 80, five, 40, seven)); } [Fact] public void PerBucket_StrictestStageWins() { // 5h is only in its soft stage (2 slots), 7d is past its hard stage (1 slot) → 1 wins. var five = new UsageThresholds(50, 65, 80); var seven = new UsageThresholds(30, 40, 90); Assert.Equal(1, UsageThrottle.EffectiveSlots(3, 55, five, 45, seven)); } [Fact] public void PerBucket_MissingBucketNeverThrottles() { // No 7d reading at all: only the 5h bucket may step parallelism down. var five = new UsageThresholds(50, 65, 80); var seven = new UsageThresholds(1, 2, 3); Assert.Equal(2, UsageThrottle.EffectiveSlots(3, 55, five, null, seven)); } }