switchroom 0.16.23 → 0.16.24
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/agent-scheduler/index.js +80 -80
- package/dist/auth-broker/index.js +80 -80
- package/dist/cli/autoaccept-poll.js +8 -8
- package/dist/cli/drive-write-pretool.mjs +10 -10
- package/dist/cli/notion-write-pretool.mjs +82 -82
- package/dist/cli/self-improve-apply-guard-pretool.mjs +6 -0
- package/dist/cli/skill-validate-pretool.mjs +2936 -119
- package/dist/cli/switchroom.js +671 -430
- package/dist/host-control/main.js +156 -156
- package/dist/vault/approvals/kernel-server.js +82 -82
- package/dist/vault/broker/server.js +83 -83
- package/package.json +5 -5
- package/telegram-plugin/answer-stream.ts +13 -43
- package/telegram-plugin/auth-snapshot-format.ts +27 -30
- package/telegram-plugin/auto-fallback-fleet.ts +6 -11
- package/telegram-plugin/card-format.ts +28 -25
- package/telegram-plugin/credits-watch.ts +5 -10
- package/telegram-plugin/dist/bridge/bridge.js +112 -112
- package/telegram-plugin/dist/gateway/gateway.js +1855 -2077
- package/telegram-plugin/dist/server.js +160 -160
- package/telegram-plugin/draft-stream.ts +4 -4
- package/telegram-plugin/format.ts +101 -662
- package/telegram-plugin/gateway/approval-callback.ts +2 -3
- package/telegram-plugin/gateway/approval-card.test.ts +17 -4
- package/telegram-plugin/gateway/approval-card.ts +16 -6
- package/telegram-plugin/gateway/approvals-commands.ts +18 -24
- package/telegram-plugin/gateway/auth-command.ts +74 -74
- package/telegram-plugin/gateway/auth-line.ts +5 -15
- package/telegram-plugin/gateway/boot-card.ts +20 -22
- package/telegram-plugin/gateway/boot-version.ts +3 -2
- package/telegram-plugin/gateway/config-approval-handler.test.ts +35 -33
- package/telegram-plugin/gateway/config-approval-handler.ts +24 -24
- package/telegram-plugin/gateway/config-snapshot.ts +9 -9
- package/telegram-plugin/gateway/diff-preview-card.test.ts +8 -8
- package/telegram-plugin/gateway/diff-preview-card.ts +2 -5
- package/telegram-plugin/gateway/disconnect-flush.ts +0 -4
- package/telegram-plugin/gateway/effort-command.ts +17 -17
- package/telegram-plugin/gateway/folder-picker-handler.test.ts +8 -2
- package/telegram-plugin/gateway/folder-picker-handler.ts +3 -4
- package/telegram-plugin/gateway/gateway.ts +751 -624
- package/telegram-plugin/gateway/inject-handler.test.ts +15 -13
- package/telegram-plugin/gateway/inject-handler.ts +5 -5
- package/telegram-plugin/gateway/ipc-protocol.ts +33 -1
- package/telegram-plugin/gateway/ipc-server.ts +28 -0
- package/telegram-plugin/gateway/linear-activity.ts +16 -14
- package/telegram-plugin/gateway/linear-setup.ts +1 -1
- package/telegram-plugin/gateway/model-command.ts +25 -25
- package/telegram-plugin/gateway/oversize-card-body.ts +6 -7
- package/telegram-plugin/gateway/skill-proposal-card.ts +167 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +19 -13
- package/telegram-plugin/issues-card.ts +6 -7
- package/telegram-plugin/model-unavailable.ts +8 -12
- package/telegram-plugin/operator-events-history.ts +1 -1
- package/telegram-plugin/operator-events.ts +24 -28
- package/telegram-plugin/package.json +1 -1
- package/telegram-plugin/pending-work-progress.ts +36 -36
- package/telegram-plugin/permission-title.ts +36 -20
- package/telegram-plugin/pty-partial-handler.ts +5 -13
- package/telegram-plugin/quota-check.ts +5 -5
- package/telegram-plugin/quota-watch.ts +13 -18
- package/telegram-plugin/recent-outbound-dedup.ts +5 -5
- package/telegram-plugin/registry/turns-schema.ts +43 -3
- package/telegram-plugin/retry-api-call.ts +15 -7
- package/telegram-plugin/rich-send.ts +57 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +6 -6
- package/telegram-plugin/secret-detect/vault-error.ts +29 -22
- package/telegram-plugin/shared/bot-runtime.ts +23 -6
- package/telegram-plugin/silence-poke.ts +26 -69
- package/telegram-plugin/slot-banner-driver.ts +9 -6
- package/telegram-plugin/slot-banner.ts +5 -8
- package/telegram-plugin/steering.ts +0 -4
- package/telegram-plugin/stream-controller.ts +59 -62
- package/telegram-plugin/stream-reply-handler.ts +40 -97
- package/telegram-plugin/subagent-watcher.ts +2 -2
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +5 -2
- package/telegram-plugin/tests/answer-stream.test.ts +54 -63
- package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +3 -2
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +19 -18
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +15 -13
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +27 -12
- package/telegram-plugin/tests/boot-card-render.test.ts +59 -48
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/bot-api.harness.ts +23 -1
- package/telegram-plugin/tests/bot-runtime.test.ts +23 -18
- package/telegram-plugin/tests/card-format.test.ts +6 -4
- package/telegram-plugin/tests/config-snapshot.test.ts +1 -1
- package/telegram-plugin/tests/credits-watch.test.ts +5 -5
- package/telegram-plugin/tests/fake-bot-api.ts +58 -4
- package/telegram-plugin/tests/finalize-callback.test.ts +11 -9
- package/telegram-plugin/tests/foreground-nesting.test.ts +1 -1
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +3 -13
- package/telegram-plugin/tests/issues-card.test.ts +15 -12
- package/telegram-plugin/tests/linear-agent-activity.test.ts +8 -5
- package/telegram-plugin/tests/model-command.test.ts +2 -2
- package/telegram-plugin/tests/model-unavailable.test.ts +13 -13
- package/telegram-plugin/tests/multi-turn-continuity.test.ts +6 -10
- package/telegram-plugin/tests/operator-events.test.ts +7 -9
- package/telegram-plugin/tests/pending-work-progress.test.ts +20 -21
- package/telegram-plugin/tests/permission-title.test.ts +45 -41
- package/telegram-plugin/tests/pty-partial-handler.test.ts +8 -8
- package/telegram-plugin/tests/quota-check.test.ts +3 -3
- package/telegram-plugin/tests/quota-watch.test.ts +8 -4
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +4 -3
- package/telegram-plugin/tests/silence-poke.test.ts +75 -112
- package/telegram-plugin/tests/single-mode-stream-reply.test.ts +137 -0
- package/telegram-plugin/tests/skill-proposal-card.test.ts +103 -0
- package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +36 -24
- package/telegram-plugin/tests/slot-banner.test.ts +9 -6
- package/telegram-plugin/tests/status-accent.test.ts +29 -32
- package/telegram-plugin/tests/{stream-controller-html-fallback.test.ts → stream-controller-parse-fallback.test.ts} +40 -42
- package/telegram-plugin/tests/stream-controller.test.ts +63 -52
- package/telegram-plugin/tests/stream-reply-error-paths.test.ts +43 -38
- package/telegram-plugin/tests/stream-reply-handler.test.ts +122 -249
- package/telegram-plugin/tests/streaming-e2e.test.ts +35 -30
- package/telegram-plugin/tests/streaming-orchestration.test.ts +29 -28
- package/telegram-plugin/tests/telegram-format.test.ts +120 -1083
- package/telegram-plugin/tests/tool-activity-summary.test.ts +144 -145
- package/telegram-plugin/tests/welcome-text.test.ts +72 -65
- package/telegram-plugin/tests/worker-activity-feed.test.ts +119 -137
- package/telegram-plugin/text-voice-scrub.ts +8 -11
- package/telegram-plugin/tool-activity-summary.ts +29 -29
- package/telegram-plugin/welcome-text.ts +82 -83
- package/telegram-plugin/worker-activity-feed.ts +2 -3
- package/telegram-plugin/html-sanitize.ts +0 -244
- package/telegram-plugin/tests/html-sanitize.test.ts +0 -146
- package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -162
|
@@ -72,10 +72,9 @@ function setup(opts: { progressCardActive?: boolean } = {}): Fixture {
|
|
|
72
72
|
? { activeDraftStreams: map, suppressPtyPreview: suppress }
|
|
73
73
|
: { activeDraftStreams: map }
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
75
|
+
// #2669: PTY previews are raw terminal output sent LITERALLY (plain
|
|
76
|
+
// sendMessage). The handler no longer takes a renderText.
|
|
77
|
+
const pty = createPtyPartialHandler(ptyState, { bot })
|
|
79
78
|
|
|
80
79
|
const callStreamReply = (args: {
|
|
81
80
|
chat_id: string
|
|
@@ -85,13 +84,13 @@ function setup(opts: { progressCardActive?: boolean } = {}): Fixture {
|
|
|
85
84
|
}) =>
|
|
86
85
|
handleStreamReply(args, srState, {
|
|
87
86
|
bot,
|
|
88
|
-
markdownToHtml: (t) => `<b>${t}</b>`, // stream_reply: bold
|
|
89
|
-
escapeMarkdownV2: (t) => t,
|
|
90
87
|
repairEscapedWhitespace: (t) => t,
|
|
91
88
|
assertAllowedChat: () => {},
|
|
92
89
|
resolveThreadId: () => undefined,
|
|
93
90
|
disableLinkPreview: true,
|
|
94
|
-
|
|
91
|
+
// Anything other than the literal 'text' is the rich-markdown path —
|
|
92
|
+
// stream_reply ships the RAW markdown via sendRichMessage (#2669).
|
|
93
|
+
defaultFormat: 'markdown',
|
|
95
94
|
logStreamingEvent: () => {},
|
|
96
95
|
historyEnabled: false,
|
|
97
96
|
recordOutbound: () => {},
|
|
@@ -129,12 +128,14 @@ describe('streaming e2e smoke', () => {
|
|
|
129
128
|
f.pty.onPartial('drafting…')
|
|
130
129
|
await microtaskFlush()
|
|
131
130
|
|
|
131
|
+
// PTY preview → literal plain sendMessage with the raw terminal text.
|
|
132
132
|
expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
133
133
|
expect(f.bot.api.sendMessage.mock.calls[0][0]).toBe('42')
|
|
134
|
-
expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('
|
|
134
|
+
expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('drafting…')
|
|
135
135
|
|
|
136
|
-
// Model calls stream_reply with canonical text.
|
|
137
|
-
//
|
|
136
|
+
// Model calls stream_reply with canonical text. It REUSES the PTY-created
|
|
137
|
+
// (literalText) stream for this chat+thread, so the edit also takes the
|
|
138
|
+
// literal path (plain string, not a rich { markdown } wrapper).
|
|
138
139
|
vi.advanceTimersByTime(1000)
|
|
139
140
|
await microtaskFlush()
|
|
140
141
|
const p1 = f.fireStreamReply({ chat_id: '42', text: 'final answer' })
|
|
@@ -145,7 +146,7 @@ describe('streaming e2e smoke', () => {
|
|
|
145
146
|
expect(r1.messageId).toBe(500)
|
|
146
147
|
expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
147
148
|
expect(f.bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
148
|
-
expect(f.bot.api.editMessageText.mock.calls[0][2]).toBe('
|
|
149
|
+
expect(f.bot.api.editMessageText.mock.calls[0][2]).toBe('final answer')
|
|
149
150
|
|
|
150
151
|
// done=true with same text → stream.finalize() resolves immediately
|
|
151
152
|
// (pendingText is null since the edit already landed).
|
|
@@ -185,7 +186,8 @@ describe('streaming e2e smoke', () => {
|
|
|
185
186
|
await microtaskFlush()
|
|
186
187
|
await pdone
|
|
187
188
|
|
|
188
|
-
|
|
189
|
+
// No PTY partial fired → stream_reply created a fresh RICH stream.
|
|
190
|
+
expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
189
191
|
const editIds = f.bot.api.editMessageText.mock.calls.map(c => c[1])
|
|
190
192
|
expect(new Set(editIds).size).toBe(1)
|
|
191
193
|
expect(f.map.has('1:_')).toBe(false)
|
|
@@ -203,7 +205,7 @@ describe('streaming e2e smoke', () => {
|
|
|
203
205
|
await microtaskFlush()
|
|
204
206
|
|
|
205
207
|
expect(f.bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
206
|
-
expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('
|
|
208
|
+
expect(f.bot.api.sendMessage.mock.calls[0][1]).toBe('early text')
|
|
207
209
|
expect(f.ptyState.pendingPtyPartial).toBeNull()
|
|
208
210
|
})
|
|
209
211
|
|
|
@@ -214,15 +216,18 @@ describe('streaming e2e smoke', () => {
|
|
|
214
216
|
const p1 = f.fireStreamReply({ chat_id: '1', text: 'stream_reply first' })
|
|
215
217
|
await microtaskFlush()
|
|
216
218
|
await p1
|
|
217
|
-
|
|
219
|
+
// stream_reply created a fresh RICH stream (no PTY partial preceded it).
|
|
220
|
+
expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
218
221
|
|
|
219
222
|
vi.advanceTimersByTime(1000)
|
|
220
223
|
await microtaskFlush()
|
|
221
224
|
const action = f.pty.onPartial('stream_reply first — more')
|
|
222
225
|
await microtaskFlush()
|
|
223
226
|
|
|
227
|
+
// The PTY partial reuses the existing stream → an edit, not a new send.
|
|
224
228
|
expect(action).toBe('update-existing')
|
|
225
|
-
expect(f.bot.api.
|
|
229
|
+
expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
230
|
+
expect(f.bot.api.sendMessage).not.toHaveBeenCalled()
|
|
226
231
|
expect(f.bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
227
232
|
expect(f.map.size).toBe(1)
|
|
228
233
|
})
|
|
@@ -240,8 +245,8 @@ describe('streaming e2e smoke', () => {
|
|
|
240
245
|
await microtaskFlush()
|
|
241
246
|
await pB
|
|
242
247
|
|
|
243
|
-
expect(f.bot.api.
|
|
244
|
-
const chats = f.bot.api.
|
|
248
|
+
expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(2)
|
|
249
|
+
const chats = f.bot.api.sendRichMessage.mock.calls.map(c => c[0]).sort()
|
|
245
250
|
expect(chats).toEqual(['A', 'B'])
|
|
246
251
|
|
|
247
252
|
vi.advanceTimersByTime(1000)
|
|
@@ -278,8 +283,8 @@ describe('streaming e2e smoke', () => {
|
|
|
278
283
|
const r = await pending
|
|
279
284
|
|
|
280
285
|
expect(r.status).toBe('updated')
|
|
281
|
-
expect(f.bot.api.
|
|
282
|
-
expect(f.bot.api.
|
|
286
|
+
expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
287
|
+
expect(f.bot.api.sendRichMessage.mock.calls[0][1]).toEqual({ markdown: 'looking into this…' })
|
|
283
288
|
// PTY slot is claimed because the model is now the answer-lane
|
|
284
289
|
// surface owner — late PTY partials defer to the model's stream
|
|
285
290
|
// rather than racing it. (Same suppression pattern as before;
|
|
@@ -296,9 +301,9 @@ describe('streaming e2e smoke', () => {
|
|
|
296
301
|
const r = await pdone
|
|
297
302
|
|
|
298
303
|
expect(r.status).toBe('finalized')
|
|
299
|
-
expect(f.bot.api.
|
|
300
|
-
expect(f.bot.api.
|
|
301
|
-
expect(f.bot.api.
|
|
304
|
+
expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
305
|
+
expect(f.bot.api.sendRichMessage.mock.calls[0][0]).toBe('1')
|
|
306
|
+
expect(f.bot.api.sendRichMessage.mock.calls[0][1]).toEqual({ markdown: 'THE ANSWER' })
|
|
302
307
|
expect(f.map.has('1:_')).toBe(false)
|
|
303
308
|
})
|
|
304
309
|
|
|
@@ -310,8 +315,8 @@ describe('streaming e2e smoke', () => {
|
|
|
310
315
|
await microtaskFlush()
|
|
311
316
|
await p1
|
|
312
317
|
|
|
313
|
-
expect(f.bot.api.
|
|
314
|
-
expect(f.bot.api.
|
|
318
|
+
expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
319
|
+
expect(f.bot.api.sendRichMessage.mock.calls[0][1]).toEqual({ markdown: 'thinking…' })
|
|
315
320
|
|
|
316
321
|
vi.advanceTimersByTime(1000)
|
|
317
322
|
await microtaskFlush()
|
|
@@ -320,7 +325,7 @@ describe('streaming e2e smoke', () => {
|
|
|
320
325
|
await pdone
|
|
321
326
|
|
|
322
327
|
// First call sent the message; second call edits it (no second send).
|
|
323
|
-
expect(f.bot.api.
|
|
328
|
+
expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
324
329
|
expect(f.bot.api.editMessageText).toHaveBeenCalled()
|
|
325
330
|
})
|
|
326
331
|
|
|
@@ -340,8 +345,8 @@ describe('streaming e2e smoke', () => {
|
|
|
340
345
|
const r = await p
|
|
341
346
|
|
|
342
347
|
expect(r.status).toBe('updated')
|
|
343
|
-
expect(f.bot.api.
|
|
344
|
-
expect(f.bot.api.
|
|
348
|
+
expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
349
|
+
expect(f.bot.api.sendRichMessage.mock.calls[0][1]).toEqual({ markdown: 'Plan → Run → Done' })
|
|
345
350
|
expect(f.map.has('1:_:progress')).toBe(true)
|
|
346
351
|
})
|
|
347
352
|
|
|
@@ -353,8 +358,8 @@ describe('streaming e2e smoke', () => {
|
|
|
353
358
|
await microtaskFlush()
|
|
354
359
|
await p1
|
|
355
360
|
|
|
356
|
-
expect(f.bot.api.
|
|
357
|
-
expect(f.bot.api.
|
|
361
|
+
expect(f.bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
362
|
+
expect(f.bot.api.sendRichMessage.mock.calls[0][1]).toEqual({ markdown: 'chunk 1' })
|
|
358
363
|
|
|
359
364
|
vi.advanceTimersByTime(1000)
|
|
360
365
|
await microtaskFlush()
|
|
@@ -381,7 +386,7 @@ describe('streaming e2e smoke', () => {
|
|
|
381
386
|
await pdone
|
|
382
387
|
|
|
383
388
|
const lastEdit = f.bot.api.editMessageText.mock.calls.at(-1)
|
|
384
|
-
expect(lastEdit?.[2]).
|
|
389
|
+
expect(lastEdit?.[2]).toEqual({ markdown: 'THE ACTUAL ANSWER' })
|
|
385
390
|
expect(f.map.has('1:_')).toBe(false)
|
|
386
391
|
})
|
|
387
392
|
})
|
|
@@ -37,7 +37,8 @@ function modelStreamReply(params: {
|
|
|
37
37
|
text: string
|
|
38
38
|
done?: boolean
|
|
39
39
|
throttleMs?: number
|
|
40
|
-
|
|
40
|
+
/** Literal plain-text stream (no rich-markdown parser). Default: rich. */
|
|
41
|
+
literalText?: boolean
|
|
41
42
|
}): { stream: DraftStreamHandle; settled: Promise<void> } {
|
|
42
43
|
const key = streamKey(params.chatId, params.threadId)
|
|
43
44
|
let stream = params.map.get(key)
|
|
@@ -46,7 +47,7 @@ function modelStreamReply(params: {
|
|
|
46
47
|
bot: params.bot,
|
|
47
48
|
chatId: params.chatId,
|
|
48
49
|
threadId: params.threadId,
|
|
49
|
-
|
|
50
|
+
literalText: params.literalText,
|
|
50
51
|
throttleMs: params.throttleMs ?? 600,
|
|
51
52
|
})
|
|
52
53
|
params.map.set(key, stream)
|
|
@@ -79,7 +80,7 @@ describe('streaming orchestration — activeDraftStreams map', () => {
|
|
|
79
80
|
|
|
80
81
|
expect(a).toBe(b)
|
|
81
82
|
expect(map.size).toBe(1)
|
|
82
|
-
expect(bot.api.
|
|
83
|
+
expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
83
84
|
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
84
85
|
})
|
|
85
86
|
|
|
@@ -96,7 +97,7 @@ describe('streaming orchestration — activeDraftStreams map', () => {
|
|
|
96
97
|
await microtaskFlush()
|
|
97
98
|
|
|
98
99
|
expect(first).not.toBe(second)
|
|
99
|
-
expect(bot.api.
|
|
100
|
+
expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(2)
|
|
100
101
|
expect(second.getMessageId()).toBe(501)
|
|
101
102
|
})
|
|
102
103
|
|
|
@@ -108,8 +109,8 @@ describe('streaming orchestration — activeDraftStreams map', () => {
|
|
|
108
109
|
await microtaskFlush()
|
|
109
110
|
|
|
110
111
|
expect(map.size).toBe(2)
|
|
111
|
-
expect(bot.api.
|
|
112
|
-
const chatIds = bot.api.
|
|
112
|
+
expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(2)
|
|
113
|
+
const chatIds = bot.api.sendRichMessage.mock.calls.map(c => c[0]).sort()
|
|
113
114
|
expect(chatIds).toEqual(['1', '2'])
|
|
114
115
|
})
|
|
115
116
|
|
|
@@ -121,32 +122,32 @@ describe('streaming orchestration — activeDraftStreams map', () => {
|
|
|
121
122
|
await microtaskFlush()
|
|
122
123
|
|
|
123
124
|
expect(map.size).toBe(2)
|
|
124
|
-
expect(bot.api.
|
|
125
|
-
const threads = bot.api.
|
|
125
|
+
expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(2)
|
|
126
|
+
const threads = bot.api.sendRichMessage.mock.calls.map(c => c[2]?.message_thread_id).sort()
|
|
126
127
|
expect(threads).toEqual([10, 20])
|
|
127
128
|
})
|
|
128
129
|
|
|
129
130
|
it('pty preview → stream_reply handoff: second call edits the PTY message in place', async () => {
|
|
130
131
|
const map = new Map<string, DraftStreamHandle>()
|
|
131
132
|
|
|
132
|
-
const ptyStream = createStreamController({ bot, chatId: '1',
|
|
133
|
+
const ptyStream = createStreamController({ bot, chatId: '1', throttleMs: 600 })
|
|
133
134
|
map.set('1:_', ptyStream)
|
|
134
|
-
void ptyStream.update('
|
|
135
|
+
void ptyStream.update('_drafting…_')
|
|
135
136
|
await microtaskFlush()
|
|
136
137
|
|
|
137
|
-
expect(bot.api.
|
|
138
|
+
expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
138
139
|
const previewId = ptyStream.getMessageId()
|
|
139
140
|
expect(previewId).toBe(500)
|
|
140
141
|
|
|
141
142
|
vi.advanceTimersByTime(600)
|
|
142
143
|
await microtaskFlush()
|
|
143
144
|
const { settled } = modelStreamReply({
|
|
144
|
-
bot, map, chatId: '1',
|
|
145
|
+
bot, map, chatId: '1', text: 'final answer', done: true,
|
|
145
146
|
})
|
|
146
147
|
await microtaskFlush()
|
|
147
148
|
await settled
|
|
148
149
|
|
|
149
|
-
expect(bot.api.
|
|
150
|
+
expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
150
151
|
expect(bot.api.editMessageText).toHaveBeenCalled()
|
|
151
152
|
expect(bot.api.editMessageText.mock.calls[0][1]).toBe(previewId)
|
|
152
153
|
expect(map.has('1:_')).toBe(false)
|
|
@@ -165,10 +166,10 @@ describe('streaming orchestration — activeDraftStreams map', () => {
|
|
|
165
166
|
}
|
|
166
167
|
await microtaskFlush()
|
|
167
168
|
|
|
168
|
-
expect(bot.api.
|
|
169
|
-
expect(bot.api.
|
|
169
|
+
expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
170
|
+
expect((bot.api.sendRichMessage.mock.calls[0][1] as { markdown: string }).markdown).toBe('one')
|
|
170
171
|
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
171
|
-
expect(bot.api.editMessageText.mock.calls[0][2]).toBe('four')
|
|
172
|
+
expect((bot.api.editMessageText.mock.calls[0][2] as { markdown: string }).markdown).toBe('four')
|
|
172
173
|
|
|
173
174
|
// No further work scheduled; advancing timers should be a no-op.
|
|
174
175
|
vi.advanceTimersByTime(5000)
|
|
@@ -188,7 +189,7 @@ describe('streaming orchestration — activeDraftStreams map', () => {
|
|
|
188
189
|
await settled
|
|
189
190
|
|
|
190
191
|
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
191
|
-
expect(bot.api.editMessageText.mock.calls[0][2]).toBe('final answer')
|
|
192
|
+
expect((bot.api.editMessageText.mock.calls[0][2] as { markdown: string }).markdown).toBe('final answer')
|
|
192
193
|
expect(map.size).toBe(0)
|
|
193
194
|
})
|
|
194
195
|
|
|
@@ -197,7 +198,7 @@ describe('streaming orchestration — activeDraftStreams map', () => {
|
|
|
197
198
|
// messageId=null. A subsequent update() triggers a fresh send on the
|
|
198
199
|
// same stream object — this is what makes a momentary network blip
|
|
199
200
|
// recoverable without the caller needing to evict-and-recreate.
|
|
200
|
-
bot.api.
|
|
201
|
+
bot.api.sendRichMessage
|
|
201
202
|
.mockImplementationOnce(async () => { throw new Error('network down') })
|
|
202
203
|
.mockImplementationOnce(async () => ({ message_id: 777 }))
|
|
203
204
|
|
|
@@ -206,14 +207,14 @@ describe('streaming orchestration — activeDraftStreams map', () => {
|
|
|
206
207
|
await microtaskFlush()
|
|
207
208
|
|
|
208
209
|
expect(stream.getMessageId()).toBeNull()
|
|
209
|
-
expect(bot.api.
|
|
210
|
+
expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(1)
|
|
210
211
|
|
|
211
212
|
// Caller issues next snapshot — should retry send, not try to edit null.
|
|
212
213
|
vi.advanceTimersByTime(1000)
|
|
213
214
|
modelStreamReply({ bot, map, chatId: '1', text: 'second try' })
|
|
214
215
|
await microtaskFlush()
|
|
215
216
|
|
|
216
|
-
expect(bot.api.
|
|
217
|
+
expect(bot.api.sendRichMessage).toHaveBeenCalledTimes(2)
|
|
217
218
|
expect(bot.api.editMessageText).not.toHaveBeenCalled()
|
|
218
219
|
expect(stream.getMessageId()).toBe(777)
|
|
219
220
|
})
|
|
@@ -249,7 +250,9 @@ function modelPtyPartial(params: {
|
|
|
249
250
|
bot: params.bot,
|
|
250
251
|
chatId: params.chatId,
|
|
251
252
|
threadId: params.threadId,
|
|
252
|
-
|
|
253
|
+
// PTY-tail previews ship as literal plain text (#2669) — the raw
|
|
254
|
+
// TUI capture is not markdown.
|
|
255
|
+
literalText: true,
|
|
253
256
|
throttleMs: params.throttleMs ?? 600,
|
|
254
257
|
})
|
|
255
258
|
params.map.set(key, stream)
|
|
@@ -404,8 +407,6 @@ function makeActivityDeps(
|
|
|
404
407
|
): StreamReplyDeps {
|
|
405
408
|
return {
|
|
406
409
|
bot,
|
|
407
|
-
markdownToHtml: (t) => t,
|
|
408
|
-
escapeMarkdownV2: (t) => t,
|
|
409
410
|
repairEscapedWhitespace: (t) => t,
|
|
410
411
|
assertAllowedChat: () => {},
|
|
411
412
|
resolveThreadId: (_, explicit) => (explicit != null ? Number(explicit) : undefined),
|
|
@@ -706,7 +707,7 @@ describe('PTY activity-lane wiring (onActivity → stream_reply lane="activity")
|
|
|
706
707
|
const deps = makeActivityDeps(bot)
|
|
707
708
|
|
|
708
709
|
await handleStreamReply(
|
|
709
|
-
{ chat_id: '1', text: '⚙️ Working…', lane: 'progress', format: '
|
|
710
|
+
{ chat_id: '1', text: '⚙️ Working…', lane: 'progress', format: 'markdown' },
|
|
710
711
|
state, deps,
|
|
711
712
|
)
|
|
712
713
|
await microtaskFlush()
|
|
@@ -726,12 +727,12 @@ describe('PTY activity-lane wiring (onActivity → stream_reply lane="activity")
|
|
|
726
727
|
|
|
727
728
|
// Turn 1: progress card lives at some messageId.
|
|
728
729
|
await handleStreamReply(
|
|
729
|
-
{ chat_id: '1', text: '⚙️ Working… (turn 1)', lane: 'progress', format: '
|
|
730
|
+
{ chat_id: '1', text: '⚙️ Working… (turn 1)', lane: 'progress', format: 'markdown' },
|
|
730
731
|
state, deps,
|
|
731
732
|
)
|
|
732
733
|
await microtaskFlush()
|
|
733
734
|
const firstProgressId = state.activeDraftStreams.get('1:_:progress')!.getMessageId()
|
|
734
|
-
const sendsBefore = bot.api.
|
|
735
|
+
const sendsBefore = bot.api.sendRichMessage.mock.calls.length
|
|
735
736
|
|
|
736
737
|
// turn_end closes the progress lane.
|
|
737
738
|
await modelCloseProgressLane(state, '1', undefined)
|
|
@@ -741,7 +742,7 @@ describe('PTY activity-lane wiring (onActivity → stream_reply lane="activity")
|
|
|
741
742
|
// edit the previous one — this is what prevents the shrink-flicker
|
|
742
743
|
// described in the bug report.
|
|
743
744
|
await handleStreamReply(
|
|
744
|
-
{ chat_id: '1', text: '⚙️ Working… (turn 2)', lane: 'progress', format: '
|
|
745
|
+
{ chat_id: '1', text: '⚙️ Working… (turn 2)', lane: 'progress', format: 'markdown' },
|
|
745
746
|
state, deps,
|
|
746
747
|
)
|
|
747
748
|
await microtaskFlush()
|
|
@@ -749,6 +750,6 @@ describe('PTY activity-lane wiring (onActivity → stream_reply lane="activity")
|
|
|
749
750
|
expect(state.activeDraftStreams.has('1:_:progress')).toBe(true)
|
|
750
751
|
const secondProgressId = state.activeDraftStreams.get('1:_:progress')!.getMessageId()
|
|
751
752
|
expect(secondProgressId).not.toBe(firstProgressId)
|
|
752
|
-
expect(bot.api.
|
|
753
|
+
expect(bot.api.sendRichMessage.mock.calls.length).toBe(sendsBefore + 1)
|
|
753
754
|
})
|
|
754
755
|
})
|