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
|
@@ -128,7 +128,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
128
128
|
agentName: 'gymbro',
|
|
129
129
|
})
|
|
130
130
|
expect(body).toBe(
|
|
131
|
-
['🔐
|
|
131
|
+
['🔐 **Gymbro** wants to edit: supplement-log.md', 'why: _logging today\'s lifts_'].join('\n'),
|
|
132
132
|
)
|
|
133
133
|
})
|
|
134
134
|
|
|
@@ -141,7 +141,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
141
141
|
description: 'Restart an agent via the host-control daemon. cross-agent (`name` ≠ $SWITCHROOM_AGENT_NAME) …',
|
|
142
142
|
agentName: 'carrie',
|
|
143
143
|
})
|
|
144
|
-
expect(body).toContain('why:
|
|
144
|
+
expect(body).toContain('why: _gateway is wedged, bouncing it_')
|
|
145
145
|
expect(body).not.toContain('$SWITCHROOM_AGENT_NAME')
|
|
146
146
|
expect(body).not.toContain('host-control daemon')
|
|
147
147
|
})
|
|
@@ -153,7 +153,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
153
153
|
description: 'Run a shell command.',
|
|
154
154
|
agentName: 'gymbro',
|
|
155
155
|
})
|
|
156
|
-
expect(body).toContain('why:
|
|
156
|
+
expect(body).toContain('why: _listing temp files_')
|
|
157
157
|
})
|
|
158
158
|
|
|
159
159
|
test('shows "not provided" when no caller reason is present (never the description)', () => {
|
|
@@ -163,7 +163,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
163
163
|
description: 'Run a shell command on the host.',
|
|
164
164
|
agentName: 'gymbro',
|
|
165
165
|
})
|
|
166
|
-
expect(body).toContain('why:
|
|
166
|
+
expect(body).toContain('why: _not provided_')
|
|
167
167
|
expect(body).not.toContain('Run a shell command')
|
|
168
168
|
})
|
|
169
169
|
|
|
@@ -174,7 +174,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
174
174
|
description: 'Run a shell command.',
|
|
175
175
|
agentName: 'gymbro',
|
|
176
176
|
})
|
|
177
|
-
expect(body).toContain('why:
|
|
177
|
+
expect(body).toContain('why: _not provided_')
|
|
178
178
|
})
|
|
179
179
|
|
|
180
180
|
test('drops the agent prefix when agentName is null (early-boot edge)', () => {
|
|
@@ -184,21 +184,23 @@ describe('formatPermissionCardBody', () => {
|
|
|
184
184
|
description: 'Use a skill.',
|
|
185
185
|
agentName: null,
|
|
186
186
|
})
|
|
187
|
-
expect(body).toBe(['🔐 Use the mail skill', 'why:
|
|
187
|
+
expect(body).toBe(['🔐 Use the mail skill', 'why: _do the thing_'].join('\n'))
|
|
188
188
|
})
|
|
189
189
|
|
|
190
|
-
test('
|
|
190
|
+
test('markdown-escapes emphasis specials in agentName / action / reason; passes < > & through (#2669)', () => {
|
|
191
191
|
const body = formatPermissionCardBody({
|
|
192
192
|
toolName: 'Bash',
|
|
193
|
-
inputPreview: JSON.stringify({ command: 'echo "
|
|
193
|
+
inputPreview: JSON.stringify({ command: 'echo "a_b *c*"', reason: 'compare a_b and c < d' }),
|
|
194
194
|
description: 'Run a shell command.',
|
|
195
|
-
agentName: '
|
|
195
|
+
agentName: 'agent_test',
|
|
196
196
|
})
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
expect(body).
|
|
200
|
-
expect(body).toContain('<
|
|
201
|
-
|
|
197
|
+
// < > & are literal in rich markdown; the underscore in the agent name is
|
|
198
|
+
// backslash-escaped so it can't open an italic run.
|
|
199
|
+
expect(body).toContain('Agent\\_test')
|
|
200
|
+
expect(body).toContain('c < d')
|
|
201
|
+
// The card's own wrappers (** for the name, _ for the why line) are present.
|
|
202
|
+
expect(body).toContain('**')
|
|
203
|
+
expect(body).toContain('why: _')
|
|
202
204
|
})
|
|
203
205
|
|
|
204
206
|
test('truncates a very long caller reason with an ellipsis', () => {
|
|
@@ -208,8 +210,8 @@ describe('formatPermissionCardBody', () => {
|
|
|
208
210
|
description: 'Use a skill.',
|
|
209
211
|
agentName: 'clerk',
|
|
210
212
|
})
|
|
211
|
-
expect(body).toContain('xxxx
|
|
212
|
-
expect(body.split('\n')[0]).toBe('🔐
|
|
213
|
+
expect(body).toContain('xxxx…_')
|
|
214
|
+
expect(body.split('\n')[0]).toBe('🔐 **Clerk** wants to use the mail skill')
|
|
213
215
|
})
|
|
214
216
|
|
|
215
217
|
test('collapses internal whitespace in the caller reason', () => {
|
|
@@ -219,7 +221,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
219
221
|
description: 'Use a skill.',
|
|
220
222
|
agentName: 'clerk',
|
|
221
223
|
})
|
|
222
|
-
expect(body).toContain('why:
|
|
224
|
+
expect(body).toContain('why: _first second paragraph_')
|
|
223
225
|
})
|
|
224
226
|
|
|
225
227
|
// config-edit-hardening: upstream Claude Code truncates `inputPreview`
|
|
@@ -247,7 +249,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
247
249
|
description: 'Propose a unified-diff patch against switchroom.yaml.',
|
|
248
250
|
agentName: 'klanker',
|
|
249
251
|
})
|
|
250
|
-
expect(body).toContain(`why:
|
|
252
|
+
expect(body).toContain(`why: _${reason}_`)
|
|
251
253
|
expect(body).not.toContain('not provided')
|
|
252
254
|
})
|
|
253
255
|
|
|
@@ -263,7 +265,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
263
265
|
description: 'desc',
|
|
264
266
|
agentName: 'klanker',
|
|
265
267
|
})
|
|
266
|
-
expect(body).toContain(`why:
|
|
268
|
+
expect(body).toContain(`why: _${reason}_`)
|
|
267
269
|
})
|
|
268
270
|
|
|
269
271
|
// #2469: hostd agent_* cards must name WHICH agent is targeted, pulled
|
|
@@ -275,7 +277,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
275
277
|
description: 'Restart an agent via the host-control daemon. $SWITCHROOM_AGENT_NAME …',
|
|
276
278
|
agentName: 'klanker',
|
|
277
279
|
})
|
|
278
|
-
expect(body.split('\n')[0]).toBe('🔐
|
|
280
|
+
expect(body.split('\n')[0]).toBe('🔐 **Klanker** wants to restart agent `carrie` in the fleet')
|
|
279
281
|
})
|
|
280
282
|
|
|
281
283
|
test('hostd start/stop/logs/exec each name the target agent (#2469)', () => {
|
|
@@ -286,10 +288,10 @@ describe('formatPermissionCardBody', () => {
|
|
|
286
288
|
description: 'static schema doc',
|
|
287
289
|
agentName: 'klanker',
|
|
288
290
|
}).split('\n')[0]
|
|
289
|
-
expect(mk('mcp__hostd__agent_start')).toBe('🔐
|
|
290
|
-
expect(mk('mcp__hostd__agent_stop')).toBe('🔐
|
|
291
|
-
expect(mk('mcp__hostd__agent_logs')).toBe("🔐
|
|
292
|
-
expect(mk('mcp__hostd__agent_exec')).toBe('🔐
|
|
291
|
+
expect(mk('mcp__hostd__agent_start')).toBe('🔐 **Klanker** wants to start agent `pixel` in the fleet')
|
|
292
|
+
expect(mk('mcp__hostd__agent_stop')).toBe('🔐 **Klanker** wants to stop agent `pixel` in the fleet')
|
|
293
|
+
expect(mk('mcp__hostd__agent_logs')).toBe("🔐 **Klanker** wants to read agent `pixel`'s container logs")
|
|
294
|
+
expect(mk('mcp__hostd__agent_exec')).toBe('🔐 **Klanker** wants to run a read-only inspection inside agent `pixel`')
|
|
293
295
|
})
|
|
294
296
|
|
|
295
297
|
test('hostd agent verb without a name arg falls back to the generic phrase (no crash) (#2469)', () => {
|
|
@@ -299,8 +301,8 @@ describe('formatPermissionCardBody', () => {
|
|
|
299
301
|
description: 'static schema doc',
|
|
300
302
|
agentName: 'klanker',
|
|
301
303
|
})
|
|
302
|
-
expect(body.split('\n')[0]).toBe('🔐
|
|
303
|
-
expect(body).toContain('why:
|
|
304
|
+
expect(body.split('\n')[0]).toBe('🔐 **Klanker** wants to restart an agent in the fleet')
|
|
305
|
+
expect(body).toContain('why: _bouncing the fleet_')
|
|
304
306
|
})
|
|
305
307
|
|
|
306
308
|
test('non-name-arg gated verb (update_apply) stays generic and does not break (#2469)', () => {
|
|
@@ -310,8 +312,8 @@ describe('formatPermissionCardBody', () => {
|
|
|
310
312
|
description: 'static schema doc',
|
|
311
313
|
agentName: 'klanker',
|
|
312
314
|
})
|
|
313
|
-
expect(body.split('\n')[0]).toBe('🔐
|
|
314
|
-
expect(body).toContain('why:
|
|
315
|
+
expect(body.split('\n')[0]).toBe('🔐 **Klanker** wants to apply a fleet-wide update (pull + recreate)')
|
|
316
|
+
expect(body).toContain('why: _rolling out v0.16_')
|
|
315
317
|
})
|
|
316
318
|
|
|
317
319
|
// Clarity fix: the card gains a third "↳" line summarizing the REST
|
|
@@ -330,8 +332,8 @@ describe('formatPermissionCardBody', () => {
|
|
|
330
332
|
agentName: 'marko',
|
|
331
333
|
})
|
|
332
334
|
const lines = body.split('\n')
|
|
333
|
-
expect(lines[0]).toBe('🔐
|
|
334
|
-
expect(lines[1]).toBe('why:
|
|
335
|
+
expect(lines[0]).toBe('🔐 **Marko** wants to POST /smtp/email (Brevo)')
|
|
336
|
+
expect(lines[1]).toBe('why: _sending the priority-access invite_')
|
|
335
337
|
// Third line: scalar keys show value; the nested `to` array shows key-only.
|
|
336
338
|
expect(lines[2]).toContain('↳')
|
|
337
339
|
expect(lines[2]).toContain('subject: Priority access')
|
|
@@ -411,7 +413,7 @@ describe('formatPermissionResumeMessage — agent-voiced verdict ack', () => {
|
|
|
411
413
|
behavior: 'allow',
|
|
412
414
|
action: 'edit: supplement-log.md',
|
|
413
415
|
}),
|
|
414
|
-
).toBe('▶️
|
|
416
|
+
).toBe('▶️ **Gymbro** — got it, continuing: _edit: supplement-log.md_')
|
|
415
417
|
})
|
|
416
418
|
|
|
417
419
|
test('deny names what it will skip, lower-cased inline', () => {
|
|
@@ -421,7 +423,7 @@ describe('formatPermissionResumeMessage — agent-voiced verdict ack', () => {
|
|
|
421
423
|
behavior: 'deny',
|
|
422
424
|
action: 'Search the web',
|
|
423
425
|
}),
|
|
424
|
-
).toBe("🚫
|
|
426
|
+
).toBe("🚫 **Ziggy** — noted, I won't search the web. Continuing without it.")
|
|
425
427
|
})
|
|
426
428
|
|
|
427
429
|
test('TTL auto-deny variant reads as a timeout, not a tap', () => {
|
|
@@ -432,17 +434,19 @@ describe('formatPermissionResumeMessage — agent-voiced verdict ack', () => {
|
|
|
432
434
|
action: 'run: deploy.sh',
|
|
433
435
|
timeoutMinutes: 5,
|
|
434
436
|
}),
|
|
435
|
-
).toBe('🚫
|
|
437
|
+
).toBe('🚫 **Finn** — no answer in 5m, continuing without it (_run: deploy.sh_).')
|
|
436
438
|
})
|
|
437
439
|
|
|
438
|
-
test('
|
|
440
|
+
test('markdown-escapes a hostile action phrase (no raw ** emphasis injection) (#2669)', () => {
|
|
439
441
|
const out = formatPermissionResumeMessage({
|
|
440
442
|
agentName: 'clerk',
|
|
441
443
|
behavior: 'allow',
|
|
442
|
-
action: 'run: echo
|
|
444
|
+
action: 'run: echo **pwned**',
|
|
443
445
|
})
|
|
444
|
-
|
|
445
|
-
|
|
446
|
+
// The injected ** markers are backslash-escaped so they render literally
|
|
447
|
+
// and cannot hijack the card's own emphasis.
|
|
448
|
+
expect(out).toContain('\\*\\*pwned\\*\\*')
|
|
449
|
+
expect(out).not.toContain('echo **pwned**')
|
|
446
450
|
})
|
|
447
451
|
|
|
448
452
|
test('cap-first on the agent name', () => {
|
|
@@ -451,22 +455,22 @@ describe('formatPermissionResumeMessage — agent-voiced verdict ack', () => {
|
|
|
451
455
|
behavior: 'allow',
|
|
452
456
|
action: 'read: contract.pdf',
|
|
453
457
|
})
|
|
454
|
-
expect(out).toContain('
|
|
458
|
+
expect(out).toContain('**Lawgpt**')
|
|
455
459
|
})
|
|
456
460
|
|
|
457
461
|
test('empty action falls back to a generic continue (no dangling phrase)', () => {
|
|
458
462
|
expect(
|
|
459
463
|
formatPermissionResumeMessage({ agentName: 'carrie', behavior: 'allow', action: '' }),
|
|
460
|
-
).toBe('▶️
|
|
464
|
+
).toBe('▶️ **Carrie** — got it, back to work.')
|
|
461
465
|
expect(
|
|
462
466
|
formatPermissionResumeMessage({ agentName: 'carrie', behavior: 'deny', action: ' ' }),
|
|
463
|
-
).toBe('🚫
|
|
467
|
+
).toBe('🚫 **Carrie** — noted, continuing without it.')
|
|
464
468
|
})
|
|
465
469
|
|
|
466
470
|
test('null agent name degrades to a neutral "Agent" label', () => {
|
|
467
471
|
expect(
|
|
468
472
|
formatPermissionResumeMessage({ agentName: null, behavior: 'allow', action: 'edit: x.md' }),
|
|
469
|
-
).toBe('▶️
|
|
473
|
+
).toBe('▶️ **Agent** — got it, continuing: _edit: x.md_')
|
|
470
474
|
})
|
|
471
475
|
})
|
|
472
476
|
|
|
@@ -33,7 +33,6 @@ function makeDeps(
|
|
|
33
33
|
): PtyHandlerDeps {
|
|
34
34
|
return {
|
|
35
35
|
bot,
|
|
36
|
-
renderText: (t) => t, // identity for tests — easier to assert
|
|
37
36
|
...overrides,
|
|
38
37
|
}
|
|
39
38
|
}
|
|
@@ -93,17 +92,18 @@ describe('handlePtyPartialPure', () => {
|
|
|
93
92
|
expect(onFirstPartial).toHaveBeenCalledWith('1', 5)
|
|
94
93
|
})
|
|
95
94
|
|
|
96
|
-
it('
|
|
95
|
+
it('sends the raw PTY text LITERALLY (literalText path, no markdown render) (#2669)', () => {
|
|
96
|
+
// PTY-tail previews are raw terminal/TUI output — they go through the
|
|
97
|
+
// literal plain `sendMessage` path, NOT the rich-markdown parser, so
|
|
98
|
+
// control glyphs / box-drawing chars aren't misinterpreted as markdown.
|
|
97
99
|
const state = makeState({ currentSessionChatId: '1' })
|
|
98
|
-
const deps = makeDeps(bot
|
|
99
|
-
renderText: (t) => `<b>${t}</b>`,
|
|
100
|
-
})
|
|
100
|
+
const deps = makeDeps(bot)
|
|
101
101
|
|
|
102
|
-
handlePtyPartialPure('bold', state, deps)
|
|
103
|
-
await microtaskFlush()
|
|
102
|
+
handlePtyPartialPure('│ tool: ls -la │ *not bold*', state, deps)
|
|
104
103
|
|
|
105
104
|
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
106
|
-
expect(bot.api.
|
|
105
|
+
expect(bot.api.sendRichMessage).not.toHaveBeenCalled()
|
|
106
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('│ tool: ls -la │ *not bold*')
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
it('reuses an existing stream on second update', async () => {
|
|
@@ -91,9 +91,9 @@ describe('formatQuotaBlock', () => {
|
|
|
91
91
|
},
|
|
92
92
|
now,
|
|
93
93
|
)
|
|
94
|
-
expect(block).toContain('
|
|
95
|
-
expect(block).toContain('
|
|
96
|
-
expect(block).toContain('
|
|
94
|
+
expect(block).toContain('**Claude plan quota**')
|
|
95
|
+
expect(block).toContain('**5h window** 29% · resets in 2h 30m')
|
|
96
|
+
expect(block).toContain('**7d window** 33% · resets in 3d')
|
|
97
97
|
expect(block).toContain('Binding window: five hour')
|
|
98
98
|
// overage=allowed should not be surfaced
|
|
99
99
|
expect(block).not.toContain('Overage:')
|
|
@@ -249,17 +249,21 @@ describe("evaluateQuotaWatchAccount — message content", () => {
|
|
|
249
249
|
expect(d.message).toContain("30%");
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
-
it("throttling message
|
|
252
|
+
it("throttling message markdown-escapes the account label (#2669)", () => {
|
|
253
|
+
// The label is interpolated inside a code span — the renderer escapes
|
|
254
|
+
// inline-markdown specials (\ ` * _ ~ = [ ] |), not HTML entities. A label
|
|
255
|
+
// with an emphasis special (e.g. an underscore) is backslash-escaped; the
|
|
256
|
+
// literal `<` / `>` pass through (they are not markdown specials).
|
|
253
257
|
const d = evaluateQuotaWatchAccount({
|
|
254
258
|
agentName: "lawgpt",
|
|
255
|
-
snap: makeSnap("
|
|
259
|
+
snap: makeSnap("ev_il@example.com", makeQuota(85, 40)),
|
|
256
260
|
prev: PREV_NEVER_NOTIFIED,
|
|
257
261
|
now: NOW,
|
|
258
262
|
});
|
|
259
263
|
expect(d.kind).toBe("notify");
|
|
260
264
|
if (d.kind !== "notify") return;
|
|
261
|
-
|
|
262
|
-
expect(d.message).
|
|
265
|
+
// The underscore is escaped so it can't open an italic run inside the span.
|
|
266
|
+
expect(d.message).toContain("ev\\_il@example.com");
|
|
263
267
|
});
|
|
264
268
|
|
|
265
269
|
it("throttling message for active account mentions /auth use", () => {
|
|
@@ -126,8 +126,9 @@ describe("secret-detect — deleteSensitiveMessage helper retains its 'surface f
|
|
|
126
126
|
it("helper posts an in-chat warning naming the leaked message id", () => {
|
|
127
127
|
// The warning is the only signal a mobile-only operator gets —
|
|
128
128
|
// stderr is invisible to them. Pinning the in-chat surface as
|
|
129
|
-
// the load-bearing piece.
|
|
130
|
-
|
|
131
|
-
expect(helperBody).toMatch(/
|
|
129
|
+
// the load-bearing piece. Post-#2669 the warning ships via the
|
|
130
|
+
// rich-message path (sendRichMessage).
|
|
131
|
+
expect(helperBody).toMatch(/sendRichMessage|sendMessage/);
|
|
132
|
+
expect(helperBody).toMatch(/delete message.*manually|delete it manually|manually/i);
|
|
132
133
|
});
|
|
133
134
|
});
|
|
@@ -251,80 +251,52 @@ describe('silence-poke — #1292 tool-aware framework fallback', () => {
|
|
|
251
251
|
expect(ctx.inFlightTools[0]!.durationMs).toBe(305_000 - 30_000)
|
|
252
252
|
})
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
])
|
|
269
|
-
expect(text).toBe(
|
|
270
|
-
'running Grep "foo" + 2 more for 5m (no update from agent in 5 min)',
|
|
271
|
-
)
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
it('tool with no label renders the bare name', () => {
|
|
275
|
-
const text = formatFrameworkFallbackText('working', 305_000, [
|
|
276
|
-
{ name: 'Bash', label: null, durationMs: 305_000 },
|
|
277
|
-
])
|
|
278
|
-
expect(text).toBe(
|
|
279
|
-
'running Bash for 5m (no update from agent in 5 min)',
|
|
280
|
-
)
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
it('raw mcp__ tool name with a human label drops the technical name and leads with the label', () => {
|
|
284
|
-
// mcp__hindsight__reflect is the internal MCP identifier — looks
|
|
285
|
-
// like a leak when surfaced to a user. The label table emits
|
|
286
|
-
// "Searching memory" for it (see hooks/tool-label-pretool.mjs);
|
|
287
|
-
// the fallback message should lead with the label, not concatenate
|
|
288
|
-
// both.
|
|
289
|
-
const text = formatFrameworkFallbackText('working', 305_000, [
|
|
290
|
-
{ name: 'mcp__hindsight__reflect', label: 'Searching memory', durationMs: 305_000 },
|
|
291
|
-
])
|
|
292
|
-
expect(text).toBe(
|
|
293
|
-
'Searching memory for 5m (no update from agent in 5 min)',
|
|
294
|
-
)
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
it('raw mcp__ tool name with NO label falls back to the bare name (no leak-but-no-better-option)', () => {
|
|
298
|
-
const text = formatFrameworkFallbackText('working', 305_000, [
|
|
299
|
-
{ name: 'mcp__some-third-party__do_thing', label: null, durationMs: 305_000 },
|
|
300
|
-
])
|
|
301
|
-
expect(text).toBe(
|
|
302
|
-
'running mcp__some-third-party__do_thing for 5m (no update from agent in 5 min)',
|
|
303
|
-
)
|
|
254
|
+
// The "no update from agent in N min" stall notice is a retired stop-gap
|
|
255
|
+
// (operator: obsolete now that the live draft + the model's own pacing beats
|
|
256
|
+
// carry progress). `formatFrameworkFallbackText` now returns null for every
|
|
257
|
+
// stall variant — the generic "still working / still thinking" notice AND the
|
|
258
|
+
// #1292 tool-aware enrichment — so the gateway sends nothing for a plain
|
|
259
|
+
// stall (it still runs the unwedge teardown). Only `blockedOnApproval`
|
|
260
|
+
// still produces a user-visible string. The tests below pin that contract.
|
|
261
|
+
|
|
262
|
+
it('stall notice (in-flight tool) returns null — no user-visible send', () => {
|
|
263
|
+
expect(
|
|
264
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
265
|
+
{ name: 'Grep', label: '"foo"', durationMs: 275_000 },
|
|
266
|
+
]),
|
|
267
|
+
).toBeNull()
|
|
304
268
|
})
|
|
305
269
|
|
|
306
|
-
it('
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
270
|
+
it('stall notice (multiple in-flight tools) returns null', () => {
|
|
271
|
+
expect(
|
|
272
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
273
|
+
{ name: 'Grep', label: '"foo"', durationMs: 275_000 },
|
|
274
|
+
{ name: 'Read', label: 'config.ts', durationMs: 120_000 },
|
|
275
|
+
{ name: 'Bash', label: null, durationMs: 60_000 },
|
|
276
|
+
]),
|
|
277
|
+
).toBeNull()
|
|
313
278
|
})
|
|
314
279
|
|
|
315
|
-
it('
|
|
280
|
+
it('stall notice (raw mcp__ tool, with or without label) returns null', () => {
|
|
316
281
|
expect(
|
|
317
|
-
formatFrameworkFallbackText('working', 305_000, [
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
).toBe('still thinking… (no update from agent in 5 min)')
|
|
282
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
283
|
+
{ name: 'mcp__hindsight__reflect', label: 'Searching memory', durationMs: 305_000 },
|
|
284
|
+
]),
|
|
285
|
+
).toBeNull()
|
|
322
286
|
expect(
|
|
323
|
-
formatFrameworkFallbackText('working', 305_000
|
|
324
|
-
|
|
287
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
288
|
+
{ name: 'mcp__some-third-party__do_thing', label: null, durationMs: 305_000 },
|
|
289
|
+
]),
|
|
290
|
+
).toBeNull()
|
|
325
291
|
})
|
|
326
292
|
|
|
327
|
-
it('
|
|
293
|
+
it('generic stall notice (empty inFlightTools, working AND thinking) returns null', () => {
|
|
294
|
+
expect(formatFrameworkFallbackText('working', 305_000, [])).toBeNull()
|
|
295
|
+
expect(formatFrameworkFallbackText('thinking', 305_000, [])).toBeNull()
|
|
296
|
+
expect(formatFrameworkFallbackText('working', 305_000)).toBeNull()
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('blockedOnApproval still names the real blocker (the one surviving user-visible send)', () => {
|
|
328
300
|
expect(
|
|
329
301
|
formatFrameworkFallbackText('working', 305_000, [], true),
|
|
330
302
|
).toBe('waiting for your approval — tap Approve or Deny on the card above (5 min)')
|
|
@@ -338,18 +310,10 @@ describe('silence-poke — #1292 tool-aware framework fallback', () => {
|
|
|
338
310
|
).toBe('waiting for your approval — tap Approve or Deny on the card above (5 min)')
|
|
339
311
|
})
|
|
340
312
|
|
|
341
|
-
it('blockedOnApproval=false
|
|
313
|
+
it('blockedOnApproval=false is a stall → returns null (stop-gap retired)', () => {
|
|
342
314
|
expect(
|
|
343
315
|
formatFrameworkFallbackText('working', 305_000, [], false),
|
|
344
|
-
).
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
it('tool-aware wording wins over "thinking" — the actual observable beats the inferred kind', () => {
|
|
348
|
-
const text = formatFrameworkFallbackText('thinking', 305_000, [
|
|
349
|
-
{ name: 'Grep', label: '"foo"', durationMs: 305_000 },
|
|
350
|
-
])
|
|
351
|
-
expect(text.startsWith('running Grep')).toBe(true)
|
|
352
|
-
expect(text).not.toContain('still thinking')
|
|
316
|
+
).toBeNull()
|
|
353
317
|
})
|
|
354
318
|
|
|
355
319
|
it('tool completed before the fallback → empty snapshot → base wording', () => {
|
|
@@ -450,22 +414,21 @@ describe('silence-poke — #1292 tool-aware framework fallback', () => {
|
|
|
450
414
|
expect(__getStateForTests('k')!.inFlightTools.size).toBe(0)
|
|
451
415
|
})
|
|
452
416
|
|
|
453
|
-
it('
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
expect(
|
|
458
|
-
'
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
it('formatFrameworkFallbackText truncates very long labels', () => {
|
|
417
|
+
it('in-flight tool stall (any duration/label) returns null — stop-gap retired', () => {
|
|
418
|
+
// Previously rendered "running Grep foo for 12s (…)" / truncated long
|
|
419
|
+
// labels. The whole tool-aware stall enrichment is gone now, so the
|
|
420
|
+
// duration + label formatting paths no longer surface — they return null.
|
|
421
|
+
expect(
|
|
422
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
423
|
+
{ name: 'Grep', label: 'foo', durationMs: 12_000 },
|
|
424
|
+
]),
|
|
425
|
+
).toBeNull()
|
|
463
426
|
const longLabel = '"' + 'x'.repeat(120) + '"'
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
427
|
+
expect(
|
|
428
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
429
|
+
{ name: 'Grep', label: longLabel, durationMs: 305_000 },
|
|
430
|
+
]),
|
|
431
|
+
).toBeNull()
|
|
469
432
|
})
|
|
470
433
|
})
|
|
471
434
|
|
|
@@ -528,38 +491,38 @@ describe('silence-poke — fallback handler errors do not break timer', () => {
|
|
|
528
491
|
})
|
|
529
492
|
|
|
530
493
|
// CC-4 from `docs/status-ask-cause-classes.md`: wording is load-bearing
|
|
531
|
-
// (`reference/rfcs/conversational-pacing.md` § Safety net).
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
494
|
+
// (`reference/rfcs/conversational-pacing.md` § Safety net). The stall notice is
|
|
495
|
+
// a retired stop-gap — its wording is gone (returns null). The ONE surviving
|
|
496
|
+
// user-visible string is the approval-blocked re-ping; snapshot it here so a
|
|
497
|
+
// refactor that drops the phrase fails loud. The `N min` parenthetical is still
|
|
498
|
+
// derived from silenceMs (honest), so pin that too.
|
|
535
499
|
describe('silence-poke — wording snapshots (CC-4)', () => {
|
|
536
|
-
it('framework fallback — working
|
|
537
|
-
expect(formatFrameworkFallbackText('working', 300_000)).
|
|
538
|
-
|
|
539
|
-
)
|
|
500
|
+
it('framework fallback — generic stall (working/thinking) emits no message', () => {
|
|
501
|
+
expect(formatFrameworkFallbackText('working', 300_000)).toBeNull()
|
|
502
|
+
expect(formatFrameworkFallbackText('thinking', 300_000)).toBeNull()
|
|
540
503
|
})
|
|
541
504
|
|
|
542
|
-
it('
|
|
543
|
-
expect(formatFrameworkFallbackText('
|
|
544
|
-
`"
|
|
505
|
+
it('approval-blocked re-ping — exact wording', () => {
|
|
506
|
+
expect(formatFrameworkFallbackText('working', 300_000, [], true)).toMatchInlineSnapshot(
|
|
507
|
+
`"waiting for your approval — tap Approve or Deny on the card above (5 min)"`,
|
|
545
508
|
)
|
|
546
509
|
})
|
|
547
510
|
|
|
548
|
-
it('
|
|
549
|
-
expect(formatFrameworkFallbackText('working', 360_000)).toBe(
|
|
550
|
-
'
|
|
511
|
+
it('approval-blocked — minutes derived from silenceMs, not hard-coded', () => {
|
|
512
|
+
expect(formatFrameworkFallbackText('working', 360_000, [], true)).toBe(
|
|
513
|
+
'waiting for your approval — tap Approve or Deny on the card above (6 min)',
|
|
551
514
|
)
|
|
552
|
-
expect(formatFrameworkFallbackText('working', 600_000)).toBe(
|
|
553
|
-
'
|
|
515
|
+
expect(formatFrameworkFallbackText('working', 600_000, [], true)).toBe(
|
|
516
|
+
'waiting for your approval — tap Approve or Deny on the card above (10 min)',
|
|
554
517
|
)
|
|
555
518
|
})
|
|
556
519
|
|
|
557
|
-
it('
|
|
558
|
-
expect(formatFrameworkFallbackText('working', 30_000)).toBe(
|
|
559
|
-
'
|
|
520
|
+
it('approval-blocked — minutes floor at 1 even when silenceMs is small', () => {
|
|
521
|
+
expect(formatFrameworkFallbackText('working', 30_000, [], true)).toBe(
|
|
522
|
+
'waiting for your approval — tap Approve or Deny on the card above (1 min)',
|
|
560
523
|
)
|
|
561
|
-
expect(formatFrameworkFallbackText('working', 0)).toBe(
|
|
562
|
-
'
|
|
524
|
+
expect(formatFrameworkFallbackText('working', 0, [], true)).toBe(
|
|
525
|
+
'waiting for your approval — tap Approve or Deny on the card above (1 min)',
|
|
563
526
|
)
|
|
564
527
|
})
|
|
565
528
|
})
|