switchroom 0.16.22 โ 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/cli/self-improve-apply-guard-pretool.mjs +6 -0
- package/dist/cli/skill-validate-pretool.mjs +2865 -48
- package/dist/cli/switchroom.js +338 -92
- package/dist/host-control/main.js +1 -1
- 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/gateway/gateway.js +1670 -1892
- 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 +44 -2
- 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
|
@@ -4,15 +4,16 @@ import {
|
|
|
4
4
|
MIN_INITIAL_CHARS,
|
|
5
5
|
} from '../answer-stream.js'
|
|
6
6
|
import { resolveAnswerLaneConfig, ANSWER_LANE_NEVER_OPENS } from '../answer-stream-flag.js'
|
|
7
|
-
import { markdownToHtml } from '../format.js'
|
|
8
|
-
import { sanitizeTelegramHtml } from '../html-sanitize.js'
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
// #2669: the answer lane no longer converts markdown โ HTML. It ships the RAW
|
|
9
|
+
// assistant transcript markdown through its sendMessage/editMessageText
|
|
10
|
+
// closures, which the gateway wires to the rich-message path
|
|
11
|
+
// (`sendRichMessage` / `editMessageText({ markdown })`). There is no
|
|
12
|
+
// renderText / parse_mode anymore โ the body IS the raw GFM markdown.
|
|
13
|
+
|
|
14
|
+
// The rich-message wire cap the answer lane guards against (matches
|
|
15
|
+
// answer-stream.ts's TELEGRAM_MAX_CHARS).
|
|
16
|
+
const TELEGRAM_MAX_CHARS = 32768
|
|
16
17
|
|
|
17
18
|
// โโโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
18
19
|
|
|
@@ -137,27 +138,27 @@ describe('answer-stream โ minInitialChars threshold', () => {
|
|
|
137
138
|
expect(sendMessage).toHaveBeenCalledWith(
|
|
138
139
|
'chat1',
|
|
139
140
|
'y'.repeat(500),
|
|
140
|
-
|
|
141
|
+
// #2669: no parse_mode โ the body ships as raw markdown via the rich path.
|
|
142
|
+
expect.not.objectContaining({ parse_mode: 'HTML' }),
|
|
141
143
|
)
|
|
142
144
|
expect(editMessageText).not.toHaveBeenCalled()
|
|
143
145
|
})
|
|
144
146
|
})
|
|
145
147
|
|
|
146
|
-
describe('answer-stream โ
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
148
|
+
describe('answer-stream โ raw GFM markdown passthrough (#2669)', () => {
|
|
149
|
+
// The answer lane historically shipped the RAW assistant transcript under
|
|
150
|
+
// parse_mode:'HTML', so `**bold**` arrived as literal asterisks. Post-#2669
|
|
151
|
+
// there is ONE rich-markdown path: the lane ships the raw markdown VERBATIM
|
|
152
|
+
// (the gateway wraps it in `sendRichMessage` / `editMessageText({ markdown })`),
|
|
153
|
+
// so `**bold**` reaches Telegram intact and renders as bold there.
|
|
152
154
|
|
|
153
|
-
it('
|
|
155
|
+
it('ships raw markdown verbatim on the opening sendMessage (no HTML conversion)', async () => {
|
|
154
156
|
const sendMessage = makeSendMessage()
|
|
155
157
|
const editMessageText = makeEditMessageText()
|
|
156
158
|
const stream = createAnswerStream({
|
|
157
159
|
chatId: 'chat1',
|
|
158
160
|
minInitialChars: 10,
|
|
159
161
|
throttleMs: 250,
|
|
160
|
-
renderText,
|
|
161
162
|
sendMessage,
|
|
162
163
|
editMessageText,
|
|
163
164
|
})
|
|
@@ -167,33 +168,27 @@ describe('answer-stream โ markdown โ HTML conversion (renderText)', () => {
|
|
|
167
168
|
|
|
168
169
|
expect(sendMessage).toHaveBeenCalledTimes(1)
|
|
169
170
|
const sentText = sendMessage.mock.calls[0][1] as string
|
|
170
|
-
|
|
171
|
-
expect(sentText).
|
|
172
|
-
expect(
|
|
173
|
-
|
|
174
|
-
expect.stringContaining('<b>bold</b>'),
|
|
175
|
-
expect.objectContaining({ parse_mode: 'HTML' }),
|
|
176
|
-
)
|
|
171
|
+
// Raw markdown passes through unchanged โ no <b> conversion.
|
|
172
|
+
expect(sentText).toContain('**bold**')
|
|
173
|
+
expect(sentText).not.toContain('<b>bold</b>')
|
|
174
|
+
expect(sendMessage.mock.calls[0][2]).not.toHaveProperty('parse_mode')
|
|
177
175
|
})
|
|
178
176
|
|
|
179
|
-
it('
|
|
177
|
+
it('ships raw markdown verbatim on a follow-up editMessageText', async () => {
|
|
180
178
|
const sendMessage = makeSendMessage()
|
|
181
179
|
const editMessageText = makeEditMessageText()
|
|
182
180
|
const stream = createAnswerStream({
|
|
183
181
|
chatId: 'chat1',
|
|
184
182
|
minInitialChars: 10,
|
|
185
183
|
throttleMs: 250,
|
|
186
|
-
renderText,
|
|
187
184
|
sendMessage,
|
|
188
185
|
editMessageText,
|
|
189
186
|
})
|
|
190
187
|
|
|
191
|
-
// First update opens the message.
|
|
192
188
|
stream.update('first chunk of the answer arriving')
|
|
193
189
|
await flushMicrotasks()
|
|
194
190
|
expect(sendMessage).toHaveBeenCalledTimes(1)
|
|
195
191
|
|
|
196
|
-
// Second update (past the throttle window) edits in place with markdown.
|
|
197
192
|
vi.advanceTimersByTime(300)
|
|
198
193
|
stream.update('first chunk of the answer arriving with **bold** added')
|
|
199
194
|
vi.advanceTimersByTime(300)
|
|
@@ -201,54 +196,31 @@ describe('answer-stream โ markdown โ HTML conversion (renderText)', () => {
|
|
|
201
196
|
|
|
202
197
|
expect(editMessageText).toHaveBeenCalled()
|
|
203
198
|
const editText = editMessageText.mock.calls.at(-1)![2] as string
|
|
204
|
-
expect(editText).toContain('
|
|
205
|
-
expect(editText).not.toContain('
|
|
199
|
+
expect(editText).toContain('**bold**')
|
|
200
|
+
expect(editText).not.toContain('<b>bold</b>')
|
|
206
201
|
})
|
|
207
202
|
|
|
208
|
-
it('
|
|
203
|
+
it('ships raw markdown verbatim on materialize', async () => {
|
|
209
204
|
const sendMessage = makeSendMessage()
|
|
210
205
|
const editMessageText = makeEditMessageText()
|
|
211
206
|
const stream = createAnswerStream({
|
|
212
207
|
chatId: 'chat1',
|
|
213
208
|
minInitialChars: 10,
|
|
214
209
|
throttleMs: 250,
|
|
215
|
-
renderText,
|
|
216
210
|
sendMessage,
|
|
217
211
|
editMessageText,
|
|
218
212
|
})
|
|
219
213
|
|
|
220
214
|
stream.update('The final answer is **definitely** forty-two.')
|
|
221
215
|
await flushMicrotasks()
|
|
222
|
-
// Opening send already converted; capture how many sends precede materialize.
|
|
223
216
|
const sendsBeforeMaterialize = sendMessage.mock.calls.length
|
|
224
217
|
|
|
225
218
|
const id = await stream.materialize()
|
|
226
219
|
expect(typeof id).toBe('number')
|
|
227
|
-
// materialize always sends a fresh message for the push notification.
|
|
228
220
|
expect(sendMessage.mock.calls.length).toBe(sendsBeforeMaterialize + 1)
|
|
229
221
|
const sentText = sendMessage.mock.calls.at(-1)![1] as string
|
|
230
|
-
expect(sentText).toContain('
|
|
231
|
-
expect(sentText).not.toContain('
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
it('sends raw text verbatim when no renderText is injected (back-compat)', async () => {
|
|
235
|
-
const sendMessage = makeSendMessage()
|
|
236
|
-
const editMessageText = makeEditMessageText()
|
|
237
|
-
const stream = createAnswerStream({
|
|
238
|
-
chatId: 'chat1',
|
|
239
|
-
minInitialChars: 10,
|
|
240
|
-
throttleMs: 250,
|
|
241
|
-
sendMessage,
|
|
242
|
-
editMessageText,
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
stream.update('Here is some **bold** narration text for the user.')
|
|
246
|
-
await flushMicrotasks()
|
|
247
|
-
|
|
248
|
-
expect(sendMessage).toHaveBeenCalledTimes(1)
|
|
249
|
-
const sentText = sendMessage.mock.calls[0][1] as string
|
|
250
|
-
// No renderer โ unchanged (preserves prior behaviour for unwired callers).
|
|
251
|
-
expect(sentText).toContain('**bold**')
|
|
222
|
+
expect(sentText).toContain('**definitely**')
|
|
223
|
+
expect(sentText).not.toContain('<b>definitely</b>')
|
|
252
224
|
})
|
|
253
225
|
})
|
|
254
226
|
|
|
@@ -467,7 +439,7 @@ describe('answer-stream โ empty / whitespace-only text is a no-op', () => {
|
|
|
467
439
|
})
|
|
468
440
|
|
|
469
441
|
describe('answer-stream โ maxChars guard', () => {
|
|
470
|
-
it('does not send when text exceeds
|
|
442
|
+
it('does not send when text exceeds the rich-message cap (#2669)', async () => {
|
|
471
443
|
const sendMessage = makeSendMessage()
|
|
472
444
|
const editMessageText = makeEditMessageText()
|
|
473
445
|
const stream = createAnswerStream({
|
|
@@ -478,14 +450,32 @@ describe('answer-stream โ maxChars guard', () => {
|
|
|
478
450
|
editMessageText,
|
|
479
451
|
})
|
|
480
452
|
|
|
481
|
-
//
|
|
482
|
-
stream.update('z'.repeat(
|
|
453
|
+
// One char over the 32768 rich-message limit.
|
|
454
|
+
stream.update('z'.repeat(TELEGRAM_MAX_CHARS + 1))
|
|
483
455
|
vi.advanceTimersByTime(1000)
|
|
484
456
|
await flushMicrotasks()
|
|
485
457
|
|
|
486
458
|
expect(sendMessage).not.toHaveBeenCalled()
|
|
487
459
|
expect(editMessageText).not.toHaveBeenCalled()
|
|
488
460
|
})
|
|
461
|
+
|
|
462
|
+
it('DOES send a body that fits under the 32768 cap (was rejected under the old 4096)', async () => {
|
|
463
|
+
const sendMessage = makeSendMessage()
|
|
464
|
+
const editMessageText = makeEditMessageText()
|
|
465
|
+
const stream = createAnswerStream({
|
|
466
|
+
chatId: 'chat1',
|
|
467
|
+
minInitialChars: 0,
|
|
468
|
+
throttleMs: 250,
|
|
469
|
+
sendMessage,
|
|
470
|
+
editMessageText,
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
// 8000 chars โ over the legacy 4096 cap but well under the rich cap.
|
|
474
|
+
stream.update('z'.repeat(8000))
|
|
475
|
+
await flushMicrotasks()
|
|
476
|
+
|
|
477
|
+
expect(sendMessage).toHaveBeenCalledTimes(1)
|
|
478
|
+
})
|
|
489
479
|
})
|
|
490
480
|
|
|
491
481
|
describe('answer-stream โ onSuperseded callback', () => {
|
|
@@ -532,7 +522,7 @@ describe('answer-stream โ onSuperseded callback', () => {
|
|
|
532
522
|
})
|
|
533
523
|
|
|
534
524
|
describe('answer-stream โ materialize() max-chars guard', () => {
|
|
535
|
-
it('does not send when buffered text exceeds
|
|
525
|
+
it('does not send when buffered text exceeds the rich-message cap (#2669)', async () => {
|
|
536
526
|
const sendMessage = makeSendMessage()
|
|
537
527
|
const editMessageText = makeEditMessageText()
|
|
538
528
|
const warn = vi.fn()
|
|
@@ -550,13 +540,14 @@ describe('answer-stream โ materialize() max-chars guard', () => {
|
|
|
550
540
|
await flushMicrotasks()
|
|
551
541
|
const sendsAfterStreaming = sendMessage.mock.calls.length
|
|
552
542
|
|
|
553
|
-
|
|
543
|
+
// One char over the 32768 rich cap.
|
|
544
|
+
stream.update('b'.repeat(TELEGRAM_MAX_CHARS + 1))
|
|
554
545
|
const result = await stream.materialize()
|
|
555
546
|
|
|
556
547
|
expect(result).toBeUndefined()
|
|
557
548
|
expect(sendMessage.mock.calls.length).toBe(sendsAfterStreaming)
|
|
558
549
|
expect(warn).toHaveBeenCalled()
|
|
559
|
-
expect(warn.mock.calls.some((c) => /
|
|
550
|
+
expect(warn.mock.calls.some((c) => /32768|exceeds/i.test(String(c[0])))).toBe(true)
|
|
560
551
|
})
|
|
561
552
|
})
|
|
562
553
|
|
|
@@ -800,7 +791,7 @@ describe('answer-stream โ onMetric callback (#203)', () => {
|
|
|
800
791
|
onMetric,
|
|
801
792
|
})
|
|
802
793
|
|
|
803
|
-
stream.update('x'.repeat(
|
|
794
|
+
stream.update('x'.repeat(TELEGRAM_MAX_CHARS + 1))
|
|
804
795
|
onMetric.mockClear()
|
|
805
796
|
const id = await stream.materialize()
|
|
806
797
|
|
|
@@ -77,7 +77,7 @@ function mockClient(over: Partial<AuthBrokerClient> = {}): AuthBrokerClient {
|
|
|
77
77
|
describe('renderShowText โ Format 2 vs legacy', () => {
|
|
78
78
|
it('falls back to legacy ASCII table when no liveQuotas given', () => {
|
|
79
79
|
const out = renderShowText(FIXTURE_STATE, NOW_MS);
|
|
80
|
-
expect(out).toContain('
|
|
80
|
+
expect(out).toContain('**Auth โ fleet snapshot**');
|
|
81
81
|
expect(out).toContain('ACCOUNT');
|
|
82
82
|
expect(out).toContain('STATUS');
|
|
83
83
|
expect(out).toContain('EXPIRES');
|
|
@@ -91,10 +91,10 @@ describe('renderShowText โ Format 2 vs legacy', () => {
|
|
|
91
91
|
tz: 'UTC',
|
|
92
92
|
liveProbedAtMs: NOW_MS,
|
|
93
93
|
});
|
|
94
|
-
expect(out).toContain('๐
|
|
94
|
+
expect(out).toContain('๐ **Auth โ fleet status**');
|
|
95
95
|
expect(out).toContain('Recommendation:');
|
|
96
|
-
expect(out).toContain('๐ด
|
|
97
|
-
expect(out).toContain('๐ข
|
|
96
|
+
expect(out).toContain('๐ด **BLOCKED**');
|
|
97
|
+
expect(out).toContain('๐ข **HEALTHY**');
|
|
98
98
|
// Legacy ASCII column headers should be absent
|
|
99
99
|
expect(out).not.toContain('ACCOUNT STATUS');
|
|
100
100
|
});
|
|
@@ -511,10 +511,11 @@ describe('handleAuthCommand โ help text lists every verb', () => {
|
|
|
511
511
|
{ agentName: 'x', isAdmin: true, client },
|
|
512
512
|
)
|
|
513
513
|
const text = reply.text
|
|
514
|
-
// Verbs (all variants). The help is
|
|
514
|
+
// Verbs (all variants). The help is GFM markdown (#2669); each verb is
|
|
515
|
+
// wrapped in a `code span`, so `<agent>` renders literally โ no HTML escape.
|
|
515
516
|
for (const fragment of [
|
|
516
517
|
'/auth show',
|
|
517
|
-
'/auth show
|
|
518
|
+
'/auth show <agent>',
|
|
518
519
|
'/auth list',
|
|
519
520
|
'/auth use',
|
|
520
521
|
'/auth rotate',
|
|
@@ -194,16 +194,16 @@ describe('renderAuthSnapshotFormat2', () => {
|
|
|
194
194
|
it('renders three health-grouped sections (BLOCKED first, then HEALTHY)', () => {
|
|
195
195
|
const out = renderAuthSnapshotFormat2(fixtureSnaps, { now: NOW, tz: 'UTC' });
|
|
196
196
|
// Headers present
|
|
197
|
-
expect(out).toContain('๐
|
|
198
|
-
expect(out).toContain('๐ด
|
|
199
|
-
expect(out).toContain('๐ข
|
|
197
|
+
expect(out).toContain('๐ **Auth โ fleet status**');
|
|
198
|
+
expect(out).toContain('๐ด **BLOCKED** (1)');
|
|
199
|
+
expect(out).toContain('๐ข **HEALTHY** (2)');
|
|
200
200
|
// Order: BLOCKED before HEALTHY
|
|
201
201
|
expect(out.indexOf('๐ด')).toBeLessThan(out.indexOf('๐ข'));
|
|
202
202
|
});
|
|
203
203
|
|
|
204
204
|
it('marks the active account with โ', () => {
|
|
205
205
|
const out = renderAuthSnapshotFormat2(fixtureSnaps, { now: NOW, tz: 'UTC' });
|
|
206
|
-
expect(out).toMatch(/โ\s
|
|
206
|
+
expect(out).toMatch(/โ\s*`you@example\.com`/);
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
it('shows "back โฆ" for blocked accounts with binding-window word', () => {
|
|
@@ -253,7 +253,7 @@ describe('renderAuthSnapshotFormat2', () => {
|
|
|
253
253
|
const out = renderAuthSnapshotFormat2(errSnaps, { now: NOW });
|
|
254
254
|
expect(out).toContain('quota probe failed');
|
|
255
255
|
expect(out).toContain('HTTP 401');
|
|
256
|
-
expect(out).toContain('โช
|
|
256
|
+
expect(out).toContain('โช **UNKNOWN**');
|
|
257
257
|
});
|
|
258
258
|
|
|
259
259
|
it('renders refresh stamp when liveProbedAtMs given', () => {
|
|
@@ -261,7 +261,7 @@ describe('renderAuthSnapshotFormat2', () => {
|
|
|
261
261
|
now: NOW,
|
|
262
262
|
liveProbedAtMs: NOW.getTime() - 12_000,
|
|
263
263
|
});
|
|
264
|
-
expect(out).toMatch(
|
|
264
|
+
expect(out).toMatch(/_Live ยท refreshed \d+s ago_/);
|
|
265
265
|
});
|
|
266
266
|
|
|
267
267
|
it('#2495 Change 2 โ renders "โ cached Nm ago" (NOT a live stamp) on staleCachedAtMs', () => {
|
|
@@ -269,7 +269,7 @@ describe('renderAuthSnapshotFormat2', () => {
|
|
|
269
269
|
now: NOW,
|
|
270
270
|
staleCachedAtMs: NOW.getTime() - 3 * 60_000, // 3 min old cache
|
|
271
271
|
});
|
|
272
|
-
expect(out).toMatch(
|
|
272
|
+
expect(out).toMatch(/_โ cached 3m ago_/);
|
|
273
273
|
// Crucially: no false live stamp.
|
|
274
274
|
expect(out).not.toContain('Live ยท refreshed');
|
|
275
275
|
});
|
|
@@ -302,7 +302,7 @@ describe('renderAuthSnapshotFormat2', () => {
|
|
|
302
302
|
expect(out).not.toContain('you@example.com');
|
|
303
303
|
// Masked fakes present (pool order from a clean cache: blocked-first
|
|
304
304
|
// render order means bob is masked first, then alice/you in healthy).
|
|
305
|
-
expect(out).toMatch(
|
|
305
|
+
expect(out).toMatch(/`[^@<]+@example\.com`/);
|
|
306
306
|
});
|
|
307
307
|
|
|
308
308
|
it('WITH demo, the recommendation footer masks the active label', () => {
|
|
@@ -381,7 +381,7 @@ describe('renderFallbackAnnouncement', () => {
|
|
|
381
381
|
now: NOW,
|
|
382
382
|
tz: 'UTC',
|
|
383
383
|
});
|
|
384
|
-
expect(out).toContain('Triggered by: agent
|
|
384
|
+
expect(out).toContain('Triggered by: agent **carrie**');
|
|
385
385
|
expect(out).toMatch(/ken@x.*recovers.*in 4h 57m/);
|
|
386
386
|
});
|
|
387
387
|
|
|
@@ -419,7 +419,7 @@ describe('renderFallbackAnnouncement', () => {
|
|
|
419
419
|
now: NOW,
|
|
420
420
|
tz: 'UTC',
|
|
421
421
|
});
|
|
422
|
-
expect(out).toContain('๐ด
|
|
422
|
+
expect(out).toContain('๐ด **All accounts blocked');
|
|
423
423
|
expect(out).toMatch(/ken@x recovers.*in 4h 57m/);
|
|
424
424
|
expect(out).toContain('/auth add');
|
|
425
425
|
});
|
|
@@ -461,7 +461,7 @@ describe('renderFallbackAnnouncement', () => {
|
|
|
461
461
|
now: NOW,
|
|
462
462
|
tz: 'UTC',
|
|
463
463
|
});
|
|
464
|
-
expect(out).toContain('๐ด
|
|
464
|
+
expect(out).toContain('๐ด **All accounts blocked');
|
|
465
465
|
// Every account is listed (not just the trigger).
|
|
466
466
|
expect(out).toContain('ken@x');
|
|
467
467
|
expect(out).toContain('you@x');
|
|
@@ -474,7 +474,7 @@ describe('renderFallbackAnnouncement', () => {
|
|
|
474
474
|
expect(out).toMatch(/quota exhausted/);
|
|
475
475
|
// The earliest recovery across the fleet (carol, 5h reset at 03:00Z = ~2h)
|
|
476
476
|
// is called out explicitly.
|
|
477
|
-
expect(out).toMatch(/Earliest recovery:\s
|
|
477
|
+
expect(out).toMatch(/Earliest recovery:\s*`carol@x`/);
|
|
478
478
|
expect(out).toContain('/auth add');
|
|
479
479
|
});
|
|
480
480
|
|
|
@@ -488,7 +488,7 @@ describe('renderFallbackAnnouncement', () => {
|
|
|
488
488
|
now: NOW,
|
|
489
489
|
tz: 'UTC',
|
|
490
490
|
});
|
|
491
|
-
expect(out).toContain('๐ด
|
|
491
|
+
expect(out).toContain('๐ด **All accounts blocked');
|
|
492
492
|
expect(out).toMatch(/ken@x recovers.*in 4h 57m/);
|
|
493
493
|
expect(out).not.toContain('Earliest recovery:');
|
|
494
494
|
});
|
|
@@ -866,10 +866,11 @@ describe('#2494 โ renderAuthSnapshotFormat2 row rendering (out_of_credits demo
|
|
|
866
866
|
{ now: NOW },
|
|
867
867
|
);
|
|
868
868
|
// Must be in HEALTHY group, not BLOCKED
|
|
869
|
-
expect(out).toContain('๐ข
|
|
870
|
-
expect(out).not.toContain('๐ด
|
|
871
|
-
// Must have informational annotation
|
|
872
|
-
|
|
869
|
+
expect(out).toContain('๐ข **HEALTHY**');
|
|
870
|
+
expect(out).not.toContain('๐ด **BLOCKED**');
|
|
871
|
+
// Must have informational annotation. #2669: the reason is markdown-
|
|
872
|
+
// escaped, so the underscores in "out_of_credits" render as "out\_of\_credits".
|
|
873
|
+
expect(out).toContain('overage off (out\\_of\\_credits) โ serving from quota');
|
|
873
874
|
// Must NOT have old blocked framing
|
|
874
875
|
expect(out).not.toContain('billing disabled');
|
|
875
876
|
expect(out).not.toContain("won't recover until billing is fixed");
|
|
@@ -913,7 +914,7 @@ describe('#2494 โ renderAuthSnapshotFormat2 row rendering (out_of_credits demo
|
|
|
913
914
|
[snap({ label: 'org@x', isActive: false, quota: quota({ fiveHourUtilizationPct: 85, overageDisabledReason: 'org_level_disabled' }) })],
|
|
914
915
|
{ now: NOW },
|
|
915
916
|
);
|
|
916
|
-
expect(out).toContain('๐ก
|
|
917
|
+
expect(out).toContain('๐ก **THROTTLING**');
|
|
917
918
|
expect(out).not.toContain('overage off');
|
|
918
919
|
});
|
|
919
920
|
|
|
@@ -78,7 +78,7 @@ describe('runFleetAutoFallback', () => {
|
|
|
78
78
|
expect(out.oldLabel).toBe('ken@x');
|
|
79
79
|
expect(out.newLabel).toBe('you@x');
|
|
80
80
|
expect(out.announcement).toContain('5-hour limit on ken@x');
|
|
81
|
-
expect(out.announcement).toContain('Triggered by: agent
|
|
81
|
+
expect(out.announcement).toContain('Triggered by: agent **carrie**');
|
|
82
82
|
expect(out.announcement).toContain('plenty of headroom');
|
|
83
83
|
}
|
|
84
84
|
});
|
|
@@ -248,20 +248,22 @@ import { renderFallbackFailureNotice } from "../auto-fallback-fleet.js";
|
|
|
248
248
|
|
|
249
249
|
describe("renderFallbackFailureNotice", () => {
|
|
250
250
|
it("names the trigger agent, the reason, and the manual recovery verbs", () => {
|
|
251
|
-
const
|
|
252
|
-
expect(
|
|
253
|
-
expect(
|
|
254
|
-
expect(
|
|
255
|
-
expect(
|
|
256
|
-
expect(
|
|
251
|
+
const out = renderFallbackFailureNotice("marko", "auth-broker unreachable (no client).");
|
|
252
|
+
expect(out).toContain("Auto-failover could not run");
|
|
253
|
+
expect(out).toContain("**marko**");
|
|
254
|
+
expect(out).toContain("auth-broker unreachable");
|
|
255
|
+
expect(out).toContain("/auth use");
|
|
256
|
+
expect(out).toContain("`/auth`");
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
-
it("
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
expect(
|
|
264
|
-
expect(
|
|
259
|
+
it("passes < > & literally in the error reason (markdown, #2669)", () => {
|
|
260
|
+
// Broker errors can contain angle brackets โ they are literal in rich
|
|
261
|
+
// markdown and cannot inject formatting; emphasis specials are escaped.
|
|
262
|
+
const out = renderFallbackFailureNotice("a<b", 'request <probe-quota> failed & a_b');
|
|
263
|
+
expect(out).toContain("a<b");
|
|
264
|
+
expect(out).toContain("<probe-quota>");
|
|
265
|
+
expect(out).toContain("&");
|
|
266
|
+
expect(out).toContain("a\\_b");
|
|
265
267
|
});
|
|
266
268
|
});
|
|
267
269
|
|
|
@@ -32,6 +32,16 @@ const NOW = 1_700_000_000_000
|
|
|
32
32
|
const VERSION = 'v0.8.0+106'
|
|
33
33
|
const AGENT = 'test-harness'
|
|
34
34
|
|
|
35
|
+
// The crash row's tail-logs command is runtime-aware: in-container agents
|
|
36
|
+
// (SWITCHROOM_RUNTIME=docker) get `docker logs โฆ`, everything else gets
|
|
37
|
+
// `journalctl โฆ`. The bun-native test set inherits the live container env,
|
|
38
|
+
// while vitest scrubs it โ so assert against whichever branch the renderer
|
|
39
|
+
// actually selected rather than pinning one shape (#2669).
|
|
40
|
+
const TAIL_CMD = (slug: string): string =>
|
|
41
|
+
process.env.SWITCHROOM_RUNTIME === 'docker'
|
|
42
|
+
? `\`docker logs --tail 100 switchroom-${slug}\``
|
|
43
|
+
: `\`journalctl --user -u switchroom-${slug} -n 100\``
|
|
44
|
+
|
|
35
45
|
function rec(offsetMs: number) {
|
|
36
46
|
return { ts: NOW - offsetMs }
|
|
37
47
|
}
|
|
@@ -57,7 +67,7 @@ describe('boot-card: reason โ user-visible render (CC-8)', () => {
|
|
|
57
67
|
version: VERSION,
|
|
58
68
|
restartReason: reason,
|
|
59
69
|
})
|
|
60
|
-
expect(card).toMatchInlineSnapshot(`"โ
|
|
70
|
+
expect(card).toMatchInlineSnapshot(`"โ
**test-harness** back up ยท v0.8.0+106"`)
|
|
61
71
|
// Negative assertions on the failure surface CC-8 worries about:
|
|
62
72
|
expect(card).not.toContain('crash recovery')
|
|
63
73
|
expect(card).not.toContain('journalctl')
|
|
@@ -83,10 +93,10 @@ describe('boot-card: reason โ user-visible render (CC-8)', () => {
|
|
|
83
93
|
restartReason: reason,
|
|
84
94
|
restartAgeMs: 3_400,
|
|
85
95
|
})
|
|
86
|
-
expect(card).toContain('โ ๏ธ
|
|
87
|
-
expect(card).toContain('โ ๏ธ
|
|
96
|
+
expect(card).toContain('โ ๏ธ **test-harness** back up')
|
|
97
|
+
expect(card).toContain('โ ๏ธ **Restart** crash recovery ยท 3.4s ago')
|
|
88
98
|
expect(card).toContain('Tail logs:')
|
|
89
|
-
expect(card).toContain('
|
|
99
|
+
expect(card).toContain(TAIL_CMD('test-harness'))
|
|
90
100
|
})
|
|
91
101
|
|
|
92
102
|
// โโโ canonical crash: no marker at all โโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -104,7 +114,7 @@ describe('boot-card: reason โ user-visible render (CC-8)', () => {
|
|
|
104
114
|
restartReason: reason,
|
|
105
115
|
restartAgeMs: 12_000,
|
|
106
116
|
})
|
|
107
|
-
expect(card).toContain('โ ๏ธ
|
|
117
|
+
expect(card).toContain('โ ๏ธ **test-harness** back up')
|
|
108
118
|
expect(card).toContain('crash recovery ยท 12.0s ago')
|
|
109
119
|
expect(card).toContain('Tail logs:')
|
|
110
120
|
})
|
|
@@ -143,7 +153,7 @@ describe('boot-card: reason โ user-visible render (CC-8)', () => {
|
|
|
143
153
|
version: VERSION,
|
|
144
154
|
restartReason: reason,
|
|
145
155
|
})
|
|
146
|
-
expect(card).toMatchInlineSnapshot(`"โ
|
|
156
|
+
expect(card).toMatchInlineSnapshot(`"โ
**test-harness** back up ยท v0.8.0+106"`)
|
|
147
157
|
expect(card).not.toContain('crash recovery')
|
|
148
158
|
})
|
|
149
159
|
|
|
@@ -161,15 +171,17 @@ describe('boot-card: reason โ user-visible render (CC-8)', () => {
|
|
|
161
171
|
version: VERSION,
|
|
162
172
|
restartReason: reason,
|
|
163
173
|
})
|
|
164
|
-
expect(card).toMatchInlineSnapshot(`"๐
|
|
174
|
+
expect(card).toMatchInlineSnapshot(`"๐ **test-harness** back up ยท v0.8.0+106"`)
|
|
165
175
|
expect(card).not.toContain('crash recovery')
|
|
166
176
|
})
|
|
167
177
|
|
|
168
|
-
// โโโ slug override path: agentSlug used in
|
|
169
|
-
it('crash card uses agentSlug (not agentName) in the
|
|
170
|
-
// The
|
|
178
|
+
// โโโ slug override path: agentSlug used in tail-logs cmd, not agentName โโโ
|
|
179
|
+
it('crash card uses agentSlug (not agentName) in the tail-logs command', () => {
|
|
180
|
+
// The tail-logs row is the user's actionable next step โ if the
|
|
171
181
|
// slug ever drifts (capitalization, special chars), the copy-paste
|
|
172
|
-
// command stops working. Pin the slug pathway explicitly.
|
|
182
|
+
// command stops working. Pin the slug pathway explicitly. The command
|
|
183
|
+
// shape (docker vs journalctl) is runtime-determined; the slug must be
|
|
184
|
+
// the lowercase systemd slug regardless.
|
|
173
185
|
const card = renderBootCard({
|
|
174
186
|
agentName: 'Test Harness Display',
|
|
175
187
|
agentSlug: 'test-harness',
|
|
@@ -177,6 +189,9 @@ describe('boot-card: reason โ user-visible render (CC-8)', () => {
|
|
|
177
189
|
restartReason: 'crash',
|
|
178
190
|
restartAgeMs: 800,
|
|
179
191
|
})
|
|
180
|
-
expect(card).toContain('
|
|
192
|
+
expect(card).toContain(TAIL_CMD('test-harness'))
|
|
193
|
+
// And never the display name in the unit target.
|
|
194
|
+
expect(card).not.toContain('Test Harness Display -n')
|
|
195
|
+
expect(card).not.toContain('switchroom-Test Harness Display')
|
|
181
196
|
})
|
|
182
197
|
})
|