switchroom 0.18.8 → 0.18.9
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/README.md +2 -2
- package/dist/cli/switchroom.js +2 -2
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +78648 -77445
- package/telegram-plugin/gateway/approval-card-stores.ts +99 -0
- package/telegram-plugin/gateway/bot-commands-ops-info.ts +194 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +2660 -0
- package/telegram-plugin/gateway/gateway.ts +527 -2880
- package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +181 -23
- package/telegram-plugin/gateway/inbound-delivery-machine.ts +8 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +375 -0
- package/telegram-plugin/gateway/pending-state-stores.ts +106 -0
- package/telegram-plugin/gateway/register-bot-commands.ts +30 -0
- package/telegram-plugin/tests/approval-card-stores.test.ts +124 -0
- package/telegram-plugin/tests/callback-query-handlers.test.ts +701 -0
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +11 -4
- package/telegram-plugin/tests/fixtures/cutover-killswitch-probe.ts +75 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +5 -1
- package/telegram-plugin/tests/inbound-delivery-cutover-flip.test.ts +418 -0
- package/telegram-plugin/tests/inbound-delivery-dispatch-equivalence.test.ts +348 -0
- package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +141 -52
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -1
- package/telegram-plugin/tests/outbound-send-chunks.test.ts +304 -0
- package/telegram-plugin/tests/outbound-send-path.test.ts +222 -0
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +34 -15
- package/telegram-plugin/tests/pending-state-stores.test.ts +235 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +18 -4
- package/telegram-plugin/tests/vault-approval-posture.test.ts +15 -7
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +8 -4
- package/telegram-plugin/tests/vault-grant-union.test.ts +8 -4
- package/telegram-plugin/tests/vault-grant-wizard.test.ts +8 -1
- package/telegram-plugin/tests/vault-grants-revoke.test.ts +8 -1
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +8 -4
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +8 -4
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +8 -4
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract + race/ordering pins for the long-tail pending-state stores
|
|
3
|
+
* (gateway/pending-state-stores.ts), extracted in #2996 Phase 3 step 2.
|
|
4
|
+
*
|
|
5
|
+
* Written BEFORE the long-tail Maps moved behind the store module. They lock:
|
|
6
|
+
*
|
|
7
|
+
* 1. Each family's EXACT `isExpired` comparison direction survives verbatim —
|
|
8
|
+
* TTL directions differ across families (`now - staged_at > TTL`,
|
|
9
|
+
* `now - startedAt > TTL`, `now - createdAt > TTL`, `now - armed_at > TTL`,
|
|
10
|
+
* and the absolute `now > expiresAt`), and a store must delete exactly the
|
|
11
|
+
* entries the old open-coded loop deleted, not one tick early or late.
|
|
12
|
+
* 2. `sweep` is delete-during-iteration safe (JS Map iterators tolerate
|
|
13
|
+
* deleting the current key — the raw reaper loops relied on this).
|
|
14
|
+
* 3. The gateway-visible EAGER-SWEEP-AT-ADD pattern (the sweep called right
|
|
15
|
+
* after a `.set` at a staging site, e.g. `sweepSecretRequests()` after the
|
|
16
|
+
* `pendingSecretRequests.set` in executeRequestSecret) is single-shot: an
|
|
17
|
+
* add followed by an eager sweep expires only entries already past TTL and
|
|
18
|
+
* leaves the just-added fresh entry intact.
|
|
19
|
+
* 4. Map-surface parity for every operation the call sites use, including the
|
|
20
|
+
* LRU eviction pattern `agentButtonMeta` uses (`keys().next().value`) and
|
|
21
|
+
* the live `size` getter.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from 'vitest'
|
|
25
|
+
import {
|
|
26
|
+
createSweepableStore,
|
|
27
|
+
createPlainStore,
|
|
28
|
+
} from '../gateway/pending-state-stores.js'
|
|
29
|
+
|
|
30
|
+
// TTL constants mirror the gateway family values so the direction pins are
|
|
31
|
+
// meaningful (values themselves are not the contract — the direction is).
|
|
32
|
+
const VAULT_INPUT_TTL_MS = 5 * 60 * 1000
|
|
33
|
+
const VAULT_PASSPHRASE_TTL_MS = 30 * 60 * 1000
|
|
34
|
+
const DEFERRED_SECRET_TTL_MS = 5 * 60 * 1000
|
|
35
|
+
const ARMED_SECRET_CAPTURE_TTL_MS = 10 * 60_000
|
|
36
|
+
const ALWAYS_ALLOW_CORRELATION_TTL_MS = 30_000
|
|
37
|
+
const MENTAL_MODEL_CORRELATION_TTL_MS = 720_000
|
|
38
|
+
const REAUTH_INTERCEPT_TTL_MS = 10 * 60_000
|
|
39
|
+
|
|
40
|
+
describe('pending-state-stores: per-family isExpired direction (byte-identical)', () => {
|
|
41
|
+
it('pendingVaultOps — now - startedAt > VAULT_INPUT_TTL_MS', () => {
|
|
42
|
+
const store = createSweepableStore<{ startedAt: number }>(
|
|
43
|
+
(v, now) => now - v.startedAt > VAULT_INPUT_TTL_MS,
|
|
44
|
+
)
|
|
45
|
+
const now = 10_000_000
|
|
46
|
+
store.set('fresh', { startedAt: now - VAULT_INPUT_TTL_MS }) // exactly TTL old: NOT expired (> is strict)
|
|
47
|
+
store.set('stale', { startedAt: now - VAULT_INPUT_TTL_MS - 1 })
|
|
48
|
+
store.sweep(now)
|
|
49
|
+
expect(store.has('fresh')).toBe(true)
|
|
50
|
+
expect(store.has('stale')).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('vaultPassphraseCache — absolute now > expiresAt', () => {
|
|
54
|
+
const store = createSweepableStore<{ expiresAt: number }>(
|
|
55
|
+
(v, now) => now > v.expiresAt,
|
|
56
|
+
)
|
|
57
|
+
const now = 10_000_000
|
|
58
|
+
store.set('atExpiry', { expiresAt: now }) // now > now is false → NOT expired
|
|
59
|
+
store.set('past', { expiresAt: now - 1 })
|
|
60
|
+
store.sweep(now)
|
|
61
|
+
expect(store.has('atExpiry')).toBe(true)
|
|
62
|
+
expect(store.has('past')).toBe(false)
|
|
63
|
+
void VAULT_PASSPHRASE_TTL_MS
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('deferredSecrets — now - staged_at > DEFERRED_SECRET_TTL_MS', () => {
|
|
67
|
+
const store = createSweepableStore<{ staged_at: number }>(
|
|
68
|
+
(v, now) => now - v.staged_at > DEFERRED_SECRET_TTL_MS,
|
|
69
|
+
)
|
|
70
|
+
const now = 10_000_000
|
|
71
|
+
store.set('fresh', { staged_at: now - DEFERRED_SECRET_TTL_MS })
|
|
72
|
+
store.set('stale', { staged_at: now - DEFERRED_SECRET_TTL_MS - 1 })
|
|
73
|
+
store.sweep(now)
|
|
74
|
+
expect(store.has('fresh')).toBe(true)
|
|
75
|
+
expect(store.has('stale')).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('armedSecretCaptures — now - armed_at > ARMED_SECRET_CAPTURE_TTL_MS', () => {
|
|
79
|
+
const store = createSweepableStore<{ armed_at: number }>(
|
|
80
|
+
(v, now) => now - v.armed_at > ARMED_SECRET_CAPTURE_TTL_MS,
|
|
81
|
+
)
|
|
82
|
+
const now = 10_000_000
|
|
83
|
+
store.set('fresh', { armed_at: now - ARMED_SECRET_CAPTURE_TTL_MS })
|
|
84
|
+
store.set('stale', { armed_at: now - ARMED_SECRET_CAPTURE_TTL_MS - 1 })
|
|
85
|
+
store.sweep(now)
|
|
86
|
+
expect(store.has('fresh')).toBe(true)
|
|
87
|
+
expect(store.has('stale')).toBe(false)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('pendingAlwaysAllowCorrelations — 30s now - createdAt > TTL', () => {
|
|
91
|
+
const store = createSweepableStore<{ createdAt: number }>(
|
|
92
|
+
(v, now) => now - v.createdAt > ALWAYS_ALLOW_CORRELATION_TTL_MS,
|
|
93
|
+
)
|
|
94
|
+
const now = 10_000_000
|
|
95
|
+
store.set('fresh', { createdAt: now - ALWAYS_ALLOW_CORRELATION_TTL_MS })
|
|
96
|
+
store.set('stale', { createdAt: now - ALWAYS_ALLOW_CORRELATION_TTL_MS - 1 })
|
|
97
|
+
store.sweep(now)
|
|
98
|
+
expect(store.has('fresh')).toBe(true)
|
|
99
|
+
expect(store.has('stale')).toBe(false)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('pendingMentalModelCorrelations — 720s now - createdAt > TTL (outlives the 30s window)', () => {
|
|
103
|
+
const store = createSweepableStore<{ createdAt: number }>(
|
|
104
|
+
(v, now) => now - v.createdAt > MENTAL_MODEL_CORRELATION_TTL_MS,
|
|
105
|
+
)
|
|
106
|
+
const now = 10_000_000
|
|
107
|
+
// A slow-but-valid tap 10 min in must NOT be swept (the whole point of the
|
|
108
|
+
// dedicated 720s TTL vs the 30s always-allow window).
|
|
109
|
+
store.set('slowTap', { createdAt: now - 10 * 60_000 })
|
|
110
|
+
store.set('stale', { createdAt: now - MENTAL_MODEL_CORRELATION_TTL_MS - 1 })
|
|
111
|
+
store.sweep(now)
|
|
112
|
+
expect(store.has('slowTap')).toBe(true)
|
|
113
|
+
expect(store.has('stale')).toBe(false)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('pendingReauthFlows — now - startedAt > REAUTH_INTERCEPT_TTL_MS', () => {
|
|
117
|
+
const store = createSweepableStore<{ startedAt: number }>(
|
|
118
|
+
(v, now) => now - v.startedAt > REAUTH_INTERCEPT_TTL_MS,
|
|
119
|
+
)
|
|
120
|
+
const now = 10_000_000
|
|
121
|
+
store.set('fresh', { startedAt: now - REAUTH_INTERCEPT_TTL_MS })
|
|
122
|
+
store.set('stale', { startedAt: now - REAUTH_INTERCEPT_TTL_MS - 1 })
|
|
123
|
+
store.sweep(now)
|
|
124
|
+
expect(store.has('fresh')).toBe(true)
|
|
125
|
+
expect(store.has('stale')).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('pending-state-stores: sweep safety', () => {
|
|
130
|
+
it('deletes every past-TTL entry across a multi-entry map (delete-during-iteration safe)', () => {
|
|
131
|
+
const store = createSweepableStore<{ staged_at: number }>(
|
|
132
|
+
(v, now) => now - v.staged_at > 1000,
|
|
133
|
+
)
|
|
134
|
+
for (let i = 0; i < 10; i++) store.set(`e${i}`, { staged_at: i % 2 === 0 ? 0 : 9_999_999 })
|
|
135
|
+
store.sweep(1_000_000) // even entries stale, odd entries fresh
|
|
136
|
+
for (let i = 0; i < 10; i++) {
|
|
137
|
+
expect(store.has(`e${i}`)).toBe(i % 2 === 1)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('is single-shot across two sweep ticks', () => {
|
|
142
|
+
const store = createSweepableStore<{ staged_at: number }>(
|
|
143
|
+
(v, now) => now - v.staged_at > 1000,
|
|
144
|
+
)
|
|
145
|
+
store.set('one', { staged_at: 0 })
|
|
146
|
+
store.sweep(1_000_000)
|
|
147
|
+
expect(store.size).toBe(0)
|
|
148
|
+
expect(() => store.sweep(1_000_000)).not.toThrow() // second tick — nothing left
|
|
149
|
+
expect(store.size).toBe(0)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('pending-state-stores: eager-sweep-at-add pattern (gateway-visible)', () => {
|
|
154
|
+
// Mirrors executeRequestSecret: dedupe-drop prior stage for the same target,
|
|
155
|
+
// .set the new one, then eagerly sweep. The just-added fresh entry survives;
|
|
156
|
+
// a stale sibling is reaped in the same eager pass.
|
|
157
|
+
it('add + eager sweep expires only stale siblings, keeps the fresh add', () => {
|
|
158
|
+
const TTL = 30 * 60_000
|
|
159
|
+
const store = createSweepableStore<{ chat_id: string; key: string; staged_at: number }>(
|
|
160
|
+
(v, now) => now - v.staged_at > TTL,
|
|
161
|
+
)
|
|
162
|
+
const now = 100_000_000
|
|
163
|
+
store.set('old', { chat_id: 'c', key: 'k1', staged_at: now - TTL - 1 }) // stale
|
|
164
|
+
// Dedupe pass (same target) would delete a live dup; here targets differ.
|
|
165
|
+
const fresh = { chat_id: 'c', key: 'k2', staged_at: now }
|
|
166
|
+
store.set('new', fresh)
|
|
167
|
+
store.sweep(now) // eager sweep at the add site
|
|
168
|
+
expect(store.has('new')).toBe(true) // fresh add never self-expires
|
|
169
|
+
expect(store.get('new')).toBe(fresh)
|
|
170
|
+
expect(store.has('old')).toBe(false) // stale sibling reaped in the eager pass
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('dedupe-then-add-then-sweep leaves exactly the new entry', () => {
|
|
174
|
+
const TTL = 30 * 60_000
|
|
175
|
+
const store = createSweepableStore<{ chat_id: string; key: string; staged_at: number }>(
|
|
176
|
+
(v, now) => now - v.staged_at > TTL,
|
|
177
|
+
)
|
|
178
|
+
const now = 100_000_000
|
|
179
|
+
store.set('dup', { chat_id: 'c', key: 'k', staged_at: now - 1000 })
|
|
180
|
+
// dedupe: drop prior stage for the same (chat, key)
|
|
181
|
+
for (const [sid, p] of store) {
|
|
182
|
+
if (p.chat_id === 'c' && p.key === 'k') store.delete(sid)
|
|
183
|
+
}
|
|
184
|
+
store.set('fresh', { chat_id: 'c', key: 'k', staged_at: now })
|
|
185
|
+
store.sweep(now)
|
|
186
|
+
expect([...store.keys()]).toEqual(['fresh'])
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('pending-state-stores: plain store (no sweep)', () => {
|
|
191
|
+
it('has no sweep method and preserves Map surface', () => {
|
|
192
|
+
const store = createPlainStore<{ n: number }>()
|
|
193
|
+
expect((store as { sweep?: unknown }).sweep).toBeUndefined()
|
|
194
|
+
store.set('a', { n: 1 })
|
|
195
|
+
store.set('b', { n: 2 })
|
|
196
|
+
expect(store.size).toBe(2)
|
|
197
|
+
expect(store.get('a')?.n).toBe(1)
|
|
198
|
+
expect(store.has('b')).toBe(true)
|
|
199
|
+
expect(store.delete('a')).toBe(true)
|
|
200
|
+
expect(store.has('a')).toBe(false)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('agentButtonMeta LRU eviction — keys().next().value is the oldest insertion', () => {
|
|
204
|
+
const AGENT_BUTTON_META_MAX = 3
|
|
205
|
+
const store = createPlainStore<Map<string, { ack: string }>>()
|
|
206
|
+
const remember = (key: string) => {
|
|
207
|
+
store.set(key, new Map([['cb', { ack: key }]]))
|
|
208
|
+
while (store.size > AGENT_BUTTON_META_MAX) {
|
|
209
|
+
const oldest = store.keys().next().value
|
|
210
|
+
if (oldest === undefined) break
|
|
211
|
+
store.delete(oldest)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
remember('m1')
|
|
215
|
+
remember('m2')
|
|
216
|
+
remember('m3')
|
|
217
|
+
remember('m4') // evicts m1 (oldest insertion)
|
|
218
|
+
expect(store.has('m1')).toBe(false)
|
|
219
|
+
expect([...store.keys()]).toEqual(['m2', 'm3', 'm4'])
|
|
220
|
+
expect(store.get('m4')?.get('cb')?.ack).toBe('m4')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('pendingAskUser surface — values()/entries()/get/delete used by call sites', () => {
|
|
224
|
+
const store = createPlainStore<{ chatId: string; messageId: number | null }>()
|
|
225
|
+
store.set('ask1', { chatId: 'c1', messageId: 10 })
|
|
226
|
+
store.set('ask2', { chatId: 'c2', messageId: null })
|
|
227
|
+
expect([...store.values()].map((v) => v.chatId).sort()).toEqual(['c1', 'c2'])
|
|
228
|
+
const collected: string[] = []
|
|
229
|
+
for (const [askId] of store.entries()) collected.push(askId)
|
|
230
|
+
expect(collected.sort()).toEqual(['ask1', 'ask2'])
|
|
231
|
+
expect(store.get('ask1')?.messageId).toBe(10)
|
|
232
|
+
store.delete('ask1')
|
|
233
|
+
expect(store.size).toBe(1)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
@@ -261,10 +261,24 @@ describe('#2798 turn-flush punctuation/bold parity with reply', () => {
|
|
|
261
261
|
)
|
|
262
262
|
|
|
263
263
|
it('reply path: normalizes AFTER redact and BEFORE the voice scrub', () => {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
264
|
+
// #2996: the reply-path entry pipeline moved into outbound-send-path.ts
|
|
265
|
+
// (`normalizeOutboundBody`). The reply path delegates to it via
|
|
266
|
+
// `normalizeOutboundBody(rawText, 'reply', redactOutboundText)`; the
|
|
267
|
+
// redact→normalize→scrub ordering is now pinned in the module source.
|
|
268
|
+
const replyDelegates = gatewaySrc.indexOf(
|
|
269
|
+
`normalizeOutboundBody(rawText, 'reply', redactOutboundText)`,
|
|
270
|
+
gatewaySrc.indexOf('async function executeReply('),
|
|
271
|
+
)
|
|
272
|
+
expect(replyDelegates).toBeGreaterThan(0)
|
|
273
|
+
|
|
274
|
+
const moduleSrc = readFileSync(
|
|
275
|
+
new URL('../gateway/outbound-send-path.ts', import.meta.url),
|
|
276
|
+
'utf8',
|
|
277
|
+
)
|
|
278
|
+
const start = moduleSrc.indexOf('export function normalizeOutboundBody(')
|
|
279
|
+
const redactIdx = moduleSrc.indexOf('redact(text, site)', start)
|
|
280
|
+
const normIdx = moduleSrc.indexOf('stripExcessBold(normalizePunctuation(text))', start)
|
|
281
|
+
const scrubIdx = moduleSrc.indexOf('scrubVoice(text)', start)
|
|
268
282
|
expect(start).toBeGreaterThan(0)
|
|
269
283
|
expect(redactIdx).toBeGreaterThan(start)
|
|
270
284
|
expect(normIdx).toBeGreaterThan(redactIdx) // normalize AFTER the reply redact
|
|
@@ -41,10 +41,14 @@ import { readFileSync } from 'node:fs'
|
|
|
41
41
|
import { resolve } from 'node:path'
|
|
42
42
|
import { resolveVaultApprovalPosture } from '../vault-approval-posture.js'
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
45
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
46
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
47
|
+
// same runtime source text.
|
|
48
|
+
const gatewaySrc =
|
|
49
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'gateway.ts'), 'utf-8') +
|
|
50
|
+
'\n' +
|
|
51
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'callback-query-handlers.ts'), 'utf-8')
|
|
48
52
|
|
|
49
53
|
function sliceAccessApproveBlock(): string {
|
|
50
54
|
const fn =
|
|
@@ -73,7 +77,10 @@ describe('vault grant approval posture — module-level wiring', () => {
|
|
|
73
77
|
describe('handleVaultRequestAccessCallback — posture branch', () => {
|
|
74
78
|
it('mints via posture attestation (NOT in-memory passphrase) when posture is telegram-id', () => {
|
|
75
79
|
const approveBlock = sliceAccessApproveBlock()
|
|
76
|
-
|
|
80
|
+
// #2996 Phase 5: inside the extracted handler module the mutable gateway
|
|
81
|
+
// `let` is read via the injected getter (gateway wires it as
|
|
82
|
+
// `getVaultApprovalAuthMode: () => VAULT_APPROVAL_AUTH_MODE`).
|
|
83
|
+
expect(approveBlock).toMatch(/getVaultApprovalAuthMode\(\) === ['"]telegram-id['"]/)
|
|
77
84
|
// Pinned: the call shape MUST be `{ kind: 'posture' }`. If the
|
|
78
85
|
// gateway ever reverts to passing a real passphrase here, the
|
|
79
86
|
// bypass surface returns.
|
|
@@ -138,7 +145,8 @@ describe('handleVaultRequestSaveCallback — posture-attested broker put (#1115
|
|
|
138
145
|
// calls `defaultVaultWritePosture` (posture-attested broker put,
|
|
139
146
|
// no passphrase). Under passphrase mode it keeps the legacy
|
|
140
147
|
// cached-passphrase + shell-out path.
|
|
141
|
-
|
|
148
|
+
// #2996 Phase 5: getter read of the gateway's mutable posture `let`.
|
|
149
|
+
expect(fnBlock).toMatch(/getVaultApprovalAuthMode\(\) === 'telegram-id'/)
|
|
142
150
|
expect(fnBlock).toMatch(/defaultVaultWritePosture\(/)
|
|
143
151
|
// Passphrase-mode branch still present.
|
|
144
152
|
expect(fnBlock).toMatch(/vaultPassphraseCache\.get\(pending\.chat_id\)/)
|
|
@@ -203,7 +211,7 @@ describe('allowlist is the first gate in every vault callback handler', () => {
|
|
|
203
211
|
'mintGrantViaBroker',
|
|
204
212
|
'performVaultAccessApproval',
|
|
205
213
|
'pendingVaultOps.set',
|
|
206
|
-
"
|
|
214
|
+
"getVaultApprovalAuthMode() === 'telegram-id'",
|
|
207
215
|
'attest_via_posture',
|
|
208
216
|
]) {
|
|
209
217
|
expect(
|
|
@@ -23,10 +23,14 @@ import { describe, it, expect } from "vitest";
|
|
|
23
23
|
import { readFileSync } from "node:fs";
|
|
24
24
|
import { resolve } from "node:path";
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
27
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
28
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
29
|
+
// same runtime source text.
|
|
30
|
+
const gatewaySrc =
|
|
31
|
+
readFileSync(resolve(__dirname, "..", "gateway", "gateway.ts"), "utf-8") +
|
|
32
|
+
"\n" +
|
|
33
|
+
readFileSync(resolve(__dirname, "..", "gateway", "callback-query-handlers.ts"), "utf-8");
|
|
30
34
|
|
|
31
35
|
function extractPerformBlock(): string {
|
|
32
36
|
const start = gatewaySrc.indexOf("async function performVaultAccessApproval");
|
|
@@ -22,10 +22,14 @@ import { describe, it, expect } from "vitest";
|
|
|
22
22
|
import { readFileSync } from "node:fs";
|
|
23
23
|
import { resolve } from "node:path";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
26
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
27
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
28
|
+
// same runtime source text.
|
|
29
|
+
const gatewaySrc =
|
|
30
|
+
readFileSync(resolve(__dirname, "..", "gateway", "gateway.ts"), "utf-8") +
|
|
31
|
+
"\n" +
|
|
32
|
+
readFileSync(resolve(__dirname, "..", "gateway", "callback-query-handlers.ts"), "utf-8");
|
|
29
33
|
|
|
30
34
|
function extractPerformBlock(): string {
|
|
31
35
|
const start = gatewaySrc.indexOf("async function performVaultAccessApproval");
|
|
@@ -40,7 +40,14 @@ function readSrc(rel: string): string {
|
|
|
40
40
|
// `telegram-plugin/docs/gateway-server-split.md` for the F4 cleanup notes.
|
|
41
41
|
|
|
42
42
|
describe('/vault grant inline-keyboard wizard — gateway (#227, #262, #265)', () => {
|
|
43
|
-
|
|
43
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
44
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
45
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
46
|
+
// same runtime source text.
|
|
47
|
+
const gatewaySrc =
|
|
48
|
+
readSrc('telegram-plugin/gateway/gateway.ts') +
|
|
49
|
+
'\n' +
|
|
50
|
+
readSrc('telegram-plugin/gateway/callback-query-handlers.ts')
|
|
44
51
|
|
|
45
52
|
it('gateway.ts: dispatches /vault grant to the wizard entry', () => {
|
|
46
53
|
expect(gatewaySrc).toMatch(/\/vault grant/i)
|
|
@@ -33,7 +33,14 @@ import { join, dirname } from 'node:path'
|
|
|
33
33
|
const __dir = dirname(fileURLToPath(import.meta.url))
|
|
34
34
|
const pluginDir = join(__dir, '..')
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
37
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
38
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
39
|
+
// same runtime source text.
|
|
40
|
+
const gatewaySrc =
|
|
41
|
+
readFileSync(join(pluginDir, 'gateway', 'gateway.ts'), 'utf8') +
|
|
42
|
+
'\n' +
|
|
43
|
+
readFileSync(join(pluginDir, 'gateway', 'callback-query-handlers.ts'), 'utf8')
|
|
37
44
|
|
|
38
45
|
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
39
46
|
|
|
@@ -27,10 +27,14 @@ import { describe, it, expect } from "vitest";
|
|
|
27
27
|
import { readFileSync } from "node:fs";
|
|
28
28
|
import { resolve } from "node:path";
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
31
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
32
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
33
|
+
// same runtime source text.
|
|
34
|
+
const gatewaySrc =
|
|
35
|
+
readFileSync(resolve(__dirname, "..", "gateway", "gateway.ts"), "utf-8") +
|
|
36
|
+
"\n" +
|
|
37
|
+
readFileSync(resolve(__dirname, "..", "gateway", "callback-query-handlers.ts"), "utf-8");
|
|
34
38
|
|
|
35
39
|
/** The exported regex literal — what the gateway actually validates against. */
|
|
36
40
|
function extractVaultKeyRegex(): RegExp {
|
|
@@ -23,10 +23,14 @@ const bridgeSrc = readFileSync(
|
|
|
23
23
|
resolve(__dirname, '..', 'bridge', 'bridge.ts'),
|
|
24
24
|
'utf-8',
|
|
25
25
|
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
27
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
28
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
29
|
+
// same runtime source text.
|
|
30
|
+
const gatewaySrc =
|
|
31
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'gateway.ts'), 'utf-8') +
|
|
32
|
+
'\n' +
|
|
33
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'callback-query-handlers.ts'), 'utf-8')
|
|
30
34
|
|
|
31
35
|
describe('vault_request_access (#1012)', () => {
|
|
32
36
|
it('bridge advertises the tool to MCP clients', () => {
|
|
@@ -22,10 +22,14 @@ import { describe, it, expect } from 'vitest'
|
|
|
22
22
|
import { readFileSync } from 'node:fs'
|
|
23
23
|
import { resolve } from 'node:path'
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
26
|
+
// gateway/callback-query-handlers.ts; these pins read the gateway source
|
|
27
|
+
// COMBINED with that module so the wiring assertions keep covering the
|
|
28
|
+
// same runtime source text.
|
|
29
|
+
const gatewaySrc =
|
|
30
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'gateway.ts'), 'utf-8') +
|
|
31
|
+
'\n' +
|
|
32
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'callback-query-handlers.ts'), 'utf-8')
|
|
29
33
|
|
|
30
34
|
describe('vault_request_access — tap-to-unlock-and-approve UX', () => {
|
|
31
35
|
it('declares the passphrase-for-access-approve PendingVaultOp variant', () => {
|