File size: 4,588 Bytes
f0634fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { SyncDescriptor } from '#/_base/di/descriptors';
import { DisposableStore } from '#/_base/di/lifecycle';
import { TestInstantiationService } from '#/_base/di/test';
import {
  ITowerRateLimitService,
  type TowerRateLimitSnapshot,
} from '#/features/tower/towerRateLimit';
import {
  RATE_LIMIT_CAPACITY_RECOVERY_INTERVAL_MS,
  TOWER_MAX_BUDGET,
  TOWER_SPAWN_PAUSE_MS,
  TowerRateLimitService,
} from '#/features/tower/towerRateLimitService';

describe('TowerRateLimitService', () => {
  let disposables: DisposableStore;
  let ix: TestInstantiationService;
  let now: number;

  beforeEach(() => {
    now = 1_000_000;
    disposables = new DisposableStore();
    ix = disposables.add(new TestInstantiationService());
    ix.set(ITowerRateLimitService, new SyncDescriptor(TowerRateLimitService, [() => now]));
  });
  afterEach(() => disposables.dispose());

  function snapshot(): TowerRateLimitSnapshot {
    return ix.get(ITowerRateLimitService).snapshot();
  }

  it('starts uncapped at the tower max budget with no pause', () => {
    expect(snapshot()).toEqual({ budget: TOWER_MAX_BUDGET, inflight: 0, blockedUntil: null });
  });

  it('acquire / release track inflight agents', () => {
    const limiter = ix.get(ITowerRateLimitService);

    expect(limiter.acquire()).toEqual({ ok: true });
    expect(limiter.acquire()).toEqual({ ok: true });
    expect(snapshot().inflight).toBe(2);

    limiter.release();
    expect(snapshot().inflight).toBe(1);

    limiter.release();
    limiter.release();
    expect(snapshot().inflight).toBe(0);
  });

  it('anchors the budget to (inflight - 1) on the first 429 and pauses spawns', () => {
    const limiter = ix.get(ITowerRateLimitService);
    expect(limiter.acquire().ok).toBe(true);
    expect(limiter.acquire().ok).toBe(true);
    expect(limiter.acquire().ok).toBe(true);

    limiter.reportRateLimited();

    expect(snapshot()).toEqual({
      budget: 2,
      inflight: 3,
      blockedUntil: now + TOWER_SPAWN_PAUSE_MS,
    });
  });

  it('refuses new spawns while the post-429 pause is in effect', () => {
    const limiter = ix.get(ITowerRateLimitService);
    limiter.reportRateLimited();

    const gate = limiter.acquire();
    expect(gate.ok).toBe(false);
    if (gate.ok) throw new Error('expected acquire to be refused');
    expect(gate.reason).toContain('new tower spawns paused');

    now += TOWER_SPAWN_PAUSE_MS;
    expect(limiter.acquire()).toEqual({ ok: true });
  });

  it('lifts the spawn pause early once a request succeeds', () => {
    const limiter = ix.get(ITowerRateLimitService);
    limiter.reportRateLimited();
    expect(snapshot().blockedUntil).not.toBeNull();

    limiter.reportSuccess();

    expect(snapshot().blockedUntil).toBeNull();
    expect(limiter.acquire()).toEqual({ ok: true });
  });

  it('refuses spawns when the inflight count reaches the budget', () => {
    const limiter = ix.get(ITowerRateLimitService);
    expect(limiter.acquire().ok).toBe(true);

    limiter.reportRateLimited();
    now += TOWER_SPAWN_PAUSE_MS;

    const gate = limiter.acquire();
    expect(gate.ok).toBe(false);
    if (gate.ok) throw new Error('expected acquire to be refused');
    expect(gate.reason).toContain('tower concurrency budget exhausted (1/1');

    limiter.release();
    expect(limiter.acquire()).toEqual({ ok: true });
  });

  it('recovers capacity by one per quiet window without 429s', () => {
    const limiter = ix.get(ITowerRateLimitService);
    expect(limiter.acquire().ok).toBe(true);
    expect(limiter.acquire().ok).toBe(true);
    limiter.reportRateLimited();
    expect(snapshot().budget).toBe(1);

    now += RATE_LIMIT_CAPACITY_RECOVERY_INTERVAL_MS;
    expect(limiter.budget()).toBe(2);

    now += RATE_LIMIT_CAPACITY_RECOVERY_INTERVAL_MS;
    expect(limiter.budget()).toBe(3);
  });

  it('never recovers beyond the tower max budget', () => {
    const limiter = ix.get(ITowerRateLimitService);
    expect(limiter.acquire().ok).toBe(true);
    limiter.reportRateLimited();

    for (let i = 0; i < TOWER_MAX_BUDGET + 4; i += 1) {
      now += RATE_LIMIT_CAPACITY_RECOVERY_INTERVAL_MS;
      limiter.budget();
    }

    expect(snapshot().budget).toBe(TOWER_MAX_BUDGET);
  });

  it('reset restores the pristine state', () => {
    const limiter = ix.get(ITowerRateLimitService);
    expect(limiter.acquire().ok).toBe(true);
    limiter.reportRateLimited();

    limiter.reset();

    expect(snapshot()).toEqual({ budget: TOWER_MAX_BUDGET, inflight: 0, blockedUntil: null });
  });
});