switchroom 0.12.16 → 0.12.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ decideProactiveCompact,
4
+ initialCompactState,
5
+ COMPACT_COOLDOWN_TURNS,
6
+ COMPACT_REARM_FRACTION,
7
+ type CompactState,
8
+ } from '../gateway/proactive-compact.js'
9
+
10
+ const CAP = 190_000
11
+
12
+ // Drive the state machine over a sequence of occupancy readings,
13
+ // returning the index of every evaluation that fired.
14
+ function run(occ: number[], start: CompactState = initialCompactState()) {
15
+ let state = start
16
+ const fires: number[] = []
17
+ occ.forEach((o, i) => {
18
+ const d = decideProactiveCompact(state, o, CAP)
19
+ state = d.state
20
+ if (d.fire) fires.push(i)
21
+ })
22
+ return { state, fires }
23
+ }
24
+
25
+ describe('decideProactiveCompact', () => {
26
+ it('does not fire below the cap', () => {
27
+ const { fires } = run([0, 50_000, 150_000, CAP - 1])
28
+ expect(fires).toEqual([])
29
+ })
30
+
31
+ it('fires exactly once when occupancy reaches the cap, then disarms', () => {
32
+ // Stays high after the fire (compaction has not landed yet).
33
+ const { fires } = run([CAP, CAP, CAP])
34
+ expect(fires).toEqual([0])
35
+ })
36
+
37
+ it('fires at occupancy strictly above the cap too', () => {
38
+ const { fires } = run([CAP + 25_000])
39
+ expect(fires).toEqual([0])
40
+ })
41
+
42
+ it('burns exactly COMPACT_COOLDOWN_TURNS idle evals after a fire before re-considering', () => {
43
+ // Fire at 0, then occupancy stays pegged high. The cooldown must
44
+ // swallow the next COMPACT_COOLDOWN_TURNS evals with no fire, and
45
+ // because it is still above the re-arm band it never re-arms ->
46
+ // never fires again. (Livelock guard.)
47
+ const seq = new Array(1 + COMPACT_COOLDOWN_TURNS + 5).fill(CAP)
48
+ const { fires } = run(seq)
49
+ expect(fires).toEqual([0])
50
+ })
51
+
52
+ it('re-arms only after occupancy falls below REARM_FRACTION × cap, never on the arming pass', () => {
53
+ const lowBand = CAP * COMPACT_REARM_FRACTION
54
+ // fire(0) -> cooldown(1..3) -> still high(4) stays disarmed ->
55
+ // drop just below band(5): arms but does NOT fire same pass ->
56
+ // climb back to cap(6): fires again.
57
+ const seq = [
58
+ CAP, // 0 fire
59
+ CAP, // 1 cooldown
60
+ CAP, // 2 cooldown
61
+ CAP, // 3 cooldown
62
+ CAP, // 4 disarmed, above band -> hold
63
+ lowBand - 1, // 5 re-arm, must NOT fire here
64
+ CAP, // 6 armed + at cap -> fire
65
+ ]
66
+ const { fires } = run(seq)
67
+ expect(fires).toEqual([0, 6])
68
+ })
69
+
70
+ it('does not re-arm if occupancy only drops to the band but not below it', () => {
71
+ const lowBand = CAP * COMPACT_REARM_FRACTION
72
+ // After cooldown, occupancy sits exactly at the band (not strictly
73
+ // below) forever -> never re-arms -> only the first fire.
74
+ const seq = [CAP, CAP, CAP, CAP, lowBand, lowBand, lowBand, CAP, CAP]
75
+ const { fires } = run(seq)
76
+ expect(fires).toEqual([0])
77
+ })
78
+
79
+ it('full healthy cycle: fire, compaction shrinks context, climbs again, fires again', () => {
80
+ const seq = [
81
+ 120_000, // below cap
82
+ CAP, // fire (idx 1)
83
+ 30_000, // cooldown 1 (post-compact, small)
84
+ 35_000, // cooldown 2
85
+ 40_000, // cooldown 3
86
+ 45_000, // disarmed, below band -> re-arm (no fire)
87
+ 90_000, // armed, below cap -> hold
88
+ CAP + 5_000, // fire again (idx 7)
89
+ ]
90
+ const { fires } = run(seq)
91
+ expect(fires).toEqual([1, 7])
92
+ })
93
+
94
+ it('never fires twice in immediate succession even with no cooldown left if still disarmed', () => {
95
+ // Construct a state that is past cooldown but disarmed, occupancy
96
+ // pegged at cap: must hold (not fire) until it drops below band.
97
+ const stuck: CompactState = { armed: false, cooldownTurns: 0 }
98
+ const { fires } = run([CAP, CAP, CAP, CAP], stuck)
99
+ expect(fires).toEqual([])
100
+ })
101
+ })