switchroom 0.19.26 → 0.19.28
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.
- package/bin/git-agent-attribution-hook.sh +144 -0
- package/dist/agent-scheduler/index.js +60 -2
- package/dist/auth-broker/index.js +244 -13
- package/dist/cli/autoaccept-poll.js +225 -17
- package/dist/cli/notion-write-pretool.mjs +60 -2
- package/dist/cli/switchroom.js +2843 -1220
- package/dist/host-control/main.js +245 -14
- package/dist/vault/approvals/kernel-server.js +242 -13
- package/dist/vault/broker/server.js +242 -13
- package/package.json +7 -2
- package/profiles/_base/cron-session.sh.hbs +8 -0
- package/profiles/_base/start.sh.hbs +175 -15
- package/telegram-plugin/card-layout.ts +328 -0
- package/telegram-plugin/dist/bridge/bridge.js +94 -1
- package/telegram-plugin/dist/gateway/gateway.js +2544 -1182
- package/telegram-plugin/dist/server.js +97 -1
- package/telegram-plugin/edit-flood-fuse.ts +841 -57
- package/telegram-plugin/flood-429-ledger.ts +526 -0
- package/telegram-plugin/flood-circuit-breaker.ts +18 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +6 -0
- package/telegram-plugin/gateway/flood-reply-queue.ts +168 -0
- package/telegram-plugin/gateway/gateway.ts +67 -70
- package/telegram-plugin/gateway/mcp-failure-hook.ts +74 -0
- package/telegram-plugin/gateway/narrative-lane.ts +14 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +36 -0
- package/telegram-plugin/gateway/outbox-sweep.ts +183 -6
- package/telegram-plugin/gateway/pinned-message-handler.ts +12 -16
- package/telegram-plugin/gateway/status-pin-retarget.ts +72 -36
- package/telegram-plugin/gateway/status-pin-store.ts +58 -9
- package/telegram-plugin/gateway/worker-pin-reaper.ts +56 -7
- package/telegram-plugin/inline-keyboard-callbacks.ts +202 -21
- package/telegram-plugin/llm-error-present.ts +61 -2
- package/telegram-plugin/mcp-credential-failure.ts +459 -0
- package/telegram-plugin/model-unavailable.ts +8 -0
- package/telegram-plugin/operator-events.ts +110 -5
- package/telegram-plugin/outbound-class.ts +81 -0
- package/telegram-plugin/provider-credit.ts +237 -0
- package/telegram-plugin/scripts/bun-test-ci.sh +36 -6
- package/telegram-plugin/send-gate.ts +24 -2
- package/telegram-plugin/status-no-truncate.ts +10 -48
- package/telegram-plugin/status-pin-driver.ts +33 -45
- package/telegram-plugin/status-pin.ts +18 -1
- package/telegram-plugin/tests/card-golden.test.ts +69 -0
- package/telegram-plugin/tests/card-lifecycle-render.test.ts +362 -0
- package/telegram-plugin/tests/card-type-distinguishability.test.ts +187 -164
- package/telegram-plugin/tests/card-variants.golden.txt +211 -0
- package/telegram-plugin/tests/card-variants.ts +366 -0
- package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +373 -0
- package/telegram-plugin/tests/edit-flood-fuse-default-deny.test.ts +319 -0
- package/telegram-plugin/tests/edit-flood-fuse-reply-reserve.test.ts +340 -0
- package/telegram-plugin/tests/edit-flood-fuse.test.ts +11 -2
- package/telegram-plugin/tests/feed-edit-rate-ceiling.test.ts +462 -0
- package/telegram-plugin/tests/finalize-callback-flood-policy.test.ts +298 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +41 -8
- package/telegram-plugin/tests/fixtures/real-429-stream.ts +220 -0
- package/telegram-plugin/tests/flood-429-ledger.test.ts +278 -0
- package/telegram-plugin/tests/flood-429-recorder-wiring.test.ts +128 -0
- package/telegram-plugin/tests/flood-reply-queue.test.ts +418 -0
- package/telegram-plugin/tests/mcp-credential-failure.test.ts +310 -0
- package/telegram-plugin/tests/outbox-sweep-flood-breaker.test.ts +221 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +19 -24
- package/telegram-plugin/tests/pinned-message-handler.test.ts +15 -15
- package/telegram-plugin/tests/provider-credit-402.test.ts +243 -0
- package/telegram-plugin/tests/status-pin-api.test.ts +11 -11
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +36 -37
- package/telegram-plugin/tests/status-pin-lifecycle.test.ts +602 -0
- package/telegram-plugin/tests/status-pin-retarget.test.ts +90 -62
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +7 -3
- package/telegram-plugin/tests/status-pin-store.test.ts +109 -60
- package/telegram-plugin/tests/status-pin.test.ts +56 -5
- package/telegram-plugin/tests/test-runner-coverage.test.ts +133 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +12 -10
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +23 -29
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +56 -59
- package/telegram-plugin/tests/worker-feed-terminal-edit-class.test.ts +335 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
- package/telegram-plugin/tool-activity-summary.ts +239 -365
- package/telegram-plugin/uat/assertions.ts +22 -11
- package/telegram-plugin/uat/feed-matcher.test.ts +24 -17
- package/telegram-plugin/worker-activity-feed.ts +105 -47
- package/vendor/hindsight-memory/CLAUDE.md +45 -0
- package/vendor/hindsight-memory/scripts/drain_pending.py +433 -11
- package/vendor/hindsight-memory/scripts/lib/config.py +33 -0
- package/vendor/hindsight-memory/scripts/lib/pending.py +193 -28
- package/vendor/hindsight-memory/scripts/recall.py +176 -7
- package/vendor/hindsight-memory/scripts/tests/test_config_recall_passthrough_env.py +170 -0
- package/vendor/hindsight-memory/scripts/tests/test_drain_circuit_breaker.py +401 -0
- package/vendor/hindsight-memory/scripts/tests/test_drain_serialisation.py +286 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +817 -8
- package/vendor/hindsight-memory/scripts/tests/test_recall_min_score.py +464 -0
- package/vendor/hindsight-memory/settings.json +1 -1
- package/vendor/hindsight-memory/tests/test_hooks.py +11 -2
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outcome tests for #3891 — `finalizeCallback` must transit the ONE retry /
|
|
3
|
+
* flood policy, and a failed card repaint must not leave a live-looking
|
|
4
|
+
* keyboard.
|
|
5
|
+
*
|
|
6
|
+
* These are deliberately NOT unit tests of a helper. Each one drives the real
|
|
7
|
+
* `createRetryApiCall` + the real `makeFloodWaitRecorder` against a real temp
|
|
8
|
+
* state dir and asserts the observable artefact an operator or `switchroom
|
|
9
|
+
* doctor` would later read:
|
|
10
|
+
*
|
|
11
|
+
* - `429-ledger.json` gains an episode for a 429 earned by a BUTTON TAP.
|
|
12
|
+
* - `flood-wait.json` gains the window.
|
|
13
|
+
* - after a repaint that fails post-retry, the card's keyboard is gone
|
|
14
|
+
* (or, if it cannot be removed, the operator is told in-channel).
|
|
15
|
+
*
|
|
16
|
+
* Pre-fix (both legs calling `ctx.*` raw) every one of these fails: the
|
|
17
|
+
* ledger file is never created, and the keyboard survives untouched.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
21
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs'
|
|
22
|
+
import { tmpdir } from 'node:os'
|
|
23
|
+
import { join } from 'node:path'
|
|
24
|
+
import { GrammyError } from 'grammy'
|
|
25
|
+
import {
|
|
26
|
+
finalizeCallback,
|
|
27
|
+
DEAD_CARD_NOTICE,
|
|
28
|
+
type FinalizeCallbackContext,
|
|
29
|
+
type FinalizeApiCall,
|
|
30
|
+
} from '../inline-keyboard-callbacks.js'
|
|
31
|
+
import { createRetryApiCall } from '../retry-api-call.js'
|
|
32
|
+
import { makeFloodWaitRecorder, makeFloodWaitProbe } from '../flood-circuit-breaker.js'
|
|
33
|
+
import {
|
|
34
|
+
readFlood429Ledger,
|
|
35
|
+
flood429LedgerPathFromFloodState,
|
|
36
|
+
} from '../flood-429-ledger.js'
|
|
37
|
+
|
|
38
|
+
/** Synthetic chat id — never a real operator chat (see check-no-pii-secrets). */
|
|
39
|
+
const CHAT_ID = -1009900112233
|
|
40
|
+
const MSG_ID = 4242
|
|
41
|
+
|
|
42
|
+
let stateDir: string
|
|
43
|
+
let floodStatePath: string
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
stateDir = mkdtempSync(join(tmpdir(), 'sr-3891-'))
|
|
47
|
+
floodStatePath = join(stateDir, 'flood-wait.json')
|
|
48
|
+
})
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
rmSync(stateDir, { recursive: true, force: true })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
/** A Telegram 429 exactly as grammy surfaces it. */
|
|
54
|
+
function flood429(retryAfter: number, method: string): GrammyError {
|
|
55
|
+
return new GrammyError(
|
|
56
|
+
`Call to '${method}' failed!`,
|
|
57
|
+
{ ok: false, error_code: 429, description: 'Too Many Requests: retry after ' + retryAfter, parameters: { retry_after: retryAfter } },
|
|
58
|
+
method,
|
|
59
|
+
{},
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A non-429, non-benign edit failure — the shape seen in the clerk incident. */
|
|
64
|
+
function hardEditFailure(method = 'editMessageText'): GrammyError {
|
|
65
|
+
return new GrammyError(
|
|
66
|
+
`Call to '${method}' failed!`,
|
|
67
|
+
{ ok: false, error_code: 400, description: "Bad Request: can't parse entities" },
|
|
68
|
+
method,
|
|
69
|
+
{},
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The production wiring, verbatim in shape: `createRetryApiCall` with BOTH
|
|
75
|
+
* breaker hooks, writing to a temp state dir. No sleeping (fake sleep) so the
|
|
76
|
+
* flood-wait retry path runs at test speed.
|
|
77
|
+
*/
|
|
78
|
+
function realPolicy(): FinalizeApiCall {
|
|
79
|
+
const record = makeFloodWaitRecorder(floodStatePath)
|
|
80
|
+
const probe = makeFloodWaitProbe(floodStatePath)
|
|
81
|
+
return createRetryApiCall({
|
|
82
|
+
log: () => {},
|
|
83
|
+
sleep: async () => {},
|
|
84
|
+
onFloodWait: (retryAfterSec) => record(retryAfterSec),
|
|
85
|
+
floodWaitRemainingMs: probe,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface Capture {
|
|
90
|
+
acks: number
|
|
91
|
+
edits: number
|
|
92
|
+
markupEdits: Array<Record<string, unknown>>
|
|
93
|
+
replies: string[]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mkCtx(cap: Capture, behaviour: {
|
|
97
|
+
ack?: () => void
|
|
98
|
+
edit?: () => void
|
|
99
|
+
markup?: () => void
|
|
100
|
+
reply?: () => void
|
|
101
|
+
} = {}): FinalizeCallbackContext {
|
|
102
|
+
return {
|
|
103
|
+
answerCallbackQuery: async () => { cap.acks++; behaviour.ack?.(); return true },
|
|
104
|
+
editMessageText: async () => { cap.edits++; behaviour.edit?.(); return { message_id: MSG_ID } },
|
|
105
|
+
editMessageReplyMarkup: async (opts) => {
|
|
106
|
+
cap.markupEdits.push(opts ?? {})
|
|
107
|
+
behaviour.markup?.()
|
|
108
|
+
return true
|
|
109
|
+
},
|
|
110
|
+
reply: async (text) => { cap.replies.push(text); behaviour.reply?.(); return { message_id: MSG_ID + 1 } },
|
|
111
|
+
callbackQuery: { message: { message_id: MSG_ID, chat: { id: CHAT_ID } } },
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const mkCap = (): Capture => ({ acks: 0, edits: 0, markupEdits: [], replies: [] })
|
|
116
|
+
|
|
117
|
+
describe('#3891 — tap-path 429s reach the flood ledger', () => {
|
|
118
|
+
it('records a 429 earned by answerCallbackQuery to the 429 ledger AND the window file', async () => {
|
|
119
|
+
const cap = mkCap()
|
|
120
|
+
let ackAttempts = 0
|
|
121
|
+
const ctx = mkCtx(cap, {
|
|
122
|
+
ack: () => { ackAttempts++; if (ackAttempts === 1) throw flood429(7, 'answerCallbackQuery') },
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
await finalizeCallback(ctx, {
|
|
126
|
+
ackText: 'Approved',
|
|
127
|
+
newText: 'resolved',
|
|
128
|
+
apiCall: realPolicy(),
|
|
129
|
+
log: () => {},
|
|
130
|
+
})
|
|
131
|
+
// The ack leg is fire-and-forget; let its retry chain settle.
|
|
132
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
133
|
+
|
|
134
|
+
const ledgerPath = flood429LedgerPathFromFloodState(floodStatePath)
|
|
135
|
+
expect(existsSync(ledgerPath)).toBe(true)
|
|
136
|
+
const episodes = readFlood429Ledger(ledgerPath)
|
|
137
|
+
expect(episodes).toHaveLength(1)
|
|
138
|
+
expect(episodes[0]?.peakRetryAfterSec).toBe(7)
|
|
139
|
+
expect(episodes[0]?.count).toBe(1)
|
|
140
|
+
// The window file the send gate / doctor / watchdog all read.
|
|
141
|
+
expect(existsSync(floodStatePath)).toBe(true)
|
|
142
|
+
// And the tap still succeeded on retry — the policy is not a silencer.
|
|
143
|
+
expect(ackAttempts).toBe(2)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('records a 429 earned by the card repaint, and the repaint still lands on retry', async () => {
|
|
147
|
+
const cap = mkCap()
|
|
148
|
+
let editAttempts = 0
|
|
149
|
+
const ctx = mkCtx(cap, {
|
|
150
|
+
edit: () => { editAttempts++; if (editAttempts === 1) throw flood429(11, 'editMessageText') },
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
await finalizeCallback(ctx, {
|
|
154
|
+
ackText: 'Approved',
|
|
155
|
+
newText: 'resolved',
|
|
156
|
+
apiCall: realPolicy(),
|
|
157
|
+
log: () => {},
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const episodes = readFlood429Ledger(flood429LedgerPathFromFloodState(floodStatePath))
|
|
161
|
+
expect(episodes).toHaveLength(1)
|
|
162
|
+
expect(episodes[0]?.peakRetryAfterSec).toBe(11)
|
|
163
|
+
expect(editAttempts).toBe(2)
|
|
164
|
+
// Repaint landed on the retry, so no ladder rung was needed.
|
|
165
|
+
expect(cap.markupEdits).toHaveLength(0)
|
|
166
|
+
expect(cap.replies).toHaveLength(0)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('scopes the flood window to the tapped chat, not `global`', async () => {
|
|
170
|
+
const seen: Array<{ chat_id?: string; verb?: string; priorityClass?: string }> = []
|
|
171
|
+
const apiCall: FinalizeApiCall = async (fn, opts) => {
|
|
172
|
+
seen.push({ chat_id: opts?.chat_id, verb: opts?.verb, priorityClass: opts?.priorityClass })
|
|
173
|
+
return fn()
|
|
174
|
+
}
|
|
175
|
+
const cap = mkCap()
|
|
176
|
+
await finalizeCallback(mkCtx(cap), { ackText: 'ok', newText: 'x', apiCall, log: () => {} })
|
|
177
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
178
|
+
|
|
179
|
+
expect(seen).toHaveLength(2)
|
|
180
|
+
for (const s of seen) {
|
|
181
|
+
expect(s.chat_id).toBe(String(CHAT_ID))
|
|
182
|
+
// `critical` is the one class the send gate never sheds. A finalize
|
|
183
|
+
// repaint is the operator's ONLY confirmation their tap resolved a
|
|
184
|
+
// human-in-the-loop decision; shedding it is the incident itself.
|
|
185
|
+
expect(s.priorityClass).toBe('critical')
|
|
186
|
+
}
|
|
187
|
+
expect(seen.map((s) => s.verb)).toEqual(['answerCallbackQuery', 'editMessageText'])
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('survives a callbackQuery with no chat (degrades to an unscoped window, never throws)', async () => {
|
|
191
|
+
const seen: Array<string | undefined> = []
|
|
192
|
+
const apiCall: FinalizeApiCall = async (fn, opts) => { seen.push(opts?.chat_id); return fn() }
|
|
193
|
+
const cap = mkCap()
|
|
194
|
+
const ctx = { ...mkCtx(cap), callbackQuery: undefined }
|
|
195
|
+
await expect(
|
|
196
|
+
finalizeCallback(ctx, { ackText: 'ok', newText: 'x', apiCall, log: () => {} }),
|
|
197
|
+
).resolves.toBeUndefined()
|
|
198
|
+
expect(seen.every((s) => s === undefined)).toBe(true)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('#3891 — a failed repaint never leaves a live-looking card', () => {
|
|
203
|
+
it('strips the keyboard when the body repaint fails post-retry', async () => {
|
|
204
|
+
const cap = mkCap()
|
|
205
|
+
const logs: string[] = []
|
|
206
|
+
const ctx = mkCtx(cap, { edit: () => { throw hardEditFailure() } })
|
|
207
|
+
|
|
208
|
+
await finalizeCallback(ctx, {
|
|
209
|
+
ackText: 'Approved',
|
|
210
|
+
newText: '**bad _markdown',
|
|
211
|
+
apiCall: realPolicy(),
|
|
212
|
+
log: (l) => logs.push(l),
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// THE outcome: the card is no longer tappable.
|
|
216
|
+
expect(cap.markupEdits).toHaveLength(1)
|
|
217
|
+
expect(cap.markupEdits[0]?.reply_markup).toEqual({ inline_keyboard: [] })
|
|
218
|
+
// Not silent, either.
|
|
219
|
+
expect(logs.some((l) => l.includes('editMessageText failed'))).toBe(true)
|
|
220
|
+
expect(logs.some((l) => l.includes('no longer tappable'))).toBe(true)
|
|
221
|
+
// No need to shout in-channel — rung 2 fixed it.
|
|
222
|
+
expect(cap.replies).toHaveLength(0)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('tells the operator in-channel when the keyboard cannot be stripped either', async () => {
|
|
226
|
+
const cap = mkCap()
|
|
227
|
+
const logs: string[] = []
|
|
228
|
+
const ctx = mkCtx(cap, {
|
|
229
|
+
edit: () => { throw hardEditFailure() },
|
|
230
|
+
markup: () => { throw hardEditFailure('editMessageReplyMarkup') },
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
await finalizeCallback(ctx, {
|
|
234
|
+
ackText: 'Approved',
|
|
235
|
+
newText: 'resolved',
|
|
236
|
+
apiCall: realPolicy(),
|
|
237
|
+
log: (l) => logs.push(l),
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
expect(cap.replies).toEqual([DEAD_CARD_NOTICE])
|
|
241
|
+
// The notice must name the buttons as stale — that is the whole point.
|
|
242
|
+
expect(DEAD_CARD_NOTICE).toContain('STALE')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('a repaint failure does NOT block the model wake-up (invariant 3 holds)', async () => {
|
|
246
|
+
const cap = mkCap()
|
|
247
|
+
let synthFired = false
|
|
248
|
+
const ctx = mkCtx(cap, {
|
|
249
|
+
edit: () => { throw hardEditFailure() },
|
|
250
|
+
markup: () => { throw hardEditFailure('editMessageReplyMarkup') },
|
|
251
|
+
reply: () => { throw hardEditFailure('sendMessage') },
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
await finalizeCallback(ctx, {
|
|
255
|
+
ackText: 'Approved',
|
|
256
|
+
newText: 'resolved',
|
|
257
|
+
apiCall: realPolicy(),
|
|
258
|
+
synthInbound: () => { synthFired = true },
|
|
259
|
+
log: () => {},
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
expect(synthFired).toBe(true)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('does NOT run the ladder when the repaint succeeds', async () => {
|
|
266
|
+
const cap = mkCap()
|
|
267
|
+
await finalizeCallback(mkCtx(cap), {
|
|
268
|
+
ackText: 'Approved',
|
|
269
|
+
newText: 'resolved',
|
|
270
|
+
apiCall: realPolicy(),
|
|
271
|
+
log: () => {},
|
|
272
|
+
})
|
|
273
|
+
expect(cap.edits).toBe(1)
|
|
274
|
+
expect(cap.markupEdits).toHaveLength(0)
|
|
275
|
+
expect(cap.replies).toHaveLength(0)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('treats the two BENIGN edit failures as done — the retry policy swallows them, no ladder', async () => {
|
|
279
|
+
// MESSAGE_TO_EDIT_NOT_FOUND: the operator deleted the card. There is no
|
|
280
|
+
// keyboard left to disarm, so shouting about a stale card would be noise.
|
|
281
|
+
const cap = mkCap()
|
|
282
|
+
const ctx = mkCtx(cap, {
|
|
283
|
+
edit: () => {
|
|
284
|
+
throw new GrammyError(
|
|
285
|
+
"Call to 'editMessageText' failed!",
|
|
286
|
+
{ ok: false, error_code: 400, description: 'Bad Request: message to edit not found' },
|
|
287
|
+
'editMessageText',
|
|
288
|
+
{},
|
|
289
|
+
)
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
await finalizeCallback(ctx, {
|
|
293
|
+
ackText: 'Approved', newText: 'resolved', apiCall: realPolicy(), log: () => {},
|
|
294
|
+
})
|
|
295
|
+
expect(cap.markupEdits).toHaveLength(0)
|
|
296
|
+
expect(cap.replies).toHaveLength(0)
|
|
297
|
+
})
|
|
298
|
+
})
|
|
@@ -16,6 +16,8 @@ import { finalizeCallback, type FinalizeCallbackContext } from '../inline-keyboa
|
|
|
16
16
|
interface Capture {
|
|
17
17
|
acks: Array<{ text?: string; show_alert?: boolean }>
|
|
18
18
|
edits: Array<{ text: string | { markdown: string }; opts: Record<string, unknown> }>
|
|
19
|
+
markupEdits: Array<Record<string, unknown>>
|
|
20
|
+
replies: string[]
|
|
19
21
|
ackThrows?: Error
|
|
20
22
|
editThrows?: Error
|
|
21
23
|
}
|
|
@@ -32,13 +34,30 @@ function mkCtx(cap: Capture): FinalizeCallbackContext {
|
|
|
32
34
|
if (cap.editThrows) throw cap.editThrows
|
|
33
35
|
return { message_id: 1 }
|
|
34
36
|
},
|
|
37
|
+
editMessageReplyMarkup: async (opts) => {
|
|
38
|
+
cap.markupEdits.push(opts ?? {})
|
|
39
|
+
return true
|
|
40
|
+
},
|
|
41
|
+
reply: async (text) => {
|
|
42
|
+
cap.replies.push(text)
|
|
43
|
+
return { message_id: 2 }
|
|
44
|
+
},
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Passthrough stand-in for the gateway's `robustApiCall` (#3891). The helper
|
|
50
|
+
* now REQUIRES the retry/flood seam; these three-invariant cases are about the
|
|
51
|
+
* invariants, not the policy, so they hand in a transparent one. The policy's
|
|
52
|
+
* own outcomes are pinned in `finalize-callback-flood-policy.test.ts`.
|
|
53
|
+
*/
|
|
54
|
+
const passthrough = <T>(fn: () => Promise<T>): Promise<T> => fn()
|
|
55
|
+
|
|
38
56
|
describe('finalizeCallback — three-invariant contract', () => {
|
|
39
57
|
it('invariant 1: acks the callback with the supplied toast text', async () => {
|
|
40
|
-
const cap: Capture = { acks: [], edits: [] }
|
|
58
|
+
const cap: Capture = { acks: [], edits: [], markupEdits: [], replies: [] }
|
|
41
59
|
await finalizeCallback(mkCtx(cap), {
|
|
60
|
+
apiCall: passthrough,
|
|
42
61
|
ackText: 'Approved',
|
|
43
62
|
newText: 'Original prompt\n\n✓ Approved by @op',
|
|
44
63
|
})
|
|
@@ -48,8 +67,9 @@ describe('finalizeCallback — three-invariant contract', () => {
|
|
|
48
67
|
})
|
|
49
68
|
|
|
50
69
|
it('invariant 1: alert=true renders as full modal (show_alert: true)', async () => {
|
|
51
|
-
const cap: Capture = { acks: [], edits: [] }
|
|
70
|
+
const cap: Capture = { acks: [], edits: [], markupEdits: [], replies: [] }
|
|
52
71
|
await finalizeCallback(mkCtx(cap), {
|
|
72
|
+
apiCall: passthrough,
|
|
53
73
|
ackText: 'Vault grant revoked',
|
|
54
74
|
alert: true,
|
|
55
75
|
newText: '...',
|
|
@@ -58,8 +78,9 @@ describe('finalizeCallback — three-invariant contract', () => {
|
|
|
58
78
|
})
|
|
59
79
|
|
|
60
80
|
it('invariant 2: strips reply_markup AND rich-edits the body in one atomic call', async () => {
|
|
61
|
-
const cap: Capture = { acks: [], edits: [] }
|
|
81
|
+
const cap: Capture = { acks: [], edits: [], markupEdits: [], replies: [] }
|
|
62
82
|
await finalizeCallback(mkCtx(cap), {
|
|
83
|
+
apiCall: passthrough,
|
|
63
84
|
ackText: 'Approved',
|
|
64
85
|
newText: '**✓ Approved**\n\nGrant minted at 22:38 UTC',
|
|
65
86
|
})
|
|
@@ -75,8 +96,8 @@ describe('finalizeCallback — three-invariant contract', () => {
|
|
|
75
96
|
})
|
|
76
97
|
|
|
77
98
|
it('invariant 2: literalText edits a plain string (no rich wrapper, no parse_mode)', async () => {
|
|
78
|
-
const cap: Capture = { acks: [], edits: [] }
|
|
79
|
-
await finalizeCallback(mkCtx(cap), { ackText: 'ok', newText: 'plain', literalText: true })
|
|
99
|
+
const cap: Capture = { acks: [], edits: [], markupEdits: [], replies: [] }
|
|
100
|
+
await finalizeCallback(mkCtx(cap), { ackText: 'ok', newText: 'plain', literalText: true, apiCall: passthrough })
|
|
80
101
|
expect(cap.edits[0]?.text).toBe('plain')
|
|
81
102
|
expect(cap.edits[0]?.opts.parse_mode).toBeUndefined()
|
|
82
103
|
})
|
|
@@ -91,8 +112,11 @@ describe('finalizeCallback — three-invariant contract', () => {
|
|
|
91
112
|
order.push('edit-end')
|
|
92
113
|
return { message_id: 1 }
|
|
93
114
|
},
|
|
115
|
+
editMessageReplyMarkup: async () => { order.push('markup'); return true },
|
|
116
|
+
reply: async () => { order.push('reply'); return { message_id: 2 } },
|
|
94
117
|
}
|
|
95
118
|
await finalizeCallback(ctx, {
|
|
119
|
+
apiCall: passthrough,
|
|
96
120
|
ackText: 'ok',
|
|
97
121
|
newText: '...',
|
|
98
122
|
synthInbound: () => { order.push('synth') },
|
|
@@ -108,8 +132,9 @@ describe('finalizeCallback — three-invariant contract', () => {
|
|
|
108
132
|
|
|
109
133
|
it('invariant 3: async synthInbound is awaited', async () => {
|
|
110
134
|
let synthResolved = false
|
|
111
|
-
const cap: Capture = { acks: [], edits: [] }
|
|
135
|
+
const cap: Capture = { acks: [], edits: [], markupEdits: [], replies: [] }
|
|
112
136
|
await finalizeCallback(mkCtx(cap), {
|
|
137
|
+
apiCall: passthrough,
|
|
113
138
|
ackText: 'ok',
|
|
114
139
|
newText: '...',
|
|
115
140
|
synthInbound: async () => {
|
|
@@ -122,9 +147,10 @@ describe('finalizeCallback — three-invariant contract', () => {
|
|
|
122
147
|
|
|
123
148
|
it('invariant 3: synthInbound errors are caught + logged, never propagated', async () => {
|
|
124
149
|
const logs: string[] = []
|
|
125
|
-
const cap: Capture = { acks: [], edits: [] }
|
|
150
|
+
const cap: Capture = { acks: [], edits: [], markupEdits: [], replies: [] }
|
|
126
151
|
await expect(
|
|
127
152
|
finalizeCallback(mkCtx(cap), {
|
|
153
|
+
apiCall: passthrough,
|
|
128
154
|
ackText: 'ok',
|
|
129
155
|
newText: '...',
|
|
130
156
|
synthInbound: () => { throw new Error('inject_inbound IPC closed') },
|
|
@@ -143,9 +169,12 @@ describe('finalizeCallback — three-invariant contract', () => {
|
|
|
143
169
|
const cap: Capture = {
|
|
144
170
|
acks: [],
|
|
145
171
|
edits: [],
|
|
172
|
+
markupEdits: [],
|
|
173
|
+
replies: [],
|
|
146
174
|
editThrows: new Error('Bad Request: message to edit not found'),
|
|
147
175
|
}
|
|
148
176
|
await finalizeCallback(mkCtx(cap), {
|
|
177
|
+
apiCall: passthrough,
|
|
149
178
|
ackText: 'Approved',
|
|
150
179
|
newText: '...',
|
|
151
180
|
synthInbound: () => { synthFired = true },
|
|
@@ -163,9 +192,12 @@ describe('finalizeCallback — three-invariant contract', () => {
|
|
|
163
192
|
const cap: Capture = {
|
|
164
193
|
acks: [],
|
|
165
194
|
edits: [],
|
|
195
|
+
markupEdits: [],
|
|
196
|
+
replies: [],
|
|
166
197
|
ackThrows: new Error('query is too old'),
|
|
167
198
|
}
|
|
168
199
|
await finalizeCallback(mkCtx(cap), {
|
|
200
|
+
apiCall: passthrough,
|
|
169
201
|
ackText: 'Approved',
|
|
170
202
|
newText: 'edited body',
|
|
171
203
|
synthInbound: () => { synthFired = true },
|
|
@@ -181,8 +213,9 @@ describe('finalizeCallback — three-invariant contract', () => {
|
|
|
181
213
|
})
|
|
182
214
|
|
|
183
215
|
it('synthInbound is optional — surfaces with no model in the loop just ack + edit', async () => {
|
|
184
|
-
const cap: Capture = { acks: [], edits: [] }
|
|
216
|
+
const cap: Capture = { acks: [], edits: [], markupEdits: [], replies: [] }
|
|
185
217
|
await finalizeCallback(mkCtx(cap), {
|
|
218
|
+
apiCall: passthrough,
|
|
186
219
|
ackText: 'Dismissed',
|
|
187
220
|
newText: '✗ Dismissed',
|
|
188
221
|
})
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real 2026-07 Telegram 429 stream, verbatim.
|
|
3
|
+
*
|
|
4
|
+
* Every `telegram gateway: 429 …` line one production agent's gateway emitted
|
|
5
|
+
* between 2026-07-12 and 2026-07-27, extracted from `gateway-supervisor.log`
|
|
6
|
+
* (plus its rotated `.gz`) as
|
|
7
|
+
* `[<iso>] … (flood ban of <n>s | rate limited, waiting <n>s)`.
|
|
8
|
+
* 170 observations; nothing sampled, synthesised, or reordered. Timestamps and
|
|
9
|
+
* `retry_after` values only — no ids, no chat content.
|
|
10
|
+
*
|
|
11
|
+
* This is the fixture the flood-pressure classifier is proved against, because
|
|
12
|
+
* the shape is the whole problem:
|
|
13
|
+
*
|
|
14
|
+
* - 72 of the 170 are short rate nudges, the largest being 5s. They are
|
|
15
|
+
* invisible to the operator: `retryApiCall` sleeps them and the send lands.
|
|
16
|
+
* - The smallest `retry_after` belonging to a real ban is 282s. There is no
|
|
17
|
+
* observed traffic between 5s and 282s, so the benign band is wide.
|
|
18
|
+
* - 87 of the remaining observations are ONE ban (2026-07-27T20:19:56Z,
|
|
19
|
+
* `retry_after=15908`) re-observed every ~5s as it counted down. Any signal
|
|
20
|
+
* built on event COUNT explodes here, during the outage it should have
|
|
21
|
+
* predicted.
|
|
22
|
+
*
|
|
23
|
+
* The four real bans:
|
|
24
|
+
* 2026-07-12T05:06:46Z retry_after=21397 (5.9h)
|
|
25
|
+
* 2026-07-13T03:17:47Z retry_after=27951 (7.8h)
|
|
26
|
+
* 2026-07-25T23:43:07Z retry_after=3713 (62min)
|
|
27
|
+
* 2026-07-27T20:19:56Z retry_after=15908 (4.4h) ← the incident
|
|
28
|
+
*
|
|
29
|
+
* Generated once from the live logs; do not edit by hand.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** One observed 429: ISO timestamp + Telegram's `parameters.retry_after`. */
|
|
33
|
+
export interface RawFlood429 {
|
|
34
|
+
iso: string
|
|
35
|
+
ts: number
|
|
36
|
+
retryAfterSec: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const RAW: ReadonlyArray<{ iso: string; retryAfterSec: number }> = [
|
|
40
|
+
{ iso: "2026-07-12T00:43:42.969Z", retryAfterSec: 3 },
|
|
41
|
+
{ iso: "2026-07-12T00:43:52.754Z", retryAfterSec: 3 },
|
|
42
|
+
{ iso: "2026-07-12T05:06:46.641Z", retryAfterSec: 21397 },
|
|
43
|
+
{ iso: "2026-07-12T23:39:24.579Z", retryAfterSec: 436 },
|
|
44
|
+
{ iso: "2026-07-13T00:37:18.647Z", retryAfterSec: 563 },
|
|
45
|
+
{ iso: "2026-07-13T00:37:19.162Z", retryAfterSec: 563 },
|
|
46
|
+
{ iso: "2026-07-13T01:42:00.795Z", retryAfterSec: 283 },
|
|
47
|
+
{ iso: "2026-07-13T01:42:01.269Z", retryAfterSec: 282 },
|
|
48
|
+
{ iso: "2026-07-13T01:42:01.741Z", retryAfterSec: 282 },
|
|
49
|
+
{ iso: "2026-07-13T02:39:43.308Z", retryAfterSec: 425 },
|
|
50
|
+
{ iso: "2026-07-13T03:17:47.764Z", retryAfterSec: 27951 },
|
|
51
|
+
{ iso: "2026-07-14T01:00:55.498Z", retryAfterSec: 3 },
|
|
52
|
+
{ iso: "2026-07-14T01:01:05.283Z", retryAfterSec: 3 },
|
|
53
|
+
{ iso: "2026-07-14T01:42:51.141Z", retryAfterSec: 3 },
|
|
54
|
+
{ iso: "2026-07-14T01:43:00.941Z", retryAfterSec: 3 },
|
|
55
|
+
{ iso: "2026-07-14T10:17:08.494Z", retryAfterSec: 3 },
|
|
56
|
+
{ iso: "2026-07-14T10:17:18.394Z", retryAfterSec: 3 },
|
|
57
|
+
{ iso: "2026-07-15T06:47:31.287Z", retryAfterSec: 3 },
|
|
58
|
+
{ iso: "2026-07-15T11:28:14.054Z", retryAfterSec: 3 },
|
|
59
|
+
{ iso: "2026-07-15T11:28:23.913Z", retryAfterSec: 3 },
|
|
60
|
+
{ iso: "2026-07-16T01:48:28.656Z", retryAfterSec: 3 },
|
|
61
|
+
{ iso: "2026-07-16T01:48:38.477Z", retryAfterSec: 3 },
|
|
62
|
+
{ iso: "2026-07-16T02:37:16.659Z", retryAfterSec: 3 },
|
|
63
|
+
{ iso: "2026-07-16T02:37:26.496Z", retryAfterSec: 3 },
|
|
64
|
+
{ iso: "2026-07-16T07:39:14.389Z", retryAfterSec: 3 },
|
|
65
|
+
{ iso: "2026-07-16T07:39:24.206Z", retryAfterSec: 3 },
|
|
66
|
+
{ iso: "2026-07-16T19:29:32.965Z", retryAfterSec: 3 },
|
|
67
|
+
{ iso: "2026-07-16T19:29:42.817Z", retryAfterSec: 3 },
|
|
68
|
+
{ iso: "2026-07-16T21:32:57.539Z", retryAfterSec: 5 },
|
|
69
|
+
{ iso: "2026-07-17T00:04:44.818Z", retryAfterSec: 3 },
|
|
70
|
+
{ iso: "2026-07-17T00:04:54.594Z", retryAfterSec: 3 },
|
|
71
|
+
{ iso: "2026-07-17T02:45:40.820Z", retryAfterSec: 3 },
|
|
72
|
+
{ iso: "2026-07-17T02:45:50.608Z", retryAfterSec: 3 },
|
|
73
|
+
{ iso: "2026-07-17T06:10:38.674Z", retryAfterSec: 3 },
|
|
74
|
+
{ iso: "2026-07-17T06:10:48.510Z", retryAfterSec: 3 },
|
|
75
|
+
{ iso: "2026-07-17T07:55:18.855Z", retryAfterSec: 3 },
|
|
76
|
+
{ iso: "2026-07-17T07:55:28.640Z", retryAfterSec: 3 },
|
|
77
|
+
{ iso: "2026-07-17T09:21:15.798Z", retryAfterSec: 3 },
|
|
78
|
+
{ iso: "2026-07-18T22:35:20.897Z", retryAfterSec: 3 },
|
|
79
|
+
{ iso: "2026-07-19T01:25:52.829Z", retryAfterSec: 3 },
|
|
80
|
+
{ iso: "2026-07-19T01:26:02.649Z", retryAfterSec: 3 },
|
|
81
|
+
{ iso: "2026-07-19T03:29:55.399Z", retryAfterSec: 3 },
|
|
82
|
+
{ iso: "2026-07-19T03:30:05.203Z", retryAfterSec: 3 },
|
|
83
|
+
{ iso: "2026-07-19T06:05:47.796Z", retryAfterSec: 3 },
|
|
84
|
+
{ iso: "2026-07-19T06:05:57.632Z", retryAfterSec: 3 },
|
|
85
|
+
{ iso: "2026-07-19T10:21:12.347Z", retryAfterSec: 3 },
|
|
86
|
+
{ iso: "2026-07-19T10:21:22.178Z", retryAfterSec: 3 },
|
|
87
|
+
{ iso: "2026-07-19T10:21:50.826Z", retryAfterSec: 3 },
|
|
88
|
+
{ iso: "2026-07-19T10:22:00.651Z", retryAfterSec: 3 },
|
|
89
|
+
{ iso: "2026-07-19T10:22:10.486Z", retryAfterSec: 3 },
|
|
90
|
+
{ iso: "2026-07-19T10:47:10.609Z", retryAfterSec: 3 },
|
|
91
|
+
{ iso: "2026-07-19T10:47:20.439Z", retryAfterSec: 3 },
|
|
92
|
+
{ iso: "2026-07-19T10:47:30.277Z", retryAfterSec: 3 },
|
|
93
|
+
{ iso: "2026-07-20T01:40:22.311Z", retryAfterSec: 3 },
|
|
94
|
+
{ iso: "2026-07-20T01:40:32.183Z", retryAfterSec: 3 },
|
|
95
|
+
{ iso: "2026-07-20T02:26:37.436Z", retryAfterSec: 3 },
|
|
96
|
+
{ iso: "2026-07-20T02:26:47.514Z", retryAfterSec: 3 },
|
|
97
|
+
{ iso: "2026-07-21T11:14:47.910Z", retryAfterSec: 3 },
|
|
98
|
+
{ iso: "2026-07-21T11:14:57.729Z", retryAfterSec: 3 },
|
|
99
|
+
{ iso: "2026-07-21T22:59:02.412Z", retryAfterSec: 3 },
|
|
100
|
+
{ iso: "2026-07-21T22:59:12.189Z", retryAfterSec: 3 },
|
|
101
|
+
{ iso: "2026-07-23T19:58:39.060Z", retryAfterSec: 3 },
|
|
102
|
+
{ iso: "2026-07-24T08:22:21.126Z", retryAfterSec: 3 },
|
|
103
|
+
{ iso: "2026-07-24T09:00:54.528Z", retryAfterSec: 3 },
|
|
104
|
+
{ iso: "2026-07-24T09:01:04.413Z", retryAfterSec: 3 },
|
|
105
|
+
{ iso: "2026-07-24T21:47:04.852Z", retryAfterSec: 3 },
|
|
106
|
+
{ iso: "2026-07-24T21:49:20.213Z", retryAfterSec: 3 },
|
|
107
|
+
{ iso: "2026-07-24T21:49:30.004Z", retryAfterSec: 3 },
|
|
108
|
+
{ iso: "2026-07-24T21:49:39.786Z", retryAfterSec: 3 },
|
|
109
|
+
{ iso: "2026-07-25T05:24:04.941Z", retryAfterSec: 3 },
|
|
110
|
+
{ iso: "2026-07-25T22:50:39.203Z", retryAfterSec: 3 },
|
|
111
|
+
{ iso: "2026-07-25T22:50:48.981Z", retryAfterSec: 3 },
|
|
112
|
+
{ iso: "2026-07-25T23:43:07.861Z", retryAfterSec: 3713 },
|
|
113
|
+
{ iso: "2026-07-26T14:07:07.428Z", retryAfterSec: 3 },
|
|
114
|
+
{ iso: "2026-07-26T14:07:20.567Z", retryAfterSec: 3 },
|
|
115
|
+
{ iso: "2026-07-26T14:07:30.355Z", retryAfterSec: 3 },
|
|
116
|
+
{ iso: "2026-07-26T22:43:03.042Z", retryAfterSec: 3 },
|
|
117
|
+
{ iso: "2026-07-27T05:50:52.944Z", retryAfterSec: 3 },
|
|
118
|
+
{ iso: "2026-07-27T10:04:58.523Z", retryAfterSec: 3 },
|
|
119
|
+
{ iso: "2026-07-27T10:05:12.142Z", retryAfterSec: 3 },
|
|
120
|
+
{ iso: "2026-07-27T10:05:21.953Z", retryAfterSec: 3 },
|
|
121
|
+
{ iso: "2026-07-27T16:22:32.052Z", retryAfterSec: 3 },
|
|
122
|
+
{ iso: "2026-07-27T20:19:56.704Z", retryAfterSec: 15908 },
|
|
123
|
+
{ iso: "2026-07-27T20:53:27.532Z", retryAfterSec: 13897 },
|
|
124
|
+
{ iso: "2026-07-27T20:53:31.923Z", retryAfterSec: 13893 },
|
|
125
|
+
{ iso: "2026-07-27T20:53:36.919Z", retryAfterSec: 13888 },
|
|
126
|
+
{ iso: "2026-07-27T20:53:41.904Z", retryAfterSec: 13883 },
|
|
127
|
+
{ iso: "2026-07-27T20:53:46.908Z", retryAfterSec: 13878 },
|
|
128
|
+
{ iso: "2026-07-27T20:53:51.904Z", retryAfterSec: 13873 },
|
|
129
|
+
{ iso: "2026-07-27T20:53:56.914Z", retryAfterSec: 13868 },
|
|
130
|
+
{ iso: "2026-07-27T20:54:01.911Z", retryAfterSec: 13863 },
|
|
131
|
+
{ iso: "2026-07-27T20:54:06.907Z", retryAfterSec: 13858 },
|
|
132
|
+
{ iso: "2026-07-27T20:54:11.932Z", retryAfterSec: 13853 },
|
|
133
|
+
{ iso: "2026-07-27T20:54:16.925Z", retryAfterSec: 13848 },
|
|
134
|
+
{ iso: "2026-07-27T20:54:21.929Z", retryAfterSec: 13843 },
|
|
135
|
+
{ iso: "2026-07-27T20:54:26.932Z", retryAfterSec: 13838 },
|
|
136
|
+
{ iso: "2026-07-27T20:54:31.929Z", retryAfterSec: 13833 },
|
|
137
|
+
{ iso: "2026-07-27T20:54:36.940Z", retryAfterSec: 13828 },
|
|
138
|
+
{ iso: "2026-07-27T20:54:41.914Z", retryAfterSec: 13823 },
|
|
139
|
+
{ iso: "2026-07-27T20:54:46.919Z", retryAfterSec: 13818 },
|
|
140
|
+
{ iso: "2026-07-27T20:54:51.949Z", retryAfterSec: 13813 },
|
|
141
|
+
{ iso: "2026-07-27T20:54:56.911Z", retryAfterSec: 13808 },
|
|
142
|
+
{ iso: "2026-07-27T20:55:01.932Z", retryAfterSec: 13803 },
|
|
143
|
+
{ iso: "2026-07-27T20:55:06.918Z", retryAfterSec: 13798 },
|
|
144
|
+
{ iso: "2026-07-27T20:55:11.949Z", retryAfterSec: 13793 },
|
|
145
|
+
{ iso: "2026-07-27T20:55:17.011Z", retryAfterSec: 13788 },
|
|
146
|
+
{ iso: "2026-07-27T20:55:21.997Z", retryAfterSec: 13783 },
|
|
147
|
+
{ iso: "2026-07-27T20:55:26.949Z", retryAfterSec: 13778 },
|
|
148
|
+
{ iso: "2026-07-27T20:55:31.941Z", retryAfterSec: 13773 },
|
|
149
|
+
{ iso: "2026-07-27T20:55:36.943Z", retryAfterSec: 13768 },
|
|
150
|
+
{ iso: "2026-07-27T20:55:41.924Z", retryAfterSec: 13763 },
|
|
151
|
+
{ iso: "2026-07-27T20:55:46.927Z", retryAfterSec: 13758 },
|
|
152
|
+
{ iso: "2026-07-27T20:55:51.928Z", retryAfterSec: 13753 },
|
|
153
|
+
{ iso: "2026-07-27T20:55:56.922Z", retryAfterSec: 13748 },
|
|
154
|
+
{ iso: "2026-07-27T20:56:01.937Z", retryAfterSec: 13743 },
|
|
155
|
+
{ iso: "2026-07-27T20:56:06.922Z", retryAfterSec: 13738 },
|
|
156
|
+
{ iso: "2026-07-27T20:56:11.950Z", retryAfterSec: 13733 },
|
|
157
|
+
{ iso: "2026-07-27T20:56:16.950Z", retryAfterSec: 13728 },
|
|
158
|
+
{ iso: "2026-07-27T20:56:21.954Z", retryAfterSec: 13723 },
|
|
159
|
+
{ iso: "2026-07-27T20:56:26.955Z", retryAfterSec: 13718 },
|
|
160
|
+
{ iso: "2026-07-27T20:56:31.952Z", retryAfterSec: 13713 },
|
|
161
|
+
{ iso: "2026-07-27T20:56:36.954Z", retryAfterSec: 13708 },
|
|
162
|
+
{ iso: "2026-07-27T20:56:41.952Z", retryAfterSec: 13703 },
|
|
163
|
+
{ iso: "2026-07-27T20:56:46.938Z", retryAfterSec: 13698 },
|
|
164
|
+
{ iso: "2026-07-27T20:56:51.937Z", retryAfterSec: 13693 },
|
|
165
|
+
{ iso: "2026-07-27T20:56:56.942Z", retryAfterSec: 13688 },
|
|
166
|
+
{ iso: "2026-07-27T20:57:01.984Z", retryAfterSec: 13683 },
|
|
167
|
+
{ iso: "2026-07-27T20:57:06.955Z", retryAfterSec: 13678 },
|
|
168
|
+
{ iso: "2026-07-27T20:57:11.959Z", retryAfterSec: 13673 },
|
|
169
|
+
{ iso: "2026-07-27T20:57:16.976Z", retryAfterSec: 13668 },
|
|
170
|
+
{ iso: "2026-07-27T20:57:21.964Z", retryAfterSec: 13663 },
|
|
171
|
+
{ iso: "2026-07-27T20:57:26.962Z", retryAfterSec: 13658 },
|
|
172
|
+
{ iso: "2026-07-27T20:57:31.968Z", retryAfterSec: 13653 },
|
|
173
|
+
{ iso: "2026-07-27T20:57:36.960Z", retryAfterSec: 13648 },
|
|
174
|
+
{ iso: "2026-07-27T20:57:41.981Z", retryAfterSec: 13643 },
|
|
175
|
+
{ iso: "2026-07-27T20:57:46.972Z", retryAfterSec: 13638 },
|
|
176
|
+
{ iso: "2026-07-27T20:57:51.976Z", retryAfterSec: 13633 },
|
|
177
|
+
{ iso: "2026-07-27T20:57:56.966Z", retryAfterSec: 13628 },
|
|
178
|
+
{ iso: "2026-07-27T20:58:01.971Z", retryAfterSec: 13623 },
|
|
179
|
+
{ iso: "2026-07-27T20:58:07.023Z", retryAfterSec: 13618 },
|
|
180
|
+
{ iso: "2026-07-27T20:58:12.081Z", retryAfterSec: 13613 },
|
|
181
|
+
{ iso: "2026-07-27T20:58:16.975Z", retryAfterSec: 13608 },
|
|
182
|
+
{ iso: "2026-07-27T20:58:21.974Z", retryAfterSec: 13603 },
|
|
183
|
+
{ iso: "2026-07-27T20:58:26.994Z", retryAfterSec: 13598 },
|
|
184
|
+
{ iso: "2026-07-27T20:58:31.979Z", retryAfterSec: 13593 },
|
|
185
|
+
{ iso: "2026-07-27T20:58:36.977Z", retryAfterSec: 13588 },
|
|
186
|
+
{ iso: "2026-07-27T20:58:41.983Z", retryAfterSec: 13583 },
|
|
187
|
+
{ iso: "2026-07-27T20:58:46.966Z", retryAfterSec: 13578 },
|
|
188
|
+
{ iso: "2026-07-27T20:58:51.982Z", retryAfterSec: 13573 },
|
|
189
|
+
{ iso: "2026-07-27T20:58:56.984Z", retryAfterSec: 13568 },
|
|
190
|
+
{ iso: "2026-07-27T20:59:01.983Z", retryAfterSec: 13563 },
|
|
191
|
+
{ iso: "2026-07-27T20:59:06.982Z", retryAfterSec: 13558 },
|
|
192
|
+
{ iso: "2026-07-27T20:59:11.987Z", retryAfterSec: 13553 },
|
|
193
|
+
{ iso: "2026-07-27T20:59:16.995Z", retryAfterSec: 13548 },
|
|
194
|
+
{ iso: "2026-07-27T20:59:21.982Z", retryAfterSec: 13543 },
|
|
195
|
+
{ iso: "2026-07-27T20:59:26.987Z", retryAfterSec: 13538 },
|
|
196
|
+
{ iso: "2026-07-27T20:59:31.991Z", retryAfterSec: 13533 },
|
|
197
|
+
{ iso: "2026-07-27T20:59:36.994Z", retryAfterSec: 13528 },
|
|
198
|
+
{ iso: "2026-07-27T20:59:41.993Z", retryAfterSec: 13523 },
|
|
199
|
+
{ iso: "2026-07-27T20:59:47.005Z", retryAfterSec: 13518 },
|
|
200
|
+
{ iso: "2026-07-27T20:59:51.997Z", retryAfterSec: 13513 },
|
|
201
|
+
{ iso: "2026-07-27T20:59:56.996Z", retryAfterSec: 13508 },
|
|
202
|
+
{ iso: "2026-07-27T21:00:02.004Z", retryAfterSec: 13503 },
|
|
203
|
+
{ iso: "2026-07-27T21:00:07.009Z", retryAfterSec: 13498 },
|
|
204
|
+
{ iso: "2026-07-27T21:00:11.996Z", retryAfterSec: 13493 },
|
|
205
|
+
{ iso: "2026-07-27T21:00:17.016Z", retryAfterSec: 13488 },
|
|
206
|
+
{ iso: "2026-07-27T21:00:22.002Z", retryAfterSec: 13483 },
|
|
207
|
+
{ iso: "2026-07-27T21:00:27.001Z", retryAfterSec: 13478 },
|
|
208
|
+
{ iso: "2026-07-27T21:00:32.002Z", retryAfterSec: 13473 },
|
|
209
|
+
{ iso: "2026-07-27T21:00:36.999Z", retryAfterSec: 13468 },
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
export const REAL_429_STREAM: ReadonlyArray<RawFlood429> = RAW.map((e) => ({
|
|
213
|
+
...e,
|
|
214
|
+
ts: Date.parse(e.iso),
|
|
215
|
+
}))
|
|
216
|
+
|
|
217
|
+
/** Convenience: `Date.parse` of an ISO instant, for assertion timestamps. */
|
|
218
|
+
export function at(iso: string): number {
|
|
219
|
+
return Date.parse(iso)
|
|
220
|
+
}
|