switchroom 0.18.32 → 0.19.0
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/dist/auth-broker/index.js +17 -1
- package/dist/cli/switchroom.js +847 -729
- package/dist/host-control/main.js +18 -2
- package/dist/vault/approvals/kernel-server.js +17 -1
- package/dist/vault/broker/server.js +44 -2
- package/package.json +2 -2
- package/profiles/_base/start.sh.hbs +105 -18
- package/telegram-plugin/dist/gateway/gateway.js +60612 -56998
- package/telegram-plugin/gateway/agent-button-callback-handler.ts +237 -0
- package/telegram-plugin/gateway/ask-callback-handler.ts +92 -0
- package/telegram-plugin/gateway/attachment-message-handlers.ts +152 -0
- package/telegram-plugin/gateway/boot-card.ts +169 -1
- package/telegram-plugin/gateway/bot-commands-model-effort.ts +209 -0
- package/telegram-plugin/gateway/bot-commands-start-info.ts +108 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +124 -0
- package/telegram-plugin/gateway/card-approval-keyboards.test.ts +28 -0
- package/telegram-plugin/gateway/card-tool-handlers.ts +639 -0
- package/telegram-plugin/gateway/checklist-message-handler.ts +107 -0
- package/telegram-plugin/gateway/delivery-confirm-wiring.ts +133 -0
- package/telegram-plugin/gateway/gateway.ts +1347 -6758
- package/telegram-plugin/gateway/inbound-interceptors.ts +1133 -0
- package/telegram-plugin/gateway/inbound-router.ts +400 -0
- package/telegram-plugin/gateway/liveness-wiring.ts +440 -0
- package/telegram-plugin/gateway/media-message-handlers.ts +256 -0
- package/telegram-plugin/gateway/mental-model-propose-card.ts +16 -0
- package/telegram-plugin/gateway/model-command.ts +23 -0
- package/telegram-plugin/gateway/narrative-lane.ts +865 -0
- package/telegram-plugin/gateway/obligation-wiring.ts +333 -0
- package/telegram-plugin/gateway/photo-message-handler.ts +80 -0
- package/telegram-plugin/gateway/pinned-message-handler.ts +86 -0
- package/telegram-plugin/gateway/secret-request-card.test.ts +46 -0
- package/telegram-plugin/gateway/secret-request-card.ts +45 -0
- package/telegram-plugin/gateway/stream-render.ts +2166 -0
- package/telegram-plugin/gateway/turn-end.ts +606 -0
- package/telegram-plugin/gateway/turn-start-surfaces.ts +298 -0
- package/telegram-plugin/gateway/vault-request-access-card.ts +16 -0
- package/telegram-plugin/gateway/vault-request-save-card.test.ts +49 -0
- package/telegram-plugin/gateway/vault-request-save-card.ts +52 -0
- package/telegram-plugin/gateway/voice-message-handler.ts +123 -0
- package/telegram-plugin/gateway/voice-ondemand-callback-handler.ts +204 -0
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +40 -0
- package/telegram-plugin/narrative-dedup.ts +24 -1
- package/telegram-plugin/narrative-flush.ts +2 -2
- package/telegram-plugin/render/render.ts +25 -1
- package/telegram-plugin/status-no-truncate.ts +13 -0
- package/telegram-plugin/subagent-watcher.ts +186 -3
- package/telegram-plugin/tests/activity-card-wiring.test.ts +8 -3
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +18 -3
- package/telegram-plugin/tests/agent-button-callback-handler.test.ts +149 -0
- package/telegram-plugin/tests/ask-callback-handler.test.ts +118 -0
- package/telegram-plugin/tests/attachment-message-handlers.test.ts +135 -0
- package/telegram-plugin/tests/boot-card-routing.test.ts +139 -0
- package/telegram-plugin/tests/bot-commands-model-effort.test.ts +189 -0
- package/telegram-plugin/tests/bot-commands-start-info.test.ts +240 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +15 -6
- package/telegram-plugin/tests/busy-ack-wiring.test.ts +6 -1
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +18 -9
- package/telegram-plugin/tests/callback-query-handlers.test.ts +101 -0
- package/telegram-plugin/tests/card-tool-handlers.test.ts +497 -0
- package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +5 -2
- package/telegram-plugin/tests/checklist-message-handler.test.ts +160 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +47 -10
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +27 -9
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +30 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +39 -18
- package/telegram-plugin/tests/gateway-boot-smoke.test.ts +160 -0
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +3 -7
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +44 -29
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +8 -2
- package/telegram-plugin/tests/gateway-request-secret.test.ts +7 -3
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +20 -10
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +8 -2
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +14 -3
- package/telegram-plugin/tests/inbound-message-types.test.ts +52 -16
- package/telegram-plugin/tests/media-message-handlers.test.ts +276 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -4
- package/telegram-plugin/tests/model-command.test.ts +30 -0
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +27 -9
- package/telegram-plugin/tests/narrative-dedup.test.ts +32 -0
- package/telegram-plugin/tests/narrative-flush.test.ts +6 -2
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +458 -0
- package/telegram-plugin/tests/no-reply-bounded-drain.test.ts +14 -3
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +16 -7
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +32 -8
- package/telegram-plugin/tests/photo-message-handler.test.ts +114 -0
- package/telegram-plugin/tests/pinned-message-handler.test.ts +108 -0
- package/telegram-plugin/tests/render/render.test.ts +42 -0
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +8 -4
- package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +38 -28
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +28 -18
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +22 -8
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +42 -49
- package/telegram-plugin/tests/stop-command.test.ts +22 -12
- package/telegram-plugin/tests/stream-render-golden.test.ts +424 -0
- package/telegram-plugin/tests/subagent-watcher-boot-skip-dead.test.ts +218 -0
- package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +14 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +35 -3
- package/telegram-plugin/tests/turn-flush-safety.test.ts +183 -5
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +9 -4
- package/telegram-plugin/tests/vault-approval-posture.test.ts +8 -2
- package/telegram-plugin/tests/vault-grant-union.test.ts +4 -1
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +16 -5
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +10 -5
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +4 -1
- package/telegram-plugin/tests/vault-subcommands.test.ts +6 -1
- package/telegram-plugin/tests/voice-message-handler.test.ts +111 -0
- package/telegram-plugin/tests/voice-ondemand-callback-handler.test.ts +140 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +86 -19
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +110 -20
- package/telegram-plugin/tests/worker-feed-resume-guard.test.ts +86 -0
- package/telegram-plugin/tool-activity-summary.ts +83 -35
- package/telegram-plugin/turn-flush-safety.ts +80 -14
- package/telegram-plugin/uat/restart-capability.ts +76 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +14 -4
- package/telegram-plugin/uat/scenarios/bridge-flap-resilience-dm.test.ts +11 -1
- package/telegram-plugin/uat/scenarios/cross-turn-pending-progress-dm.test.ts +19 -2
- package/telegram-plugin/uat/scenarios/jtbd-always-on-after-restart-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-deliberate-restart-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-interrupted-turn-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-multipart-render-dm.test.ts +47 -13
- package/telegram-plugin/worker-activity-feed.ts +10 -4
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outcome pins for the media-envelope inbound handlers extracted from
|
|
3
|
+
* gateway.ts (switchroom#2996 P6 cluster A). Each handler is driven with a
|
|
4
|
+
* fake grammy ctx + mock dispatch deps; the assertions pin the EXACT text
|
|
5
|
+
* envelope forwarded to `handleInbound` (and the stderr log line, and the
|
|
6
|
+
* ack/refuse dispatch) — the behaviour the gateway previously produced inline.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
10
|
+
import type { Context } from 'grammy'
|
|
11
|
+
import {
|
|
12
|
+
safeName,
|
|
13
|
+
PASSPORT_REFUSAL_TEXT,
|
|
14
|
+
handleContactMessage,
|
|
15
|
+
handleLocationMessage,
|
|
16
|
+
handleVenueMessage,
|
|
17
|
+
handlePollMessage,
|
|
18
|
+
handleWebAppDataMessage,
|
|
19
|
+
handleUsersSharedMessage,
|
|
20
|
+
handleChatSharedMessage,
|
|
21
|
+
handleSuccessfulPaymentMessage,
|
|
22
|
+
handlePassportDataMessage,
|
|
23
|
+
handleDiceMessage,
|
|
24
|
+
handleGameMessage,
|
|
25
|
+
handleStoryMessage,
|
|
26
|
+
handlePaidMediaMessage,
|
|
27
|
+
type MediaEnvelopeDeps,
|
|
28
|
+
} from '../gateway/media-message-handlers.js'
|
|
29
|
+
|
|
30
|
+
function makeDeps() {
|
|
31
|
+
const handleInbound = vi.fn(async () => {})
|
|
32
|
+
const handleAckOnly = vi.fn(async () => {})
|
|
33
|
+
const handleRefusal = vi.fn(async () => {})
|
|
34
|
+
const logs: string[] = []
|
|
35
|
+
const deps: MediaEnvelopeDeps = {
|
|
36
|
+
handleInbound,
|
|
37
|
+
handleAckOnly,
|
|
38
|
+
handleRefusal,
|
|
39
|
+
log: line => logs.push(line),
|
|
40
|
+
}
|
|
41
|
+
return { deps, handleInbound, handleAckOnly, handleRefusal, logs }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Build a fake ctx whose `message` carries the given content key + a chat id.
|
|
45
|
+
function ctxWith(content: Record<string, unknown>, chatId: number | null = 42): any {
|
|
46
|
+
return {
|
|
47
|
+
message: content,
|
|
48
|
+
chat: chatId == null ? undefined : { id: chatId },
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('safeName', () => {
|
|
53
|
+
it('strips framing/injection characters', () => {
|
|
54
|
+
expect(safeName('a<b>[c]\r\nd;e')).toBe('a_b__c___d_e')
|
|
55
|
+
})
|
|
56
|
+
it('passes undefined through', () => {
|
|
57
|
+
expect(safeName(undefined)).toBeUndefined()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('handleContactMessage', () => {
|
|
62
|
+
it('renders the contact envelope and forwards it', async () => {
|
|
63
|
+
const { deps, handleInbound, logs } = makeDeps()
|
|
64
|
+
const ctx = ctxWith({
|
|
65
|
+
contact: { phone_number: '+15551234', first_name: 'Ada', last_name: 'Love', user_id: 7 },
|
|
66
|
+
})
|
|
67
|
+
await handleContactMessage(ctx, deps)
|
|
68
|
+
expect(handleInbound).toHaveBeenCalledWith(
|
|
69
|
+
ctx,
|
|
70
|
+
'(contact: name="Ada Love" phone="+15551234" user_id=7)',
|
|
71
|
+
undefined,
|
|
72
|
+
)
|
|
73
|
+
expect(logs[0]).toBe('telegram gateway: inbound contact from chat=42\n')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('falls back to ? for missing fields and omits user_id', async () => {
|
|
77
|
+
const { deps, handleInbound } = makeDeps()
|
|
78
|
+
const ctx = ctxWith({ contact: {} })
|
|
79
|
+
await handleContactMessage(ctx, deps)
|
|
80
|
+
expect(handleInbound).toHaveBeenCalledWith(ctx, '(contact: name="?" phone="?")', undefined)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('logs and swallows a builder error without forwarding', async () => {
|
|
84
|
+
const { deps, handleInbound, logs } = makeDeps()
|
|
85
|
+
// ctx.message is null → property access throws inside try.
|
|
86
|
+
const ctx = { message: null, chat: { id: 42 } } as unknown as Context
|
|
87
|
+
await handleContactMessage(ctx as any, deps)
|
|
88
|
+
expect(handleInbound).not.toHaveBeenCalled()
|
|
89
|
+
expect(logs.some(l => l.startsWith('telegram gateway: contact handler error:'))).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('handleLocationMessage', () => {
|
|
94
|
+
it('renders lat/lon at 6dp with live_period', async () => {
|
|
95
|
+
const { deps, handleInbound } = makeDeps()
|
|
96
|
+
const ctx = ctxWith({ location: { latitude: 12.3, longitude: -45.6, live_period: 60 } })
|
|
97
|
+
await handleLocationMessage(ctx, deps)
|
|
98
|
+
expect(handleInbound).toHaveBeenCalledWith(
|
|
99
|
+
ctx,
|
|
100
|
+
'(location: lat=12.300000 lon=-45.600000 live_period=60s)',
|
|
101
|
+
undefined,
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
it('omits live_period when absent and ? for non-numeric coords', async () => {
|
|
105
|
+
const { deps, handleInbound } = makeDeps()
|
|
106
|
+
const ctx = ctxWith({ location: {} })
|
|
107
|
+
await handleLocationMessage(ctx, deps)
|
|
108
|
+
expect(handleInbound).toHaveBeenCalledWith(ctx, '(location: lat=? lon=?)', undefined)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('handleVenueMessage', () => {
|
|
113
|
+
it('renders the venue envelope', async () => {
|
|
114
|
+
const { deps, handleInbound } = makeDeps()
|
|
115
|
+
const ctx = ctxWith({
|
|
116
|
+
venue: { title: 'Cafe', address: '1 St', location: { latitude: 1, longitude: 2 } },
|
|
117
|
+
})
|
|
118
|
+
await handleVenueMessage(ctx, deps)
|
|
119
|
+
expect(handleInbound).toHaveBeenCalledWith(
|
|
120
|
+
ctx,
|
|
121
|
+
'(venue: title="Cafe" address="1 St" lat=1.000000 lon=2.000000)',
|
|
122
|
+
undefined,
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('handlePollMessage', () => {
|
|
128
|
+
it('renders question, count, anonymity and first-10 choices', async () => {
|
|
129
|
+
const { deps, handleInbound } = makeDeps()
|
|
130
|
+
const ctx = ctxWith({
|
|
131
|
+
poll: {
|
|
132
|
+
question: 'Pick',
|
|
133
|
+
is_anonymous: true,
|
|
134
|
+
options: [{ text: 'A' }, { text: 'B' }],
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
await handlePollMessage(ctx, deps)
|
|
138
|
+
expect(handleInbound).toHaveBeenCalledWith(
|
|
139
|
+
ctx,
|
|
140
|
+
'(poll: question="Pick" options=2 anonymous choices=[A | B])',
|
|
141
|
+
undefined,
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('handleWebAppDataMessage', () => {
|
|
147
|
+
it('renders JSON-encoded data and logs byte count', async () => {
|
|
148
|
+
const { deps, handleInbound, logs } = makeDeps()
|
|
149
|
+
const ctx = ctxWith({ web_app_data: { data: 'hi', button_text: 'Go' } })
|
|
150
|
+
await handleWebAppDataMessage(ctx, deps)
|
|
151
|
+
expect(handleInbound).toHaveBeenCalledWith(
|
|
152
|
+
ctx,
|
|
153
|
+
'(web_app_data: button="Go" data="hi")',
|
|
154
|
+
undefined,
|
|
155
|
+
)
|
|
156
|
+
expect(logs[0]).toContain('button="Go" bytes=2')
|
|
157
|
+
})
|
|
158
|
+
it('truncates oversized data at 4096 chars', async () => {
|
|
159
|
+
const { deps, handleInbound } = makeDeps()
|
|
160
|
+
const big = 'x'.repeat(5000)
|
|
161
|
+
const ctx = ctxWith({ web_app_data: { data: big, button_text: 'B' } })
|
|
162
|
+
await handleWebAppDataMessage(ctx, deps)
|
|
163
|
+
const arg = (handleInbound as any).mock.calls[0][1] as string
|
|
164
|
+
expect(arg).toContain('…(truncated)')
|
|
165
|
+
expect(arg).toContain(JSON.stringify('x'.repeat(4096) + '…(truncated)'))
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('handleUsersSharedMessage', () => {
|
|
170
|
+
it('renders shared user ids and count', async () => {
|
|
171
|
+
const { deps, handleInbound } = makeDeps()
|
|
172
|
+
const ctx = ctxWith({ users_shared: { request_id: 9, users: [{ user_id: 1 }, { user_id: 2 }] } })
|
|
173
|
+
await handleUsersSharedMessage(ctx, deps)
|
|
174
|
+
expect(handleInbound).toHaveBeenCalledWith(
|
|
175
|
+
ctx,
|
|
176
|
+
'(users_shared: request_id=9 user_ids=[1,2] count=2)',
|
|
177
|
+
undefined,
|
|
178
|
+
)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('handleChatSharedMessage', () => {
|
|
183
|
+
it('renders shared chat with optional title', async () => {
|
|
184
|
+
const { deps, handleInbound } = makeDeps()
|
|
185
|
+
const ctx = ctxWith({ chat_shared: { request_id: 3, chat_id: -100, title: 'Grp' } })
|
|
186
|
+
await handleChatSharedMessage(ctx, deps)
|
|
187
|
+
expect(handleInbound).toHaveBeenCalledWith(
|
|
188
|
+
ctx,
|
|
189
|
+
'(chat_shared: request_id=3 chat_id=-100 title="Grp")',
|
|
190
|
+
undefined,
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
it('omits title when absent', async () => {
|
|
194
|
+
const { deps, handleInbound } = makeDeps()
|
|
195
|
+
const ctx = ctxWith({ chat_shared: { request_id: 3, chat_id: -100 } })
|
|
196
|
+
await handleChatSharedMessage(ctx, deps)
|
|
197
|
+
expect(handleInbound).toHaveBeenCalledWith(
|
|
198
|
+
ctx,
|
|
199
|
+
'(chat_shared: request_id=3 chat_id=-100)',
|
|
200
|
+
undefined,
|
|
201
|
+
)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('handleSuccessfulPaymentMessage', () => {
|
|
206
|
+
it('logs structured fields and ack-only-warns, never forwarding', async () => {
|
|
207
|
+
const { deps, handleInbound, handleAckOnly, logs } = makeDeps()
|
|
208
|
+
const ctx = ctxWith({
|
|
209
|
+
successful_payment: {
|
|
210
|
+
currency: 'USD',
|
|
211
|
+
total_amount: 500,
|
|
212
|
+
invoice_payload: 'order-1',
|
|
213
|
+
provider_payment_charge_id: 'pc',
|
|
214
|
+
telegram_payment_charge_id: 'tc',
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
await handleSuccessfulPaymentMessage(ctx, deps)
|
|
218
|
+
expect(handleInbound).not.toHaveBeenCalled()
|
|
219
|
+
expect(handleAckOnly).toHaveBeenCalledWith(ctx, 'successful_payment', { warn: true })
|
|
220
|
+
expect(logs[0]).toContain('currency=USD total_amount=500')
|
|
221
|
+
expect(logs[0]).toContain('payload="order-1"')
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe('ack-only leaves (dice/game/story/paid_media)', () => {
|
|
226
|
+
it('handleDiceMessage logs and reacts 🎲, never forwarding', async () => {
|
|
227
|
+
const { deps, handleInbound, handleAckOnly, logs } = makeDeps()
|
|
228
|
+
const ctx = ctxWith({ dice: { emoji: '🎲', value: 4 } })
|
|
229
|
+
await handleDiceMessage(ctx, deps)
|
|
230
|
+
expect(handleInbound).not.toHaveBeenCalled()
|
|
231
|
+
expect(handleAckOnly).toHaveBeenCalledWith(ctx, 'dice', { emoji: '🎲' })
|
|
232
|
+
expect(logs[0]).toBe('telegram gateway: inbound dice from chat=42\n')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('handleGameMessage logs and acks with no options', async () => {
|
|
236
|
+
const { deps, handleAckOnly, logs } = makeDeps()
|
|
237
|
+
const ctx = ctxWith({ game: {} })
|
|
238
|
+
await handleGameMessage(ctx, deps)
|
|
239
|
+
expect(handleAckOnly).toHaveBeenCalledWith(ctx, 'game')
|
|
240
|
+
expect(logs[0]).toBe('telegram gateway: inbound game from chat=42\n')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('handleStoryMessage logs and acks with no options', async () => {
|
|
244
|
+
const { deps, handleAckOnly, logs } = makeDeps()
|
|
245
|
+
const ctx = ctxWith({ story: {} })
|
|
246
|
+
await handleStoryMessage(ctx, deps)
|
|
247
|
+
expect(handleAckOnly).toHaveBeenCalledWith(ctx, 'story')
|
|
248
|
+
expect(logs[0]).toBe('telegram gateway: inbound story from chat=42\n')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('handlePaidMediaMessage logs and acks warn:true', async () => {
|
|
252
|
+
const { deps, handleInbound, handleAckOnly, logs } = makeDeps()
|
|
253
|
+
const ctx = ctxWith({ paid_media: {} })
|
|
254
|
+
await handlePaidMediaMessage(ctx, deps)
|
|
255
|
+
expect(handleInbound).not.toHaveBeenCalled()
|
|
256
|
+
expect(handleAckOnly).toHaveBeenCalledWith(ctx, 'paid_media', { warn: true })
|
|
257
|
+
expect(logs[0]).toBe('telegram gateway: inbound paid_media from chat=42\n')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('falls back to ? for a missing chat id', async () => {
|
|
261
|
+
const { deps, logs } = makeDeps()
|
|
262
|
+
const ctx = ctxWith({ dice: {} }, null)
|
|
263
|
+
await handleDiceMessage(ctx, deps)
|
|
264
|
+
expect(logs[0]).toBe('telegram gateway: inbound dice from chat=?\n')
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
describe('handlePassportDataMessage', () => {
|
|
269
|
+
it('refuses with the fixed message and never forwards', async () => {
|
|
270
|
+
const { deps, handleInbound, handleRefusal } = makeDeps()
|
|
271
|
+
const ctx = ctxWith({ passport_data: {} })
|
|
272
|
+
await handlePassportDataMessage(ctx, deps)
|
|
273
|
+
expect(handleInbound).not.toHaveBeenCalled()
|
|
274
|
+
expect(handleRefusal).toHaveBeenCalledWith(ctx, 'passport_data', PASSPORT_REFUSAL_TEXT)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
@@ -20,13 +20,17 @@ import { readFileSync } from 'node:fs'
|
|
|
20
20
|
import { resolve } from 'node:path'
|
|
21
21
|
|
|
22
22
|
// #2996 Phase 5: the callback-query handler families moved verbatim to
|
|
23
|
-
// gateway/callback-query-handlers.ts
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// gateway/callback-query-handlers.ts, and the executeMentalModelPropose tool
|
|
24
|
+
// handler to gateway/card-tool-handlers.ts (P5-tail); these pins read the
|
|
25
|
+
// gateway source COMBINED with those modules so the wiring assertions keep
|
|
26
|
+
// covering the same runtime source text. (The MENTAL_MODEL_* cap constant
|
|
27
|
+
// DEFINITIONS stay in gateway.ts and are injected.)
|
|
26
28
|
const gatewaySrc =
|
|
27
29
|
readFileSync(resolve(__dirname, '..', 'gateway', 'gateway.ts'), 'utf-8') +
|
|
28
30
|
'\n' +
|
|
29
|
-
readFileSync(resolve(__dirname, '..', 'gateway', 'callback-query-handlers.ts'), 'utf-8')
|
|
31
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'callback-query-handlers.ts'), 'utf-8') +
|
|
32
|
+
'\n' +
|
|
33
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'card-tool-handlers.ts'), 'utf-8')
|
|
30
34
|
|
|
31
35
|
function proposeCallbackBlock(): string {
|
|
32
36
|
return (
|
|
@@ -444,6 +444,36 @@ describe("SR_MODEL_ALIASES / expandSrAlias", () => {
|
|
|
444
444
|
it("every alias target is a sr-* id", () => {
|
|
445
445
|
for (const target of Object.values(SR_MODEL_ALIASES)) expect(isSrModel(target)).toBe(true);
|
|
446
446
|
});
|
|
447
|
+
it("2026-07-19 grok/kimi/gpt flagship buttons expand to their sr-* ids", () => {
|
|
448
|
+
expect(expandSrAlias("grok")).toBe("sr-grok-4.5");
|
|
449
|
+
expect(expandSrAlias("kimi")).toBe("sr-kimi-k3");
|
|
450
|
+
expect(expandSrAlias("gpt")).toBe("sr-gpt-5.6-sol");
|
|
451
|
+
});
|
|
452
|
+
it("per-tier GPT-5.6 buttons sol/terra/luna expand to their sr-* ids", () => {
|
|
453
|
+
expect(expandSrAlias("sol")).toBe("sr-gpt-5.6-sol");
|
|
454
|
+
expect(expandSrAlias("terra")).toBe("sr-gpt-5.6-terra");
|
|
455
|
+
expect(expandSrAlias("luna")).toBe("sr-gpt-5.6-luna");
|
|
456
|
+
for (const a of ["sol", "terra", "luna"]) expect(isOfflineTrustedModelToken(a)).toBe(true);
|
|
457
|
+
});
|
|
458
|
+
it("new flagship buttons are offline-trustable carriers (#3042 gate)", () => {
|
|
459
|
+
for (const a of ["grok", "kimi", "gpt"]) expect(isOfflineTrustedModelToken(a)).toBe(true);
|
|
460
|
+
for (const id of ["sr-grok-4.5", "sr-kimi-k3", "sr-gpt-5.6-sol"])
|
|
461
|
+
expect(isOfflineTrustedModelToken(id)).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
it("labels every new grok/kimi/gpt-5.6 sr-* id (typeable tiers included)", () => {
|
|
464
|
+
for (const id of [
|
|
465
|
+
"sr-grok-4.5",
|
|
466
|
+
"sr-grok-4.3",
|
|
467
|
+
"sr-kimi-k3",
|
|
468
|
+
"sr-kimi-k2.7-code",
|
|
469
|
+
"sr-gpt-5.6-sol",
|
|
470
|
+
"sr-gpt-5.6-terra",
|
|
471
|
+
"sr-gpt-5.6-luna",
|
|
472
|
+
]) {
|
|
473
|
+
expect(SR_MODEL_LABELS[id]).toBeTypeOf("string");
|
|
474
|
+
expect(isValidModelArg(id)).toBe(true);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
447
477
|
});
|
|
448
478
|
|
|
449
479
|
describe("classifyDiscoveredOptions", () => {
|
|
@@ -19,10 +19,26 @@ const sendPathSrc = readFileSync(
|
|
|
19
19
|
'utf-8',
|
|
20
20
|
)
|
|
21
21
|
const sendReplyFn = sendPathSrc.split('export async function sendReply(')[1]?.split('\nexport ')[0] ?? ''
|
|
22
|
-
const gatewaySrc =
|
|
23
|
-
resolve(__dirname, '..', 'gateway', 'gateway.ts'),
|
|
22
|
+
const gatewaySrc =
|
|
23
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'gateway.ts'), 'utf-8') +
|
|
24
|
+
'\n' +
|
|
25
|
+
// P7 PR-10 (#2996): the InboundMessage envelope assembly (origin_turn_id,
|
|
26
|
+
// topic_scope, meta lane) moved verbatim into gateway/inbound-router.ts
|
|
27
|
+
// (buildInboundEnvelope) — include it in the scraped corpus.
|
|
28
|
+
readFileSync(resolve(__dirname, '..', 'gateway', 'inbound-router.ts'), 'utf-8')
|
|
29
|
+
// #2996 P4-A: the enqueue handler (turn ctor: `const turnId = deriveTurnId`,
|
|
30
|
+
// `rememberRecentTurn(next)`, `promoteQueuedStatus`) moved VERBATIM into
|
|
31
|
+
// stream-render.ts with handleSessionEvent. Enqueue-seam assertions span both.
|
|
32
|
+
// #2996 P8 PR-B: the turn-end funnel bodies moved verbatim to turn-end.ts.
|
|
33
|
+
const turnEndSrc = readFileSync(
|
|
34
|
+
resolve(__dirname, '..', 'gateway', 'turn-end.ts'),
|
|
24
35
|
'utf-8',
|
|
25
36
|
)
|
|
37
|
+
const streamSrc = readFileSync(
|
|
38
|
+
resolve(__dirname, '..', 'gateway', 'stream-render.ts'),
|
|
39
|
+
'utf-8',
|
|
40
|
+
)
|
|
41
|
+
const gatewayAndStreamSrc = gatewaySrc + '\n' + streamSrc
|
|
26
42
|
const bridgeSrc = readFileSync(
|
|
27
43
|
resolve(__dirname, '..', 'bridge', 'bridge.ts'),
|
|
28
44
|
'utf-8',
|
|
@@ -38,12 +54,12 @@ const deriveTurnIdSrc = readFileSync(
|
|
|
38
54
|
describe('component 3 — turn-origin reply routing', () => {
|
|
39
55
|
it('CurrentTurn carries a turnId, and the enqueue handler initialises it', () => {
|
|
40
56
|
expect(gatewaySrc).toMatch(/turnId: string/)
|
|
41
|
-
expect(
|
|
42
|
-
expect(
|
|
57
|
+
expect(gatewayAndStreamSrc).toMatch(/const turnId\s*=\s*\n?\s*deriveTurnId\(/)
|
|
58
|
+
expect(gatewayAndStreamSrc).toMatch(/rememberRecentTurn\(next\)/)
|
|
43
59
|
})
|
|
44
60
|
|
|
45
61
|
it('the inbound meta stamps origin_turn_id derived from chat/thread/messageId', () => {
|
|
46
|
-
expect(gatewaySrc).toMatch(/const originTurnId = deriveTurnId\(chat_id, messageThreadId \?\? null, msgId\)/)
|
|
62
|
+
expect(gatewaySrc).toMatch(/const originTurnId = deriveTurnId\((?:p\.)?chat_id, (?:p\.)?messageThreadId \?\? null, (?:p\.)?msgId\)/)
|
|
47
63
|
expect(gatewaySrc).toMatch(/origin_turn_id: originTurnId/)
|
|
48
64
|
})
|
|
49
65
|
|
|
@@ -129,7 +145,9 @@ describe('component 4 — per-turn topic framing', () => {
|
|
|
129
145
|
expect(gatewaySrc).toMatch(/TOPIC_FRAMING_ENABLED/)
|
|
130
146
|
expect(gatewaySrc).toMatch(/topic_scope: topicScope/)
|
|
131
147
|
// Only for topic inbounds — DMs get nothing.
|
|
132
|
-
|
|
148
|
+
// P7 PR-10: the guard reads the injected constant + params inside
|
|
149
|
+
// buildInboundEnvelope (topicFramingEnabled / p.messageThreadId).
|
|
150
|
+
expect(gatewaySrc).toMatch(/(?:TOPIC_FRAMING_ENABLED|p\.topicFramingEnabled) && (?:p\.)?messageThreadId != null/)
|
|
133
151
|
})
|
|
134
152
|
|
|
135
153
|
it('the bridge instructions frame each channel message as the current topic', () => {
|
|
@@ -151,17 +169,17 @@ describe('component 5 — queued-status UX (delete-on-answer)', () => {
|
|
|
151
169
|
})
|
|
152
170
|
|
|
153
171
|
it('Hook B promotes the placeholder to "On it" when the buffered turn starts', () => {
|
|
154
|
-
expect(
|
|
172
|
+
expect(gatewayAndStreamSrc).toMatch(/promoteQueuedStatus\(ev\.chatId, enqThreadIdNum\)/)
|
|
155
173
|
})
|
|
156
174
|
|
|
157
175
|
it('Hook C reaps the placeholder on the answer (executeReply / stream)', () => {
|
|
158
|
-
const reapCalls = gatewaySrc.match(/reapQueuedStatus\(/g) ?? []
|
|
176
|
+
const reapCalls = (gatewaySrc + turnEndSrc).match(/reapQueuedStatus\(/g) ?? []
|
|
159
177
|
// definition + executeReply + purge cleanup (2 branches)
|
|
160
178
|
expect(reapCalls.length).toBeGreaterThanOrEqual(4)
|
|
161
179
|
})
|
|
162
180
|
|
|
163
181
|
it('purgeReactionTracking reaps the placeholder on abnormal turn-end (defense-in-depth)', () => {
|
|
164
|
-
const fn =
|
|
182
|
+
const fn = turnEndSrc.split('function purgeReactionTracking')[1]?.split('\nfunction ')[0] ?? ''
|
|
165
183
|
expect(fn).toMatch(/reapQueuedStatus\(/)
|
|
166
184
|
// Reap sits alongside the activeReactionMsgIds cleanup.
|
|
167
185
|
expect(fn).toMatch(/activeReactionMsgIds\.delete\(key\)/)
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
normalizeNarrative,
|
|
4
4
|
prefixSimilarity,
|
|
5
5
|
isDraftOfReply,
|
|
6
|
+
isReplyTool,
|
|
6
7
|
DRAFT_SUPPRESS_THRESHOLD,
|
|
7
8
|
REPLY_TOOLS,
|
|
8
9
|
} from '../narrative-dedup.js'
|
|
@@ -18,6 +19,37 @@ describe('narrative-dedup', () => {
|
|
|
18
19
|
expect(REPLY_TOOLS.has('Bash')).toBe(false)
|
|
19
20
|
})
|
|
20
21
|
|
|
22
|
+
describe('isReplyTool (#3231 prefix-aware matching)', () => {
|
|
23
|
+
it('matches the PREFIXED prod wire shape (what real jsonl actually carries)', () => {
|
|
24
|
+
// The whole point of #3231: production jsonl carries mcp__…__telegram__<tool>,
|
|
25
|
+
// so a bare REPLY_TOOLS.has(name) was always false in prod → suppression inert.
|
|
26
|
+
expect(isReplyTool('mcp__switchroom-telegram__stream_reply')).toBe(true)
|
|
27
|
+
expect(isReplyTool('mcp__switchroom-telegram__reply')).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('matches under ANY registration key (clerk-telegram, forks)', () => {
|
|
31
|
+
expect(isReplyTool('mcp__clerk-telegram__stream_reply')).toBe(true)
|
|
32
|
+
expect(isReplyTool('mcp__my-fork-telegram__reply')).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('still matches BARE names (non-MCP sources)', () => {
|
|
36
|
+
expect(isReplyTool('reply')).toBe(true)
|
|
37
|
+
expect(isReplyTool('stream_reply')).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('rejects non-reply tools, prefixed or bare', () => {
|
|
41
|
+
expect(isReplyTool('Bash')).toBe(false)
|
|
42
|
+
expect(isReplyTool('mcp__switchroom-telegram__react')).toBe(false)
|
|
43
|
+
expect(isReplyTool('mcp__switchroom-telegram__edit_message')).toBe(false)
|
|
44
|
+
expect(isReplyTool('mcp__hindsight__retain')).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('is null/undefined safe', () => {
|
|
48
|
+
expect(isReplyTool(null)).toBe(false)
|
|
49
|
+
expect(isReplyTool(undefined)).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
21
53
|
describe('normalizeNarrative', () => {
|
|
22
54
|
it('strips markdown emphasis/heading/quote marks, collapses whitespace, lowercases', () => {
|
|
23
55
|
expect(normalizeNarrative('**Bold** _italic_ `code`')).toBe('bold italic code')
|
|
@@ -101,7 +101,10 @@ describe('anti-double-print (deferred path) — a draft-then-send reply is suppr
|
|
|
101
101
|
const answer = 'The build is green — all 412 tests pass.'
|
|
102
102
|
ctrl.stage(answer) // the model drafting its answer just before reply()
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
// #3231: feed the PREFIXED prod wire shape (mcp__…__reply). Before the
|
|
105
|
+
// isReplyTool fix a bare REPLY_TOOLS.has() missed this and the draft was
|
|
106
|
+
// WRONGLY shown — the suppression path was inert on the real wire shape.
|
|
107
|
+
ctrl.resolveOnTool('mcp__switchroom-telegram__reply', { text: answer }) // draft-then-send → SUPPRESS
|
|
105
108
|
expect(show).not.toHaveBeenCalled()
|
|
106
109
|
expect(retractShown).not.toHaveBeenCalled() // nothing was shown to retract
|
|
107
110
|
expect(scheduler.isArmed).toBe(false)
|
|
@@ -128,7 +131,8 @@ describe('anti-double-print (TIMER path) — a timer-painted draft is retracted'
|
|
|
128
131
|
|
|
129
132
|
// The reply finally lands and IS that block → must be retracted, not left
|
|
130
133
|
// on the card to double-print against the canonical reply.
|
|
131
|
-
|
|
134
|
+
// #3231: prefixed prod wire shape.
|
|
135
|
+
ctrl.resolveOnTool('mcp__switchroom-telegram__stream_reply', { text: answer })
|
|
132
136
|
expect(retractShown).toHaveBeenCalledTimes(1)
|
|
133
137
|
expect(retractShown).toHaveBeenCalledWith(answer)
|
|
134
138
|
expect(show).toHaveBeenCalledTimes(1) // not re-shown
|