switchroom 0.15.44 → 0.16.4
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 +122 -88
- package/dist/auth-broker/index.js +463 -177
- package/dist/cli/autoaccept-poll.js +4842 -35
- package/dist/cli/drive-write-pretool.mjs +17 -14
- package/dist/cli/notion-write-pretool.mjs +117 -86
- package/dist/cli/self-improve-apply-guard-pretool.mjs +626 -0
- package/dist/cli/self-improve-stop.mjs +428 -0
- package/dist/cli/skill-validate-pretool.mjs +72 -72
- package/dist/cli/switchroom.js +3249 -1241
- package/dist/cli/ui/index.html +1 -1
- package/dist/host-control/main.js +2833 -355
- package/dist/vault/approvals/kernel-server.js +7482 -7439
- package/dist/vault/broker/server.js +11315 -11272
- package/examples/minimal.yaml +1 -0
- package/examples/switchroom.yaml +1 -0
- package/package.json +3 -3
- package/profiles/_base/start.sh.hbs +88 -1
- package/profiles/_shared/execution-discipline.md.hbs +18 -0
- package/profiles/default/CLAUDE.md.hbs +3 -22
- package/telegram-plugin/.claude-plugin/plugin.json +2 -2
- package/telegram-plugin/answer-stream-flag.ts +12 -49
- package/telegram-plugin/answer-stream.ts +5 -150
- package/telegram-plugin/auth-snapshot-format.ts +280 -48
- package/telegram-plugin/auto-fallback-fleet.ts +44 -1
- package/telegram-plugin/context-exhaustion.ts +12 -0
- package/telegram-plugin/demo-mask.ts +154 -0
- package/telegram-plugin/dist/bridge/bridge.js +167 -124
- package/telegram-plugin/dist/gateway/gateway.js +3039 -1159
- package/telegram-plugin/dist/server.js +215 -172
- package/telegram-plugin/docs/waiting-ux-spec.md +2 -2
- package/telegram-plugin/draft-stream.ts +47 -410
- package/telegram-plugin/final-answer-detect.ts +17 -12
- package/telegram-plugin/fleet-fallback-resume.ts +131 -0
- package/telegram-plugin/format.ts +56 -19
- package/telegram-plugin/gateway/auth-add-flow.ts +332 -127
- package/telegram-plugin/gateway/auth-broker-client.ts +2 -2
- package/telegram-plugin/gateway/auth-command.ts +70 -14
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +44 -0
- package/telegram-plugin/gateway/config-approval-handler.test.ts +91 -4
- package/telegram-plugin/gateway/config-approval-handler.ts +94 -13
- package/telegram-plugin/gateway/current-turn-map.ts +188 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +3 -1
- package/telegram-plugin/gateway/effort-command.ts +8 -3
- package/telegram-plugin/gateway/emission-authority.ts +369 -0
- package/telegram-plugin/gateway/feed-open-gate.ts +292 -0
- package/telegram-plugin/gateway/gateway.ts +1837 -291
- package/telegram-plugin/gateway/inject-handler.test.ts +2 -1
- package/telegram-plugin/gateway/ms365-write-approval.test.ts +4 -4
- package/telegram-plugin/gateway/represent-guard.ts +72 -0
- package/telegram-plugin/gateway/status-surface-log.test.ts +5 -4
- package/telegram-plugin/gateway/status-surface-log.ts +14 -3
- package/telegram-plugin/history.ts +33 -11
- package/telegram-plugin/hooks/repo-context-pretool.mjs +26 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +5 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +8 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +39 -15
- package/telegram-plugin/issues-card.ts +4 -0
- package/telegram-plugin/model-unavailable.ts +124 -0
- package/telegram-plugin/narrative-dedup.ts +69 -0
- package/telegram-plugin/over-ping-safety-net.ts +70 -4
- package/telegram-plugin/package.json +3 -3
- package/telegram-plugin/pending-work-progress.ts +12 -0
- package/telegram-plugin/permission-rule.ts +32 -5
- package/telegram-plugin/permission-title.ts +152 -9
- package/telegram-plugin/quota-check.ts +13 -0
- package/telegram-plugin/quota-watch.ts +135 -7
- package/telegram-plugin/registry/turns-schema.test.ts +24 -0
- package/telegram-plugin/registry/turns-schema.ts +9 -0
- package/telegram-plugin/runtime-metrics.ts +13 -0
- package/telegram-plugin/session-tail.ts +96 -11
- package/telegram-plugin/silence-poke.ts +170 -24
- package/telegram-plugin/slot-banner-driver.ts +3 -0
- package/telegram-plugin/status-no-truncate.ts +44 -0
- package/telegram-plugin/status-reactions.ts +20 -3
- package/telegram-plugin/stream-controller.ts +4 -23
- package/telegram-plugin/stream-reply-handler.ts +6 -24
- package/telegram-plugin/streaming-metrics.ts +91 -0
- package/telegram-plugin/subagent-watcher.ts +212 -66
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +47 -0
- package/telegram-plugin/tests/answer-stream-dedup.test.ts +9 -26
- package/telegram-plugin/tests/answer-stream-flag.test.ts +25 -58
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +41 -51
- package/telegram-plugin/tests/answer-stream.test.ts +2 -411
- package/telegram-plugin/tests/auth-add-flow.test.ts +488 -253
- package/telegram-plugin/tests/auth-command-format2.test.ts +71 -1
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +376 -6
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +120 -0
- package/telegram-plugin/tests/cross-turn-card-gate.test.ts +424 -0
- package/telegram-plugin/tests/demo-mask.test.ts +127 -0
- package/telegram-plugin/tests/draft-stream.test.ts +0 -827
- package/telegram-plugin/tests/emission-authority-card-drain-gate.test.ts +236 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +488 -0
- package/telegram-plugin/tests/emission-authority-open-gate.test.ts +179 -0
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +395 -0
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +177 -0
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +146 -0
- package/telegram-plugin/tests/feed-open-gate.test.ts +259 -0
- package/telegram-plugin/tests/feed-survival.test.ts +526 -0
- package/telegram-plugin/tests/fleet-fallback-resume.test.ts +197 -0
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +117 -0
- package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +4 -11
- package/telegram-plugin/tests/history.test.ts +60 -0
- package/telegram-plugin/tests/model-unavailable.test.ts +118 -0
- package/telegram-plugin/tests/narrative-dedup.test.ts +118 -0
- package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +285 -0
- package/telegram-plugin/tests/over-ping-final-answer-decoupling.test.ts +194 -0
- package/telegram-plugin/tests/over-ping-safety-net.test.ts +2 -2
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +373 -0
- package/telegram-plugin/tests/permission-card-origin-kill-switch.test.ts +42 -0
- package/telegram-plugin/tests/permission-rule.test.ts +17 -0
- package/telegram-plugin/tests/permission-title.test.ts +206 -17
- package/telegram-plugin/tests/quota-watch.test.ts +252 -9
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -1
- package/telegram-plugin/tests/repo-context-pretool.test.ts +62 -0
- package/telegram-plugin/tests/represent-guard.test.ts +162 -0
- package/telegram-plugin/tests/session-tail.test.ts +147 -3
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +18 -0
- package/telegram-plugin/tests/status-card-budget-parity.test.ts +72 -0
- package/telegram-plugin/tests/status-surface-log.test.ts +146 -0
- package/telegram-plugin/tests/subagent-watcher-clip-narrative.test.ts +58 -0
- package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +102 -0
- package/telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts +225 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +147 -0
- package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +597 -0
- package/telegram-plugin/tests/telegram-format.test.ts +101 -6
- package/telegram-plugin/tests/tool-activity-summary.test.ts +550 -15
- package/telegram-plugin/tests/tool-label-pretool.test.ts +73 -0
- package/telegram-plugin/tests/tool-label-sidecar.test.ts +44 -0
- package/telegram-plugin/tests/tool-labels.test.ts +67 -0
- package/telegram-plugin/tests/turn-liveness-floor.test.ts +196 -0
- package/telegram-plugin/tests/turn-liveness-invariant.test.ts +340 -0
- package/telegram-plugin/tests/welcome-text.test.ts +32 -3
- package/telegram-plugin/tests/worker-activity-feed.test.ts +470 -22
- package/telegram-plugin/tool-activity-summary.ts +375 -58
- package/telegram-plugin/turn-liveness-floor.ts +240 -0
- package/telegram-plugin/uat/assertions.ts +115 -0
- package/telegram-plugin/uat/driver.ts +68 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +119 -133
- package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +94 -0
- package/telegram-plugin/uat/scenarios/jtbd-cross-turn-card-dm.test.ts +109 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-thinkgap-dm.test.ts +478 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +396 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-feed-open-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/jtbd-reply-is-last-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +93 -87
- package/telegram-plugin/welcome-text.ts +13 -1
- package/telegram-plugin/worker-activity-feed.ts +157 -82
- package/telegram-plugin/draft-transport.ts +0 -122
- package/telegram-plugin/tests/draft-retirement-wiring.test.ts +0 -82
- package/telegram-plugin/tests/draft-transport.test.ts +0 -211
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import {
|
|
3
3
|
createAnswerStream,
|
|
4
|
-
__resetDraftIdForTests,
|
|
5
4
|
MIN_INITIAL_CHARS,
|
|
6
|
-
DRAFT_METHOD_UNAVAILABLE_RE,
|
|
7
|
-
DRAFT_CHAT_UNSUPPORTED_RE,
|
|
8
5
|
} from '../answer-stream.js'
|
|
9
6
|
import { resolveAnswerLaneConfig, ANSWER_LANE_NEVER_OPENS } from '../answer-stream-flag.js'
|
|
10
7
|
|
|
@@ -39,13 +36,6 @@ type EditMessageTextFn = (
|
|
|
39
36
|
},
|
|
40
37
|
) => Promise<unknown>
|
|
41
38
|
|
|
42
|
-
type SendMessageDraftFn = (
|
|
43
|
-
chatId: string,
|
|
44
|
-
draftId: number,
|
|
45
|
-
text: string,
|
|
46
|
-
params?: { message_thread_id?: number },
|
|
47
|
-
) => Promise<unknown>
|
|
48
|
-
|
|
49
39
|
let nextMessageId = 1000
|
|
50
40
|
|
|
51
41
|
function makeSendMessage(): ReturnType<typeof vi.fn> & SendMessageFn {
|
|
@@ -59,14 +49,9 @@ function makeEditMessageText(): ReturnType<typeof vi.fn> & EditMessageTextFn {
|
|
|
59
49
|
return vi.fn(async () => {}) as unknown as ReturnType<typeof vi.fn> & EditMessageTextFn
|
|
60
50
|
}
|
|
61
51
|
|
|
62
|
-
function makeSendMessageDraft(): ReturnType<typeof vi.fn> & SendMessageDraftFn {
|
|
63
|
-
return vi.fn(async () => {}) as unknown as ReturnType<typeof vi.fn> & SendMessageDraftFn
|
|
64
|
-
}
|
|
65
|
-
|
|
66
52
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
67
53
|
|
|
68
54
|
beforeEach(() => {
|
|
69
|
-
__resetDraftIdForTests()
|
|
70
55
|
nextMessageId = 1000
|
|
71
56
|
vi.useFakeTimers()
|
|
72
57
|
})
|
|
@@ -81,7 +66,6 @@ describe('answer-stream — minInitialChars threshold', () => {
|
|
|
81
66
|
const editMessageText = makeEditMessageText()
|
|
82
67
|
const stream = createAnswerStream({
|
|
83
68
|
chatId: 'chat1',
|
|
84
|
-
isPrivateChat: false,
|
|
85
69
|
minInitialChars: 400,
|
|
86
70
|
throttleMs: 250,
|
|
87
71
|
sendMessage,
|
|
@@ -101,7 +85,7 @@ describe('answer-stream — minInitialChars threshold', () => {
|
|
|
101
85
|
// End-to-end proof that the resolved default config produces no visible
|
|
102
86
|
// preview → nothing to retract → no flash. Wires the ACTUAL resolver output
|
|
103
87
|
// (not a hand-picked threshold) into the stream.
|
|
104
|
-
const lane = resolveAnswerLaneConfig({ visibleEnabled: false
|
|
88
|
+
const lane = resolveAnswerLaneConfig({ visibleEnabled: false })
|
|
105
89
|
expect(lane.state).toBe('dormant')
|
|
106
90
|
expect(lane.minInitialChars).toBe(ANSWER_LANE_NEVER_OPENS)
|
|
107
91
|
|
|
@@ -109,12 +93,10 @@ describe('answer-stream — minInitialChars threshold', () => {
|
|
|
109
93
|
const editMessageText = makeEditMessageText()
|
|
110
94
|
const stream = createAnswerStream({
|
|
111
95
|
chatId: 'chat1',
|
|
112
|
-
isPrivateChat: false,
|
|
113
96
|
minInitialChars: lane.minInitialChars,
|
|
114
97
|
throttleMs: 250,
|
|
115
98
|
sendMessage,
|
|
116
99
|
editMessageText,
|
|
117
|
-
// no sendMessageDraft — dormant lane has no transport
|
|
118
100
|
})
|
|
119
101
|
|
|
120
102
|
// A realistic full answer — 2000 chars, far above any normal threshold.
|
|
@@ -131,7 +113,6 @@ describe('answer-stream — minInitialChars threshold', () => {
|
|
|
131
113
|
const editMessageText = makeEditMessageText()
|
|
132
114
|
const stream = createAnswerStream({
|
|
133
115
|
chatId: 'chat1',
|
|
134
|
-
isPrivateChat: false,
|
|
135
116
|
minInitialChars: 400,
|
|
136
117
|
throttleMs: 250,
|
|
137
118
|
sendMessage,
|
|
@@ -153,120 +134,6 @@ describe('answer-stream — minInitialChars threshold', () => {
|
|
|
153
134
|
})
|
|
154
135
|
})
|
|
155
136
|
|
|
156
|
-
describe('answer-stream — draft transport selection', () => {
|
|
157
|
-
it('uses sendMessageDraft for DMs (isPrivateChat: true)', async () => {
|
|
158
|
-
const sendMessage = makeSendMessage()
|
|
159
|
-
const editMessageText = makeEditMessageText()
|
|
160
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
161
|
-
const stream = createAnswerStream({
|
|
162
|
-
chatId: 'chat1',
|
|
163
|
-
isPrivateChat: true,
|
|
164
|
-
throttleMs: 250,
|
|
165
|
-
sendMessage,
|
|
166
|
-
editMessageText,
|
|
167
|
-
sendMessageDraft,
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
// Draft transport bypasses minInitialChars gate — any non-empty text goes
|
|
171
|
-
stream.update('Hello from DM!')
|
|
172
|
-
await flushMicrotasks()
|
|
173
|
-
|
|
174
|
-
expect(sendMessageDraft).toHaveBeenCalledTimes(1)
|
|
175
|
-
expect(sendMessageDraft).toHaveBeenCalledWith(
|
|
176
|
-
'chat1',
|
|
177
|
-
expect.any(Number),
|
|
178
|
-
'Hello from DM!',
|
|
179
|
-
undefined,
|
|
180
|
-
)
|
|
181
|
-
expect(sendMessage).not.toHaveBeenCalled()
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('uses sendMessage for non-DM chats even when sendMessageDraft is provided', async () => {
|
|
185
|
-
const sendMessage = makeSendMessage()
|
|
186
|
-
const editMessageText = makeEditMessageText()
|
|
187
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
188
|
-
const stream = createAnswerStream({
|
|
189
|
-
chatId: 'chat1',
|
|
190
|
-
isPrivateChat: false,
|
|
191
|
-
minInitialChars: 10,
|
|
192
|
-
throttleMs: 250,
|
|
193
|
-
sendMessage,
|
|
194
|
-
editMessageText,
|
|
195
|
-
sendMessageDraft,
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
stream.update('x'.repeat(50))
|
|
199
|
-
await flushMicrotasks()
|
|
200
|
-
|
|
201
|
-
expect(sendMessage).toHaveBeenCalledTimes(1)
|
|
202
|
-
expect(sendMessageDraft).not.toHaveBeenCalled()
|
|
203
|
-
})
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
describe('answer-stream — runtime fallback when sendMessageDraft rejects', () => {
|
|
207
|
-
it('falls back to sendMessage when sendMessageDraft throws DRAFT_METHOD_UNAVAILABLE_RE pattern', async () => {
|
|
208
|
-
const sendMessage = makeSendMessage()
|
|
209
|
-
const editMessageText = makeEditMessageText()
|
|
210
|
-
// The shouldFallbackFromDraftTransport helper checks for "sendMessageDraft" in the
|
|
211
|
-
// error message plus the regex pattern.
|
|
212
|
-
const sendMessageDraft = vi.fn(async () => {
|
|
213
|
-
throw new Error('sendMessageDraft: unknown method')
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
const stream = createAnswerStream({
|
|
217
|
-
chatId: 'chat1',
|
|
218
|
-
isPrivateChat: true,
|
|
219
|
-
throttleMs: 250,
|
|
220
|
-
sendMessage,
|
|
221
|
-
editMessageText,
|
|
222
|
-
sendMessageDraft,
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
// First update — draft throws, falls back
|
|
226
|
-
stream.update('Hello DM!')
|
|
227
|
-
await flushMicrotasks()
|
|
228
|
-
|
|
229
|
-
expect(sendMessageDraft).toHaveBeenCalledTimes(1)
|
|
230
|
-
// After fallback, sendMessage should have been called with the same text
|
|
231
|
-
expect(sendMessage).toHaveBeenCalledTimes(1)
|
|
232
|
-
expect(sendMessage).toHaveBeenCalledWith('chat1', 'Hello DM!', expect.any(Object))
|
|
233
|
-
|
|
234
|
-
// Subsequent update should use sendMessage+editMessageText, not draft
|
|
235
|
-
sendMessageDraft.mockClear()
|
|
236
|
-
sendMessage.mockClear()
|
|
237
|
-
vi.advanceTimersByTime(1000)
|
|
238
|
-
stream.update('Follow-up!')
|
|
239
|
-
await flushMicrotasks()
|
|
240
|
-
|
|
241
|
-
expect(sendMessageDraft).not.toHaveBeenCalled()
|
|
242
|
-
expect(editMessageText).toHaveBeenCalledTimes(1)
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
it('falls back when sendMessageDraft throws DRAFT_CHAT_UNSUPPORTED_RE pattern', async () => {
|
|
246
|
-
const sendMessage = makeSendMessage()
|
|
247
|
-
const editMessageText = makeEditMessageText()
|
|
248
|
-
const sendMessageDraft = vi.fn(async () => {
|
|
249
|
-
throw new Error("sendMessageDraft can't be used in this chat")
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
const stream = createAnswerStream({
|
|
253
|
-
chatId: 'chat1',
|
|
254
|
-
isPrivateChat: true,
|
|
255
|
-
throttleMs: 250,
|
|
256
|
-
sendMessage,
|
|
257
|
-
editMessageText,
|
|
258
|
-
sendMessageDraft,
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
stream.update('Hello!')
|
|
262
|
-
await flushMicrotasks()
|
|
263
|
-
|
|
264
|
-
expect(sendMessageDraft).toHaveBeenCalledTimes(1)
|
|
265
|
-
expect(sendMessage).toHaveBeenCalledTimes(1)
|
|
266
|
-
expect(sendMessage).toHaveBeenCalledWith('chat1', 'Hello!', expect.any(Object))
|
|
267
|
-
})
|
|
268
|
-
})
|
|
269
|
-
|
|
270
137
|
describe('answer-stream — throttling', () => {
|
|
271
138
|
it('three rapid updates within throttleMs result in at most two transport calls', async () => {
|
|
272
139
|
const sendMessage = makeSendMessage()
|
|
@@ -274,7 +141,6 @@ describe('answer-stream — throttling', () => {
|
|
|
274
141
|
const THROTTLE = 1000
|
|
275
142
|
const stream = createAnswerStream({
|
|
276
143
|
chatId: 'chat1',
|
|
277
|
-
isPrivateChat: false,
|
|
278
144
|
minInitialChars: 10,
|
|
279
145
|
throttleMs: THROTTLE,
|
|
280
146
|
sendMessage,
|
|
@@ -317,7 +183,6 @@ describe('answer-stream — materialize()', () => {
|
|
|
317
183
|
const THROTTLE = 1000
|
|
318
184
|
const stream = createAnswerStream({
|
|
319
185
|
chatId: 'chat1',
|
|
320
|
-
isPrivateChat: false,
|
|
321
186
|
minInitialChars: 10,
|
|
322
187
|
throttleMs: THROTTLE,
|
|
323
188
|
sendMessage,
|
|
@@ -348,7 +213,6 @@ describe('answer-stream — materialize()', () => {
|
|
|
348
213
|
const editMessageText = makeEditMessageText()
|
|
349
214
|
const stream = createAnswerStream({
|
|
350
215
|
chatId: 'chat1',
|
|
351
|
-
isPrivateChat: false,
|
|
352
216
|
minInitialChars: 10,
|
|
353
217
|
throttleMs: 250,
|
|
354
218
|
sendMessage,
|
|
@@ -376,7 +240,6 @@ describe('answer-stream — forceNewMessage() supersession', () => {
|
|
|
376
240
|
const THROTTLE = 1000
|
|
377
241
|
const stream = createAnswerStream({
|
|
378
242
|
chatId: 'chat1',
|
|
379
|
-
isPrivateChat: false,
|
|
380
243
|
minInitialChars: 10,
|
|
381
244
|
throttleMs: THROTTLE,
|
|
382
245
|
sendMessage,
|
|
@@ -387,7 +250,6 @@ describe('answer-stream — forceNewMessage() supersession', () => {
|
|
|
387
250
|
stream.update('x'.repeat(50))
|
|
388
251
|
await flushMicrotasks()
|
|
389
252
|
expect(sendMessage).toHaveBeenCalledTimes(1)
|
|
390
|
-
const firstMsgId = sendMessage.mock.calls[0]
|
|
391
253
|
|
|
392
254
|
// Advance past throttle so a new update is ready to send
|
|
393
255
|
vi.advanceTimersByTime(THROTTLE)
|
|
@@ -414,7 +276,6 @@ describe('answer-stream — stop() cancels pending throttled edits', () => {
|
|
|
414
276
|
const THROTTLE = 1000
|
|
415
277
|
const stream = createAnswerStream({
|
|
416
278
|
chatId: 'chat1',
|
|
417
|
-
isPrivateChat: false,
|
|
418
279
|
minInitialChars: 10,
|
|
419
280
|
throttleMs: THROTTLE,
|
|
420
281
|
sendMessage,
|
|
@@ -447,209 +308,12 @@ describe('answer-stream — stop() cancels pending throttled edits', () => {
|
|
|
447
308
|
})
|
|
448
309
|
})
|
|
449
310
|
|
|
450
|
-
// ─── #1704 regression — clear the sendMessageDraft on every terminal path ──
|
|
451
|
-
//
|
|
452
|
-
// In DMs the answer-stream uses sendMessageDraft, which renders inside the
|
|
453
|
-
// user's compose box. Telegram Desktop blocks the user from typing while
|
|
454
|
-
// the bot's draft is live — so stop() / retract() / materialize() must
|
|
455
|
-
// all clear the draft. Without these tests the bug class slips back in
|
|
456
|
-
// the next time someone tweaks the lifecycle.
|
|
457
|
-
|
|
458
|
-
describe('answer-stream — clears sendMessageDraft on terminal paths (#1704)', () => {
|
|
459
|
-
it('stop() clears the draft when draft transport was in use', async () => {
|
|
460
|
-
const sendMessage = makeSendMessage()
|
|
461
|
-
const editMessageText = makeEditMessageText()
|
|
462
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
463
|
-
const stream = createAnswerStream({
|
|
464
|
-
chatId: 'chat1',
|
|
465
|
-
isPrivateChat: true,
|
|
466
|
-
throttleMs: 250,
|
|
467
|
-
sendMessage,
|
|
468
|
-
editMessageText,
|
|
469
|
-
sendMessageDraft,
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
stream.update('mid-turn thought')
|
|
473
|
-
await flushMicrotasks()
|
|
474
|
-
expect(sendMessageDraft).toHaveBeenCalledTimes(1)
|
|
475
|
-
|
|
476
|
-
stream.stop()
|
|
477
|
-
// stop() is sync but the clear fires fire-and-forget — drain microtasks.
|
|
478
|
-
await flushMicrotasks()
|
|
479
|
-
|
|
480
|
-
// A second draft call must have landed with empty text, clearing the
|
|
481
|
-
// compose-box preview. The draft id matches the in-flight stream's.
|
|
482
|
-
const draftId = (sendMessageDraft.mock.calls[0] as unknown as [string, number, string, unknown])[1]
|
|
483
|
-
expect(sendMessageDraft).toHaveBeenCalledWith('chat1', draftId, '', undefined)
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
it('retract() clears the draft when draft transport was in use', async () => {
|
|
487
|
-
const sendMessage = makeSendMessage()
|
|
488
|
-
const editMessageText = makeEditMessageText()
|
|
489
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
490
|
-
const stream = createAnswerStream({
|
|
491
|
-
chatId: 'chat1',
|
|
492
|
-
isPrivateChat: true,
|
|
493
|
-
throttleMs: 250,
|
|
494
|
-
sendMessage,
|
|
495
|
-
editMessageText,
|
|
496
|
-
sendMessageDraft,
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
stream.update('mid-turn thought')
|
|
500
|
-
await flushMicrotasks()
|
|
501
|
-
expect(sendMessageDraft).toHaveBeenCalledTimes(1)
|
|
502
|
-
|
|
503
|
-
await stream.retract()
|
|
504
|
-
|
|
505
|
-
const draftId = (sendMessageDraft.mock.calls[0] as unknown as [string, number, string, unknown])[1]
|
|
506
|
-
expect(sendMessageDraft).toHaveBeenCalledWith('chat1', draftId, '', undefined)
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
it('stop() is a no-op on the draft API when message transport was in use', async () => {
|
|
510
|
-
const sendMessage = makeSendMessage()
|
|
511
|
-
const editMessageText = makeEditMessageText()
|
|
512
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
513
|
-
const stream = createAnswerStream({
|
|
514
|
-
chatId: 'chat1',
|
|
515
|
-
isPrivateChat: false, // forces message transport
|
|
516
|
-
minInitialChars: 0,
|
|
517
|
-
throttleMs: 250,
|
|
518
|
-
sendMessage,
|
|
519
|
-
editMessageText,
|
|
520
|
-
sendMessageDraft,
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
stream.update('mid-turn thought')
|
|
524
|
-
await flushMicrotasks()
|
|
525
|
-
expect(sendMessage).toHaveBeenCalledTimes(1)
|
|
526
|
-
expect(sendMessageDraft).not.toHaveBeenCalled()
|
|
527
|
-
|
|
528
|
-
stream.stop()
|
|
529
|
-
await flushMicrotasks()
|
|
530
|
-
|
|
531
|
-
// Never touched the draft API at all.
|
|
532
|
-
expect(sendMessageDraft).not.toHaveBeenCalled()
|
|
533
|
-
})
|
|
534
|
-
|
|
535
|
-
it('forwards message_thread_id to the draft-clear call', async () => {
|
|
536
|
-
const sendMessage = makeSendMessage()
|
|
537
|
-
const editMessageText = makeEditMessageText()
|
|
538
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
539
|
-
const stream = createAnswerStream({
|
|
540
|
-
chatId: 'chat1',
|
|
541
|
-
isPrivateChat: true,
|
|
542
|
-
threadId: 42,
|
|
543
|
-
throttleMs: 250,
|
|
544
|
-
sendMessage,
|
|
545
|
-
editMessageText,
|
|
546
|
-
sendMessageDraft,
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
stream.update('mid-turn thought')
|
|
550
|
-
await flushMicrotasks()
|
|
551
|
-
|
|
552
|
-
await stream.retract()
|
|
553
|
-
|
|
554
|
-
const lastCall = sendMessageDraft.mock.calls[sendMessageDraft.mock.calls.length - 1] as unknown as [string, number, string, { message_thread_id?: number } | undefined]
|
|
555
|
-
expect(lastCall[2]).toBe('')
|
|
556
|
-
expect(lastCall[3]).toEqual({ message_thread_id: 42 })
|
|
557
|
-
})
|
|
558
|
-
})
|
|
559
|
-
|
|
560
|
-
// ─── #1792 — forceNewMessage clears the stale draftId before rotating ───
|
|
561
|
-
//
|
|
562
|
-
// Background: `forceNewMessage()` rotates `draftId` to a fresh allocation
|
|
563
|
-
// so the stream can be re-used for a new turn (typical caller: gateway
|
|
564
|
-
// rapid-steer path in `handleSessionEvent` enqueue branch — calls
|
|
565
|
-
// `forceNewMessage(); stop()` on the prior turn's stream before opening
|
|
566
|
-
// the new turn). Pre-#1792, the rotation orphaned the prior turn's
|
|
567
|
-
// draft content in the user's compose box until Telegram's 30 s draft
|
|
568
|
-
// expiry — `stop()`'s fire-and-forget clear closed over the (now-new)
|
|
569
|
-
// `draftId`, so the clear targeted the unused id, not the stale one.
|
|
570
|
-
//
|
|
571
|
-
// Post-fix: `forceNewMessage` itself clears the stale draftId BEFORE
|
|
572
|
-
// rotating. `stop()` continues to clear whatever draftId is current
|
|
573
|
-
// at the time it runs (defensive, also fine: clearing an unused id
|
|
574
|
-
// is a harmless no-op for the user).
|
|
575
|
-
|
|
576
|
-
describe('answer-stream — forceNewMessage clears the stale draft before rotating (#1792)', () => {
|
|
577
|
-
it('clears the pre-rotation draftId when forceNewMessage rotates', async () => {
|
|
578
|
-
const sendMessage = makeSendMessage()
|
|
579
|
-
const editMessageText = makeEditMessageText()
|
|
580
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
581
|
-
const stream = createAnswerStream({
|
|
582
|
-
chatId: 'chat1',
|
|
583
|
-
isPrivateChat: true,
|
|
584
|
-
throttleMs: 250,
|
|
585
|
-
sendMessage,
|
|
586
|
-
editMessageText,
|
|
587
|
-
sendMessageDraft,
|
|
588
|
-
})
|
|
589
|
-
|
|
590
|
-
// Open the stream — this allocates draftId N and fires sendDraft(N).
|
|
591
|
-
stream.update('first turn thought')
|
|
592
|
-
await flushMicrotasks()
|
|
593
|
-
expect(sendMessageDraft).toHaveBeenCalledTimes(1)
|
|
594
|
-
const staleDraftId = (sendMessageDraft.mock.calls[0] as unknown as [string, number, string, unknown])[1]
|
|
595
|
-
sendMessageDraft.mockClear()
|
|
596
|
-
|
|
597
|
-
// Rotate. forceNewMessage must enqueue a clear against the OLD
|
|
598
|
-
// draftId before bumping to the new allocation — pre-fix the
|
|
599
|
-
// stale content stayed in the compose box for 30 s.
|
|
600
|
-
stream.forceNewMessage()
|
|
601
|
-
await flushMicrotasks()
|
|
602
|
-
|
|
603
|
-
expect(sendMessageDraft).toHaveBeenCalledTimes(1)
|
|
604
|
-
const clearedId = (sendMessageDraft.mock.calls[0] as unknown as [string, number, string, unknown])[1]
|
|
605
|
-
const clearedText = (sendMessageDraft.mock.calls[0] as unknown as [string, number, string, unknown])[2]
|
|
606
|
-
expect(clearedId).toBe(staleDraftId)
|
|
607
|
-
expect(clearedText).toBe('')
|
|
608
|
-
})
|
|
609
|
-
|
|
610
|
-
it('the gateway sequence forceNewMessage(); stop() clears the stale draftId', async () => {
|
|
611
|
-
// Mirrors the only production caller — telegram-plugin/gateway/
|
|
612
|
-
// gateway.ts:6476-6477 cleans up the prior turn's answer-stream
|
|
613
|
-
// before opening a new turn (rapid steer / queue path).
|
|
614
|
-
const sendMessage = makeSendMessage()
|
|
615
|
-
const editMessageText = makeEditMessageText()
|
|
616
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
617
|
-
const stream = createAnswerStream({
|
|
618
|
-
chatId: 'chat1',
|
|
619
|
-
isPrivateChat: true,
|
|
620
|
-
throttleMs: 250,
|
|
621
|
-
sendMessage,
|
|
622
|
-
editMessageText,
|
|
623
|
-
sendMessageDraft,
|
|
624
|
-
})
|
|
625
|
-
|
|
626
|
-
stream.update('prior turn thought')
|
|
627
|
-
await flushMicrotasks()
|
|
628
|
-
const staleDraftId = (sendMessageDraft.mock.calls[0] as unknown as [string, number, string, unknown])[1]
|
|
629
|
-
sendMessageDraft.mockClear()
|
|
630
|
-
|
|
631
|
-
stream.forceNewMessage()
|
|
632
|
-
stream.stop()
|
|
633
|
-
await flushMicrotasks()
|
|
634
|
-
|
|
635
|
-
// The stale id must have been cleared by ONE of the two calls
|
|
636
|
-
// (forceNewMessage in this design); the new unused id may also
|
|
637
|
-
// be cleared by stop() — harmless. The load-bearing invariant
|
|
638
|
-
// is "the stale id reaches sendMessageDraft('') somewhere".
|
|
639
|
-
const clearedIds = (sendMessageDraft.mock.calls as unknown as Array<[string, number, string, unknown]>)
|
|
640
|
-
.filter(c => c[2] === '')
|
|
641
|
-
.map(c => c[1])
|
|
642
|
-
expect(clearedIds).toContain(staleDraftId)
|
|
643
|
-
})
|
|
644
|
-
})
|
|
645
|
-
|
|
646
311
|
describe('answer-stream — empty / whitespace-only text is a no-op', () => {
|
|
647
312
|
it('update("") does not trigger any transport call', async () => {
|
|
648
313
|
const sendMessage = makeSendMessage()
|
|
649
314
|
const editMessageText = makeEditMessageText()
|
|
650
315
|
const stream = createAnswerStream({
|
|
651
316
|
chatId: 'chat1',
|
|
652
|
-
isPrivateChat: false,
|
|
653
317
|
minInitialChars: 0,
|
|
654
318
|
throttleMs: 250,
|
|
655
319
|
sendMessage,
|
|
@@ -669,7 +333,6 @@ describe('answer-stream — empty / whitespace-only text is a no-op', () => {
|
|
|
669
333
|
const editMessageText = makeEditMessageText()
|
|
670
334
|
const stream = createAnswerStream({
|
|
671
335
|
chatId: 'chat1',
|
|
672
|
-
isPrivateChat: false,
|
|
673
336
|
minInitialChars: 0,
|
|
674
337
|
throttleMs: 250,
|
|
675
338
|
sendMessage,
|
|
@@ -691,7 +354,6 @@ describe('answer-stream — maxChars guard', () => {
|
|
|
691
354
|
const editMessageText = makeEditMessageText()
|
|
692
355
|
const stream = createAnswerStream({
|
|
693
356
|
chatId: 'chat1',
|
|
694
|
-
isPrivateChat: false,
|
|
695
357
|
minInitialChars: 0,
|
|
696
358
|
throttleMs: 250,
|
|
697
359
|
sendMessage,
|
|
@@ -708,40 +370,6 @@ describe('answer-stream — maxChars guard', () => {
|
|
|
708
370
|
})
|
|
709
371
|
})
|
|
710
372
|
|
|
711
|
-
describe('answer-stream — materialize() on draft transport', () => {
|
|
712
|
-
it('clears the draft and sends a fresh sendMessage on materialize', async () => {
|
|
713
|
-
const sendMessage = makeSendMessage()
|
|
714
|
-
const editMessageText = makeEditMessageText()
|
|
715
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
716
|
-
const stream = createAnswerStream({
|
|
717
|
-
chatId: 'chat1',
|
|
718
|
-
isPrivateChat: true,
|
|
719
|
-
minInitialChars: 0,
|
|
720
|
-
throttleMs: 250,
|
|
721
|
-
sendMessage,
|
|
722
|
-
editMessageText,
|
|
723
|
-
sendMessageDraft,
|
|
724
|
-
})
|
|
725
|
-
|
|
726
|
-
stream.update('hello world')
|
|
727
|
-
vi.advanceTimersByTime(500)
|
|
728
|
-
await flushMicrotasks()
|
|
729
|
-
|
|
730
|
-
expect(sendMessageDraft).toHaveBeenCalled()
|
|
731
|
-
const draftCallsBeforeMaterialize = sendMessageDraft.mock.calls.length
|
|
732
|
-
|
|
733
|
-
const finalId = await stream.materialize()
|
|
734
|
-
|
|
735
|
-
expect(sendMessage).toHaveBeenCalledTimes(1)
|
|
736
|
-
expect(sendMessage.mock.calls[0][1]).toBe('hello world')
|
|
737
|
-
expect(typeof finalId).toBe('number')
|
|
738
|
-
|
|
739
|
-
expect(sendMessageDraft.mock.calls.length).toBeGreaterThan(draftCallsBeforeMaterialize)
|
|
740
|
-
const lastDraftCall = sendMessageDraft.mock.calls[sendMessageDraft.mock.calls.length - 1]
|
|
741
|
-
expect(lastDraftCall[2]).toBe('')
|
|
742
|
-
})
|
|
743
|
-
})
|
|
744
|
-
|
|
745
373
|
describe('answer-stream — onSuperseded callback', () => {
|
|
746
374
|
it('invokes onSuperseded when a send resolves after forceNewMessage', async () => {
|
|
747
375
|
const onSuperseded = vi.fn()
|
|
@@ -757,7 +385,6 @@ describe('answer-stream — onSuperseded callback', () => {
|
|
|
757
385
|
const editMessageText = makeEditMessageText()
|
|
758
386
|
const stream = createAnswerStream({
|
|
759
387
|
chatId: 'chat1',
|
|
760
|
-
isPrivateChat: false,
|
|
761
388
|
minInitialChars: 0,
|
|
762
389
|
throttleMs: 250,
|
|
763
390
|
sendMessage,
|
|
@@ -793,7 +420,6 @@ describe('answer-stream — materialize() max-chars guard', () => {
|
|
|
793
420
|
const warn = vi.fn()
|
|
794
421
|
const stream = createAnswerStream({
|
|
795
422
|
chatId: 'chat1',
|
|
796
|
-
isPrivateChat: false,
|
|
797
423
|
minInitialChars: 0,
|
|
798
424
|
throttleMs: 250,
|
|
799
425
|
sendMessage,
|
|
@@ -824,7 +450,6 @@ describe('answer-stream — retract() (#251)', () => {
|
|
|
824
450
|
const deleteMessage = vi.fn(async () => {})
|
|
825
451
|
const stream = createAnswerStream({
|
|
826
452
|
chatId: 'chat251',
|
|
827
|
-
isPrivateChat: false,
|
|
828
453
|
minInitialChars: 0,
|
|
829
454
|
throttleMs: 250,
|
|
830
455
|
sendMessage,
|
|
@@ -858,7 +483,6 @@ describe('answer-stream — retract() (#251)', () => {
|
|
|
858
483
|
const deleteMessage = vi.fn(async () => {})
|
|
859
484
|
const stream = createAnswerStream({
|
|
860
485
|
chatId: 'chat251',
|
|
861
|
-
isPrivateChat: false,
|
|
862
486
|
// High threshold so the text below never triggers a send
|
|
863
487
|
minInitialChars: 5000,
|
|
864
488
|
throttleMs: 250,
|
|
@@ -887,7 +511,6 @@ describe('answer-stream — retract() (#251)', () => {
|
|
|
887
511
|
const deleteMessage = vi.fn(async () => {})
|
|
888
512
|
const stream = createAnswerStream({
|
|
889
513
|
chatId: 'chat251',
|
|
890
|
-
isPrivateChat: false,
|
|
891
514
|
minInitialChars: 0,
|
|
892
515
|
throttleMs: 250,
|
|
893
516
|
sendMessage,
|
|
@@ -923,7 +546,6 @@ describe('answer-stream — retract() (#251)', () => {
|
|
|
923
546
|
const warn = vi.fn()
|
|
924
547
|
const stream = createAnswerStream({
|
|
925
548
|
chatId: 'chat251',
|
|
926
|
-
isPrivateChat: false,
|
|
927
549
|
minInitialChars: 0,
|
|
928
550
|
throttleMs: 250,
|
|
929
551
|
sendMessage,
|
|
@@ -949,7 +571,6 @@ describe('answer-stream — retract() (#251)', () => {
|
|
|
949
571
|
const editMessageText = makeEditMessageText()
|
|
950
572
|
const stream = createAnswerStream({
|
|
951
573
|
chatId: 'chat251',
|
|
952
|
-
isPrivateChat: false,
|
|
953
574
|
minInitialChars: 0,
|
|
954
575
|
throttleMs: 250,
|
|
955
576
|
sendMessage,
|
|
@@ -968,13 +589,12 @@ describe('answer-stream — retract() (#251)', () => {
|
|
|
968
589
|
|
|
969
590
|
// ─── Issue #203: onMetric callback ──────────────────────────────────────────
|
|
970
591
|
describe('answer-stream — onMetric callback (#203)', () => {
|
|
971
|
-
it('fires answer_lane_update on first sendMessage (
|
|
592
|
+
it('fires answer_lane_update on first sendMessage (message transport)', async () => {
|
|
972
593
|
const onMetric = vi.fn()
|
|
973
594
|
const sendMessage = makeSendMessage()
|
|
974
595
|
const editMessageText = makeEditMessageText()
|
|
975
596
|
const stream = createAnswerStream({
|
|
976
597
|
chatId: 'chatX',
|
|
977
|
-
isPrivateChat: false,
|
|
978
598
|
minInitialChars: 0,
|
|
979
599
|
throttleMs: 250,
|
|
980
600
|
sendMessage,
|
|
@@ -1001,7 +621,6 @@ describe('answer-stream — onMetric callback (#203)', () => {
|
|
|
1001
621
|
const editMessageText = makeEditMessageText()
|
|
1002
622
|
const stream = createAnswerStream({
|
|
1003
623
|
chatId: 'chatX',
|
|
1004
|
-
isPrivateChat: false,
|
|
1005
624
|
minInitialChars: 0,
|
|
1006
625
|
throttleMs: 250,
|
|
1007
626
|
sendMessage,
|
|
@@ -1022,39 +641,12 @@ describe('answer-stream — onMetric callback (#203)', () => {
|
|
|
1022
641
|
expect(transports).toContain('edit')
|
|
1023
642
|
})
|
|
1024
643
|
|
|
1025
|
-
it('fires answer_lane_update on draft transport for DMs', async () => {
|
|
1026
|
-
const onMetric = vi.fn()
|
|
1027
|
-
const sendMessage = makeSendMessage()
|
|
1028
|
-
const editMessageText = makeEditMessageText()
|
|
1029
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
1030
|
-
const stream = createAnswerStream({
|
|
1031
|
-
chatId: 'chatX',
|
|
1032
|
-
isPrivateChat: true,
|
|
1033
|
-
minInitialChars: 0,
|
|
1034
|
-
throttleMs: 250,
|
|
1035
|
-
sendMessage,
|
|
1036
|
-
editMessageText,
|
|
1037
|
-
sendMessageDraft,
|
|
1038
|
-
onMetric,
|
|
1039
|
-
})
|
|
1040
|
-
|
|
1041
|
-
stream.update('streaming via draft')
|
|
1042
|
-
vi.advanceTimersByTime(500)
|
|
1043
|
-
await flushMicrotasks()
|
|
1044
|
-
|
|
1045
|
-
const draftEvents = onMetric.mock.calls
|
|
1046
|
-
.map((c) => c[0] as { kind: string; transport?: string })
|
|
1047
|
-
.filter((ev) => ev.kind === 'answer_lane_update' && ev.transport === 'draft')
|
|
1048
|
-
expect(draftEvents.length).toBeGreaterThan(0)
|
|
1049
|
-
})
|
|
1050
|
-
|
|
1051
644
|
it('fires answer_lane_materialized on materialize success', async () => {
|
|
1052
645
|
const onMetric = vi.fn()
|
|
1053
646
|
const sendMessage = makeSendMessage()
|
|
1054
647
|
const editMessageText = makeEditMessageText()
|
|
1055
648
|
const stream = createAnswerStream({
|
|
1056
649
|
chatId: 'chatX',
|
|
1057
|
-
isPrivateChat: false,
|
|
1058
650
|
minInitialChars: 0,
|
|
1059
651
|
throttleMs: 250,
|
|
1060
652
|
sendMessage,
|
|
@@ -1083,7 +675,6 @@ describe('answer-stream — onMetric callback (#203)', () => {
|
|
|
1083
675
|
const editMessageText = makeEditMessageText()
|
|
1084
676
|
const stream = createAnswerStream({
|
|
1085
677
|
chatId: 'chatX',
|
|
1086
|
-
isPrivateChat: false,
|
|
1087
678
|
minInitialChars: 0,
|
|
1088
679
|
throttleMs: 250,
|
|
1089
680
|
sendMessage,
|