switchroom 0.18.15 → 0.18.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.
Files changed (76) hide show
  1. package/dist/agent-scheduler/index.js +16 -0
  2. package/dist/auth-broker/index.js +445 -10
  3. package/dist/cli/notion-write-pretool.mjs +16 -0
  4. package/dist/cli/switchroom.js +654 -479
  5. package/dist/host-control/main.js +20 -1
  6. package/dist/vault/approvals/kernel-server.js +16 -0
  7. package/dist/vault/broker/server.js +16 -0
  8. package/package.json +1 -1
  9. package/profiles/_base/start.sh.hbs +81 -139
  10. package/telegram-plugin/bridge/bridge.ts +7 -1
  11. package/telegram-plugin/dist/bridge/bridge.js +26 -1
  12. package/telegram-plugin/dist/gateway/gateway.js +1758 -661
  13. package/telegram-plugin/dist/server.js +26 -1
  14. package/telegram-plugin/draft-stream.ts +78 -3
  15. package/telegram-plugin/fleet-fallback-resume.ts +26 -3
  16. package/telegram-plugin/gateway/approval-hold.ts +49 -0
  17. package/telegram-plugin/gateway/bridge-dead-watchdog.ts +64 -22
  18. package/telegram-plugin/gateway/effort-command.ts +9 -7
  19. package/telegram-plugin/gateway/gateway.ts +627 -291
  20. package/telegram-plugin/gateway/linear-activity.ts +20 -4
  21. package/telegram-plugin/gateway/litellm-local-notice-wiring.ts +200 -0
  22. package/telegram-plugin/gateway/model-command.ts +96 -18
  23. package/telegram-plugin/gateway/pending-session-command.ts +10 -8
  24. package/telegram-plugin/gateway/premium-recovery-wiring.ts +122 -0
  25. package/telegram-plugin/gateway/session-model-file.ts +141 -172
  26. package/telegram-plugin/gateway/tier-downgrade-wiring.ts +121 -0
  27. package/telegram-plugin/gateway/unhandled-rejection-policy.ts +14 -1
  28. package/telegram-plugin/litellm-local-notice.ts +189 -0
  29. package/telegram-plugin/llm-error-present.ts +436 -0
  30. package/telegram-plugin/operator-events.ts +7 -1
  31. package/telegram-plugin/permission-title.ts +172 -10
  32. package/telegram-plugin/premium-recovery.ts +101 -0
  33. package/telegram-plugin/quota-watch.ts +16 -4
  34. package/telegram-plugin/raw-error-scrub.ts +73 -0
  35. package/telegram-plugin/retry-api-call.ts +8 -2
  36. package/telegram-plugin/runtime-metrics.ts +16 -0
  37. package/telegram-plugin/send-gate-degraded.test.ts +161 -8
  38. package/telegram-plugin/send-gate-observability.test.ts +140 -0
  39. package/telegram-plugin/send-gate-observability.ts +65 -20
  40. package/telegram-plugin/send-gate.test.ts +143 -1
  41. package/telegram-plugin/send-gate.ts +246 -23
  42. package/telegram-plugin/session-tail.ts +16 -0
  43. package/telegram-plugin/shared/local-time.ts +69 -0
  44. package/telegram-plugin/stream-controller.ts +143 -20
  45. package/telegram-plugin/stream-reply-handler.ts +12 -2
  46. package/telegram-plugin/tests/approval-hold-harness.ts +6 -6
  47. package/telegram-plugin/tests/approval-hold-outcome.test.ts +10 -2
  48. package/telegram-plugin/tests/bot-api.harness.ts +7 -2
  49. package/telegram-plugin/tests/bridge-dead-watchdog.test.ts +61 -0
  50. package/telegram-plugin/tests/draft-stream.test.ts +110 -1
  51. package/telegram-plugin/tests/effort-command.test.ts +4 -4
  52. package/telegram-plugin/tests/fleet-fallback-resume.test.ts +39 -0
  53. package/telegram-plugin/tests/flood-windows-persistence.test.ts +5 -4
  54. package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +33 -19
  55. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +47 -127
  56. package/telegram-plugin/tests/linear-create-issue.test.ts +30 -2
  57. package/telegram-plugin/tests/litellm-local-notice.test.ts +417 -0
  58. package/telegram-plugin/tests/llm-error-present.test.ts +380 -0
  59. package/telegram-plugin/tests/model-command.test.ts +84 -1
  60. package/telegram-plugin/tests/permission-title.test.ts +167 -4
  61. package/telegram-plugin/tests/premium-recovery-wiring.test.ts +150 -0
  62. package/telegram-plugin/tests/premium-recovery.test.ts +165 -0
  63. package/telegram-plugin/tests/quota-watch.test.ts +21 -0
  64. package/telegram-plugin/tests/reaction-gate-routing.test.ts +8 -3
  65. package/telegram-plugin/tests/retry-api-call.test.ts +21 -0
  66. package/telegram-plugin/tests/session-model-file.test.ts +7 -155
  67. package/telegram-plugin/tests/stream-controller-send-gate.test.ts +521 -0
  68. package/telegram-plugin/tests/stream-reply-handler.test.ts +44 -0
  69. package/telegram-plugin/tests/tier-downgrade-wiring.test.ts +165 -0
  70. package/telegram-plugin/tests/tier-downgrade.test.ts +141 -0
  71. package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +27 -1
  72. package/telegram-plugin/tests/worker-activity-feed.test.ts +212 -2
  73. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +492 -0
  74. package/telegram-plugin/tier-downgrade.ts +198 -0
  75. package/telegram-plugin/tool-activity-summary.ts +99 -0
  76. package/telegram-plugin/worker-activity-feed.ts +543 -368
@@ -0,0 +1,417 @@
1
+ /**
2
+ * Unit tests for the litellm-local 429 notice — the debounced "fleet token
3
+ * limiter engaged" message (telegram-plugin/litellm-local-notice.ts) and its
4
+ * side-effect runner (telegram-plugin/gateway/litellm-local-notice-wiring.ts).
5
+ *
6
+ * Pins the operator spec:
7
+ * - first litellm-local 429 → ONE calm notice
8
+ * - further 429s inside the cooldown window → suppressed, counted silently
9
+ * - first 429 after the window → notice carrying "throttled N more times
10
+ * since the last notice"
11
+ * - a notice only ever fires on an actual throttle event (evaluate-on-event,
12
+ * no timers — a quiet agent posts nothing)
13
+ * - never fires for non-litellm-local classifications
14
+ * - copy names the fleet limiter (LiteLLM tpm_limit/rpm_limit), explicitly
15
+ * NOT an Anthropic account limit, turn retries, no action needed
16
+ * - window is operator-tunable with a validated 15-min default
17
+ * - the window is armed / the metric emitted / "posted" logged only when a
18
+ * send was actually ISSUED (empty allowFrom or all-throwing sends retry)
19
+ * - the ordering contract with the shared per-kind card cooldown
20
+ * (decideRateLimitedSurface at the REAL shouldEmitOperatorEvent seam):
21
+ * litellm-local neither arms nor is suppressed by `${agent}:rate-limited`
22
+ *
23
+ * No real Date.now in the pure module — `now` is a parameter throughout
24
+ * (same injected-clock convention as throttle-tier.test.ts).
25
+ */
26
+
27
+ import { beforeEach, describe, it, expect } from 'vitest'
28
+ import {
29
+ evaluateLitellmLocalNotice,
30
+ initialLitellmLocalNoticeState,
31
+ isLitellmLocalNoticeEligible,
32
+ LITELLM_LOCAL_NOTICE_WINDOW_MS_DEFAULT,
33
+ parseLitellmNoticeWindowMs,
34
+ renderLitellmLocalNotice,
35
+ type LitellmLocalNoticeState,
36
+ } from '../litellm-local-notice.js'
37
+ import {
38
+ createLitellmLocalNoticeRunner,
39
+ decideRateLimitedSurface,
40
+ } from '../gateway/litellm-local-notice-wiring.js'
41
+ import { classify429Detail } from '../throttle-tier.js'
42
+ import { resetAllCooldowns, shouldEmitOperatorEvent } from '../operator-events.js'
43
+
44
+ const NOW = Date.UTC(2026, 6, 12, 8, 0, 0) // 2026-07-12T08:00:00Z
45
+ const WINDOW = LITELLM_LOCAL_NOTICE_WINDOW_MS_DEFAULT
46
+
47
+ // Verbatim litellm-proxy-local fixture (same provenance as
48
+ // throttle-tier.test.ts / litellmProxyLocal429Signals): the v1 parallel
49
+ // request limiter's wrapped exception.
50
+ const LITELLM_LOCAL_DETAIL =
51
+ 'litellm.RateLimitError: RateLimitError: Crossed TPM / RPM Limit. ' +
52
+ 'current tpm: 20112, tpm limit: 20000. Limit type: tpm. ' +
53
+ 'Rate limit exceeded for key. litellm rate limit handler'
54
+
55
+ const ACCOUNT_SCOPED_DETAIL =
56
+ "This request would exceed your account's rate limit. Your account will reset at 8:50am (UTC)."
57
+
58
+ // ─── Cooldown state machine ──────────────────────────────────────────────────
59
+
60
+ describe('evaluateLitellmLocalNotice — per-agent cooldown state machine', () => {
61
+ it('first 429 for an agent sends, with zero suppressed', () => {
62
+ const v = evaluateLitellmLocalNotice(initialLitellmLocalNoticeState(), 'clerk', NOW, WINDOW)
63
+ expect(v.send).toBe(true)
64
+ expect(v.suppressedSinceLastNotice).toBe(0)
65
+ expect(v.next.lastSentAtMsByAgent['clerk']).toBe(NOW)
66
+ expect(v.next.suppressedCountByAgent['clerk']).toBe(0)
67
+ })
68
+
69
+ it('second 429 inside the window is suppressed and counted', () => {
70
+ const first = evaluateLitellmLocalNotice(initialLitellmLocalNoticeState(), 'clerk', NOW, WINDOW)
71
+ const second = evaluateLitellmLocalNotice(first.next, 'clerk', NOW + 60_000, WINDOW)
72
+ expect(second.send).toBe(false)
73
+ expect(second.next.suppressedCountByAgent['clerk']).toBe(1)
74
+ const third = evaluateLitellmLocalNotice(second.next, 'clerk', NOW + 120_000, WINDOW)
75
+ expect(third.send).toBe(false)
76
+ expect(third.next.suppressedCountByAgent['clerk']).toBe(2)
77
+ // The last-sent stamp is NOT refreshed by suppressed events — the window
78
+ // anchors on the last NOTICE, so a sustained burst can't starve it.
79
+ expect(third.next.lastSentAtMsByAgent['clerk']).toBe(NOW)
80
+ })
81
+
82
+ it('first 429 after the window sends and carries the suppressed count, then resets', () => {
83
+ let state = evaluateLitellmLocalNotice(initialLitellmLocalNoticeState(), 'clerk', NOW, WINDOW).next
84
+ for (let i = 1; i <= 3; i++) {
85
+ state = evaluateLitellmLocalNotice(state, 'clerk', NOW + i * 60_000, WINDOW).next
86
+ }
87
+ const after = evaluateLitellmLocalNotice(state, 'clerk', NOW + WINDOW + 1, WINDOW)
88
+ expect(after.send).toBe(true)
89
+ expect(after.suppressedSinceLastNotice).toBe(3)
90
+ expect(after.next.suppressedCountByAgent['clerk']).toBe(0)
91
+ expect(after.next.lastSentAtMsByAgent['clerk']).toBe(NOW + WINDOW + 1)
92
+ })
93
+
94
+ it('window boundary: elapsed === windowMs sends; one ms earlier suppresses', () => {
95
+ const armed = evaluateLitellmLocalNotice(initialLitellmLocalNoticeState(), 'clerk', NOW, WINDOW).next
96
+ expect(evaluateLitellmLocalNotice(armed, 'clerk', NOW + WINDOW - 1, WINDOW).send).toBe(false)
97
+ expect(evaluateLitellmLocalNotice(armed, 'clerk', NOW + WINDOW, WINDOW).send).toBe(true)
98
+ })
99
+
100
+ it('agents debounce independently (per-agent window)', () => {
101
+ const a = evaluateLitellmLocalNotice(initialLitellmLocalNoticeState(), 'clerk', NOW, WINDOW)
102
+ const b = evaluateLitellmLocalNotice(a.next, 'gymbro', NOW + 1_000, WINDOW)
103
+ expect(b.send).toBe(true)
104
+ // clerk's window is untouched by gymbro's notice.
105
+ const c = evaluateLitellmLocalNotice(b.next, 'clerk', NOW + 2_000, WINDOW)
106
+ expect(c.send).toBe(false)
107
+ })
108
+
109
+ it('never mutates the previous state (pure)', () => {
110
+ const prev: LitellmLocalNoticeState = {
111
+ lastSentAtMsByAgent: { clerk: NOW },
112
+ suppressedCountByAgent: { clerk: 1 },
113
+ }
114
+ evaluateLitellmLocalNotice(prev, 'clerk', NOW + 1, WINDOW)
115
+ expect(prev.suppressedCountByAgent['clerk']).toBe(1)
116
+ evaluateLitellmLocalNotice(prev, 'clerk', NOW + WINDOW, WINDOW)
117
+ expect(prev.lastSentAtMsByAgent['clerk']).toBe(NOW)
118
+ expect(prev.suppressedCountByAgent['clerk']).toBe(1)
119
+ })
120
+ })
121
+
122
+ // ─── Config parsing ──────────────────────────────────────────────────────────
123
+
124
+ describe('parseLitellmNoticeWindowMs — tunable window with validated default', () => {
125
+ it('defaults to 15 minutes when unset', () => {
126
+ expect(LITELLM_LOCAL_NOTICE_WINDOW_MS_DEFAULT).toBe(15 * 60_000)
127
+ expect(parseLitellmNoticeWindowMs(undefined)).toBe(LITELLM_LOCAL_NOTICE_WINDOW_MS_DEFAULT)
128
+ expect(parseLitellmNoticeWindowMs(null)).toBe(LITELLM_LOCAL_NOTICE_WINDOW_MS_DEFAULT)
129
+ })
130
+
131
+ it('accepts a custom positive number', () => {
132
+ expect(parseLitellmNoticeWindowMs(5 * 60_000)).toBe(5 * 60_000)
133
+ expect(parseLitellmNoticeWindowMs(1)).toBe(1)
134
+ })
135
+
136
+ it('rejects invalid values back to the default (zero, negative, NaN, Infinity, non-number)', () => {
137
+ for (const bad of [0, -1, NaN, Infinity, -Infinity, '600000', {}, [], true]) {
138
+ expect(parseLitellmNoticeWindowMs(bad)).toBe(LITELLM_LOCAL_NOTICE_WINDOW_MS_DEFAULT)
139
+ }
140
+ })
141
+ })
142
+
143
+ // ─── Notice text ─────────────────────────────────────────────────────────────
144
+
145
+ describe('renderLitellmLocalNotice — copy contract', () => {
146
+ it('names the fleet limiter, negates the account reading, promises the retry', () => {
147
+ const text = renderLitellmLocalNotice({ agent: 'clerk', suppressedSinceLastNotice: 0 })
148
+ expect(text).toContain('Fleet token limiter')
149
+ expect(text).toContain('tpm_limit')
150
+ expect(text).toContain('rpm_limit')
151
+ expect(text).toContain('LiteLLM')
152
+ expect(text).toContain('not an Anthropic account limit')
153
+ expect(text).toContain('retries automatically')
154
+ expect(text).toContain('no action needed')
155
+ expect(text).toContain('**clerk**')
156
+ })
157
+
158
+ it('omits the suppressed line on a first notice, includes it after silent throttles', () => {
159
+ const first = renderLitellmLocalNotice({ agent: 'clerk', suppressedSinceLastNotice: 0 })
160
+ expect(first).not.toContain('since the last notice')
161
+ const later = renderLitellmLocalNotice({ agent: 'clerk', suppressedSinceLastNotice: 4 })
162
+ expect(later).toContain('Throttled 4 more times since the last notice.')
163
+ })
164
+
165
+ it('pluralizes the suppressed line correctly for exactly one silent throttle', () => {
166
+ const text = renderLitellmLocalNotice({ agent: 'clerk', suppressedSinceLastNotice: 1 })
167
+ expect(text).toContain('Throttled 1 more time since the last notice.')
168
+ expect(text).not.toContain('1 more times')
169
+ })
170
+
171
+ it('escapes markdown metacharacters in the agent name', () => {
172
+ const text = renderLitellmLocalNotice({ agent: 'a_b*c', suppressedSinceLastNotice: 0 })
173
+ expect(text).toContain('a\\_b\\*c')
174
+ expect(text).not.toContain('**a_b*c**')
175
+ })
176
+ })
177
+
178
+ // ─── Classification guard ────────────────────────────────────────────────────
179
+
180
+ describe('isLitellmLocalNoticeEligible — only litellm-local qualifies', () => {
181
+ it('accepts litellm-local and rejects everything else', () => {
182
+ expect(isLitellmLocalNoticeEligible('litellm-local')).toBe(true)
183
+ expect(isLitellmLocalNoticeEligible('account-scoped')).toBe(false)
184
+ expect(isLitellmLocalNoticeEligible('generic-transient')).toBe(false)
185
+ expect(isLitellmLocalNoticeEligible(null)).toBe(false)
186
+ expect(isLitellmLocalNoticeEligible(undefined)).toBe(false)
187
+ })
188
+
189
+ it('agrees with classify429Detail on real fixtures', () => {
190
+ expect(isLitellmLocalNoticeEligible(classify429Detail(LITELLM_LOCAL_DETAIL))).toBe(true)
191
+ expect(isLitellmLocalNoticeEligible(classify429Detail(ACCOUNT_SCOPED_DETAIL))).toBe(false)
192
+ expect(isLitellmLocalNoticeEligible(classify429Detail('overloaded_error'))).toBe(false)
193
+ })
194
+ })
195
+
196
+ // ─── Surface decision — the shared card-cooldown seam ────────────────────────
197
+
198
+ describe('decideRateLimitedSurface — ordering contract with shouldEmitOperatorEvent', () => {
199
+ // These run against the REAL shared cooldown gate (operator-events.ts), so
200
+ // they pin the gateway behavior change: litellm-local resolves before the
201
+ // gate — it neither ARMS `${agent}:rate-limited` nor is SUPPRESSED by it.
202
+ beforeEach(() => {
203
+ resetAllCooldowns()
204
+ })
205
+
206
+ const realGate = (a: string) => shouldEmitOperatorEvent(a, 'rate-limited')
207
+
208
+ it('litellm-local does NOT arm the shared rate-limited cooldown', () => {
209
+ expect(
210
+ decideRateLimitedSurface({
211
+ classification: 'litellm-local',
212
+ agent: 'seam-a',
213
+ shouldEmitCard: realGate,
214
+ }),
215
+ ).toBe('litellm-local-notice')
216
+ // If litellm-local had consulted (and armed) the gate, this immediate
217
+ // generic-transient would read 'cooldown-suppressed'. It must get its card.
218
+ expect(
219
+ decideRateLimitedSurface({
220
+ classification: 'generic-transient',
221
+ agent: 'seam-a',
222
+ shouldEmitCard: realGate,
223
+ }),
224
+ ).toBe('generic-card')
225
+ })
226
+
227
+ it('litellm-local is NOT suppressed by a cooldown a recent generic/529 card armed', () => {
228
+ // A generic transient (e.g. a 529 card) arms the shared cooldown…
229
+ expect(shouldEmitOperatorEvent('seam-b', 'rate-limited')).toBe(true)
230
+ expect(shouldEmitOperatorEvent('seam-b', 'rate-limited')).toBe(false) // armed
231
+ // …but the litellm-local notice still resolves (its own debounce governs).
232
+ expect(
233
+ decideRateLimitedSurface({
234
+ classification: 'litellm-local',
235
+ agent: 'seam-b',
236
+ shouldEmitCard: realGate,
237
+ }),
238
+ ).toBe('litellm-local-notice')
239
+ })
240
+
241
+ it('generic-transient consults (and arms) the gate exactly once per decide', () => {
242
+ expect(
243
+ decideRateLimitedSurface({
244
+ classification: 'generic-transient',
245
+ agent: 'seam-c',
246
+ shouldEmitCard: realGate,
247
+ }),
248
+ ).toBe('generic-card')
249
+ // The first decide armed the gate — a burst sibling is suppressed.
250
+ expect(
251
+ decideRateLimitedSurface({
252
+ classification: 'generic-transient',
253
+ agent: 'seam-c',
254
+ shouldEmitCard: realGate,
255
+ }),
256
+ ).toBe('cooldown-suppressed')
257
+ })
258
+ })
259
+
260
+ // ─── Wiring runner ───────────────────────────────────────────────────────────
261
+
262
+ interface Harness {
263
+ runner: ReturnType<typeof createLitellmLocalNoticeRunner>
264
+ sends: Array<{ chatId: string | number; markdown: string }>
265
+ metrics: unknown[]
266
+ logs: string[]
267
+ clock: { now: number }
268
+ window: { ms: number }
269
+ chats: Array<string | number>
270
+ }
271
+
272
+ function makeHarness(opts?: { chats?: Array<string | number> }): Harness {
273
+ const sends: Array<{ chatId: string | number; markdown: string }> = []
274
+ const metrics: unknown[] = []
275
+ const logs: string[] = []
276
+ const clock = { now: NOW }
277
+ const window = { ms: WINDOW }
278
+ const chats: Array<string | number> = opts?.chats ?? ['111', '222']
279
+ const runner = createLitellmLocalNoticeRunner({
280
+ listNoticeChats: () => chats,
281
+ sendNotice: (chatId, markdown) => sends.push({ chatId, markdown }),
282
+ windowMs: () => window.ms,
283
+ emitMetric: (ev) => metrics.push(ev),
284
+ log: (m) => logs.push(m),
285
+ now: () => clock.now,
286
+ })
287
+ return { runner, sends, metrics, logs, clock, window, chats }
288
+ }
289
+
290
+ describe('createLitellmLocalNoticeRunner — side-effect sequencing', () => {
291
+ it('first litellm-local 429 broadcasts one notice to every authorized chat + one metric, outcome sent', () => {
292
+ const h = makeHarness()
293
+ expect(h.runner.onRateLimited('litellm-local', 'clerk')).toBe('sent')
294
+ expect(h.sends.map((s) => s.chatId)).toEqual(['111', '222'])
295
+ expect(h.sends[0].markdown).toContain('Fleet token limiter')
296
+ // Full metric object — also pins buildLitellmLocalNoticeMetric's shape.
297
+ expect(h.metrics).toEqual([
298
+ { kind: 'litellm_local_429_notice', agent: 'clerk', suppressed_count: 0, window_ms: WINDOW },
299
+ ])
300
+ })
301
+
302
+ it('suppresses inside the window; elapsed time alone never sends; the next event reports the count', () => {
303
+ const h = makeHarness()
304
+ expect(h.runner.onRateLimited('litellm-local', 'clerk')).toBe('sent')
305
+ h.clock.now = NOW + 60_000
306
+ expect(h.runner.onRateLimited('litellm-local', 'clerk')).toBe('suppressed')
307
+ h.clock.now = NOW + 120_000
308
+ expect(h.runner.onRateLimited('litellm-local', 'clerk')).toBe('suppressed')
309
+ // Two suppressed events: still just the first broadcast, one metric.
310
+ expect(h.sends).toHaveLength(2) // 2 chats × 1 notice
311
+ expect(h.metrics).toHaveLength(1)
312
+ expect(h.logs.some((l) => l.includes('suppressed (cooldown)'))).toBe(true)
313
+ // A quiet stretch — the window expires, then MULTIPLE further windows
314
+ // pass with zero throttle events. Notices are evaluate-on-event: nothing
315
+ // may send or emit on elapsed time alone.
316
+ h.clock.now = NOW + 10 * WINDOW
317
+ expect(h.sends).toHaveLength(2)
318
+ expect(h.metrics).toHaveLength(1)
319
+ // The next ACTUAL event (long past expiry) posts, carrying the count of
320
+ // the two silently absorbed throttles.
321
+ expect(h.runner.onRateLimited('litellm-local', 'clerk')).toBe('sent')
322
+ expect(h.sends).toHaveLength(4)
323
+ expect(h.sends[2].markdown).toContain('Throttled 2 more times since the last notice.')
324
+ expect(h.metrics[1]).toMatchObject({ suppressed_count: 2 })
325
+ })
326
+
327
+ it('never fires for non-litellm-local classifications (outcome skipped, state untouched)', () => {
328
+ const h = makeHarness()
329
+ expect(h.runner.onRateLimited('account-scoped', 'clerk')).toBe('skipped')
330
+ expect(h.runner.onRateLimited('generic-transient', 'clerk')).toBe('skipped')
331
+ expect(h.sends).toHaveLength(0)
332
+ expect(h.metrics).toHaveLength(0)
333
+ // Non-eligible events must not perturb the debounce state either: the
334
+ // next real litellm-local 429 is still a FIRST notice.
335
+ expect(h.runner.onRateLimited('litellm-local', 'clerk')).toBe('sent')
336
+ expect(h.sends).toHaveLength(2)
337
+ expect(h.sends[0].markdown).not.toContain('since the last notice')
338
+ })
339
+
340
+ it('re-reads the tunable window per event (operator retune takes effect live)', () => {
341
+ const h = makeHarness({ chats: ['111'] })
342
+ h.window.ms = 5 * 60_000
343
+ h.runner.onRateLimited('litellm-local', 'clerk')
344
+ h.clock.now = NOW + 5 * 60_000
345
+ h.runner.onRateLimited('litellm-local', 'clerk')
346
+ expect(h.sends).toHaveLength(2)
347
+ expect(h.metrics[1]).toMatchObject({ window_ms: 5 * 60_000 })
348
+ })
349
+
350
+ it('empty allowFrom: no metric, no "posted" log, window NOT armed — a later event with chats sends', () => {
351
+ const h = makeHarness({ chats: [] })
352
+ expect(h.runner.onRateLimited('litellm-local', 'clerk')).toBe('skipped')
353
+ expect(h.sends).toHaveLength(0)
354
+ expect(h.metrics).toHaveLength(0)
355
+ expect(h.logs.some((l) => l.includes('posted'))).toBe(false)
356
+ expect(h.logs.some((l) => l.includes('no sends issued'))).toBe(true)
357
+ // A chat becomes authorized — the very next event delivers even though
358
+ // it is INSIDE what would have been the window (the window was never
359
+ // armed for a notice nobody could hear).
360
+ h.chats.push('111')
361
+ h.clock.now = NOW + 1_000
362
+ expect(h.runner.onRateLimited('litellm-local', 'clerk')).toBe('sent')
363
+ expect(h.sends).toHaveLength(1)
364
+ expect(h.metrics).toHaveLength(1)
365
+ })
366
+
367
+ it('all-throwing sends: outcome skipped, window not armed, and the caller never sees a throw', () => {
368
+ const sends: string[] = []
369
+ const logs: string[] = []
370
+ const clock = { now: NOW }
371
+ let shouldThrow = true
372
+ const runner = createLitellmLocalNoticeRunner({
373
+ listNoticeChats: () => ['111'],
374
+ sendNotice: (_chatId, markdown) => {
375
+ if (shouldThrow) throw new Error('boom')
376
+ sends.push(markdown)
377
+ },
378
+ windowMs: () => WINDOW,
379
+ emitMetric: () => {
380
+ if (shouldThrow) {
381
+ throw new Error('metric sink must not be reached when nothing was issued')
382
+ }
383
+ },
384
+ log: (m) => logs.push(m),
385
+ now: () => clock.now,
386
+ })
387
+ expect(() => runner.onRateLimited('litellm-local', 'clerk')).not.toThrow()
388
+ expect(runner.inspect().state.lastSentAtMsByAgent['clerk']).toBeUndefined()
389
+ expect(logs.some((l) => l.includes('send failed') && l.includes('boom'))).toBe(true)
390
+ expect(logs.some((l) => l.includes('no sends issued'))).toBe(true)
391
+ // Window was NOT armed by the failed attempt: the immediate next event
392
+ // (well inside 15 min) retries and delivers.
393
+ shouldThrow = false
394
+ clock.now = NOW + 1_000
395
+ expect(runner.onRateLimited('litellm-local', 'clerk')).toBe('sent')
396
+ expect(sends).toHaveLength(1)
397
+ })
398
+
399
+ it('a throwing log sink cannot void the never-throws contract', () => {
400
+ const runner = createLitellmLocalNoticeRunner({
401
+ listNoticeChats: () => {
402
+ throw new Error('chats exploded')
403
+ },
404
+ sendNotice: () => {},
405
+ windowMs: () => WINDOW,
406
+ emitMetric: () => {},
407
+ log: () => {
408
+ throw new Error('stderr sink died')
409
+ },
410
+ now: () => NOW,
411
+ })
412
+ // listNoticeChats throws → outer catch → deps.log throws too → still no
413
+ // throw escapes to the operator-event path.
414
+ expect(() => runner.onRateLimited('litellm-local', 'clerk')).not.toThrow()
415
+ expect(runner.onRateLimited('litellm-local', 'clerk')).toBe('skipped')
416
+ })
417
+ })