switchroom 0.16.46 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +83 -81
- package/dist/auth-broker/index.js +104 -88
- package/dist/cli/autoaccept-poll.js +8 -8
- package/dist/cli/drive-write-pretool.mjs +10 -15
- package/dist/cli/notion-write-pretool.mjs +85 -83
- package/dist/cli/skill-validate-pretool.mjs +91 -91
- package/dist/cli/switchroom.js +1720 -1392
- package/dist/cli/ui/index.html +84 -12
- package/dist/host-control/main.js +209 -173
- package/dist/vault/approvals/kernel-server.js +86 -83
- package/dist/vault/broker/server.js +284 -139
- package/package.json +3 -3
- package/profiles/_base/cron-session.sh.hbs +1 -1
- package/profiles/_base/start.sh.hbs +54 -3
- package/skills/switchroom-architecture/telegram.md +8 -15
- package/skills/switchroom-cli/SKILL.md +4 -5
- package/skills/telegram-test-harness/SKILL.md +1 -1
- package/telegram-plugin/README.md +18 -29
- package/telegram-plugin/bridge/bridge.ts +1 -41
- package/telegram-plugin/bridge/tool-filter.ts +3 -4
- package/telegram-plugin/dist/bridge/bridge.js +120 -155
- package/telegram-plugin/dist/gateway/gateway.js +1127 -1029
- package/telegram-plugin/dist/server.js +168 -203
- package/telegram-plugin/gateway/busy-key-reaper.ts +113 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +11 -0
- package/telegram-plugin/gateway/escalation-bridge-gate.ts +46 -0
- package/telegram-plugin/gateway/gate-parity-probe.ts +102 -0
- package/telegram-plugin/gateway/gateway.ts +566 -631
- package/telegram-plugin/gateway/inbound-delivery-confirm.ts +89 -7
- package/telegram-plugin/gateway/inbound-spool.ts +108 -10
- package/telegram-plugin/gateway/model-command.ts +51 -3
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +26 -0
- package/telegram-plugin/gateway/represent-guard.ts +28 -11
- package/telegram-plugin/gateway/status-pin-store.ts +124 -45
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +19 -0
- package/telegram-plugin/history.ts +5 -0
- package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +1 -2
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +9 -1
- package/telegram-plugin/registry/subagents-schema.ts +126 -1
- package/telegram-plugin/registry/turns-schema.ts +65 -1
- package/telegram-plugin/session-tail.ts +26 -4
- package/telegram-plugin/slot-banner-driver.ts +42 -2
- package/telegram-plugin/status-query-telemetry.ts +100 -0
- package/telegram-plugin/stream-reply-handler.ts +15 -16
- package/telegram-plugin/subagent-watcher.ts +182 -30
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +4 -10
- package/telegram-plugin/tests/busy-key-reaper.test.ts +191 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +11 -17
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +5 -26
- package/telegram-plugin/tests/escalation-bridge-gate.test.ts +38 -0
- package/telegram-plugin/tests/gate-parity-probe.test.ts +171 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +13 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +14 -11
- package/telegram-plugin/tests/inbound-delivery-confirm.test.ts +146 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +143 -0
- package/telegram-plugin/tests/model-command.test.ts +54 -1
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +5 -11
- package/telegram-plugin/tests/nested-worker-visibility-harness.test.ts +329 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +53 -0
- package/telegram-plugin/tests/progress-update-redact.test.ts +99 -0
- package/telegram-plugin/tests/registry-turns.test.ts +67 -0
- package/telegram-plugin/tests/represent-guard.test.ts +42 -6
- package/telegram-plugin/tests/resume-inbound-builder.test.ts +1 -0
- package/telegram-plugin/tests/session-tail.test.ts +10 -1
- package/telegram-plugin/tests/slot-banner-boot-recovery.test.ts +246 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +0 -14
- package/telegram-plugin/tests/status-pin-store.test.ts +220 -5
- package/telegram-plugin/tests/status-query-telemetry.test.ts +115 -0
- package/telegram-plugin/tests/subagent-nested-dispatch.test.ts +209 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +37 -0
- package/telegram-plugin/tests/subagent-watcher-boot-promotion-replay.test.ts +167 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +46 -3
- package/telegram-plugin/tests/subagent-watcher-stall-notification.test.ts +70 -0
- package/telegram-plugin/tests/tool-activity-summary.test.ts +16 -0
- package/telegram-plugin/tests/tool-filter.test.ts +1 -3
- package/telegram-plugin/tests/tool-label-pretool.test.ts +1 -4
- package/telegram-plugin/tests/turn-flush-safety.test.ts +222 -1
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +46 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +202 -9
- package/telegram-plugin/tests/worker-feed-dispatch.test.ts +25 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +295 -0
- package/telegram-plugin/tool-activity-summary.ts +19 -0
- package/telegram-plugin/turn-flush-safety.ts +16 -1
- package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +8 -9
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-narration-intent-dm.test.ts +1 -1
- package/telegram-plugin/worker-activity-feed.ts +75 -15
- package/vendor/hindsight-memory/CHANGELOG.md +24 -0
- package/vendor/hindsight-memory/README.md +5 -0
- package/vendor/hindsight-memory/scripts/lib/client.py +31 -1
- package/vendor/hindsight-memory/scripts/lib/config.py +41 -2
- package/vendor/hindsight-memory/scripts/lib/content.py +4 -1
- package/vendor/hindsight-memory/scripts/lib/daemon.py +11 -2
- package/vendor/hindsight-memory/scripts/recall.py +74 -1
- package/vendor/hindsight-memory/scripts/retain.py +8 -1
- package/vendor/hindsight-memory/scripts/tests/test_config_client_casts.py +111 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +85 -1
- package/vendor/hindsight-memory/scripts/tests/test_recall_tag_filters.py +107 -0
- package/vendor/hindsight-memory/settings.json +4 -0
- package/vendor/hindsight-memory/tests/test_client.py +130 -0
- package/vendor/hindsight-memory/tests/test_config.py +47 -0
- package/vendor/hindsight-memory/tests/test_content.py +18 -0
- package/vendor/hindsight-memory/tests/test_hooks.py +62 -0
- package/telegram-plugin/gateway/error-envelope-card.ts +0 -64
- package/telegram-plugin/gateway/resolve-calling-subagent.ts +0 -78
- package/telegram-plugin/silent-reply.ts +0 -58
- package/telegram-plugin/tests/error-envelope-unlock-card.test.ts +0 -79
- package/telegram-plugin/tests/resolve-calling-subagent.test.ts +0 -269
- package/telegram-plugin/tests/silent-reply-guard.test.ts +0 -122
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for resolveCallingSubagent (issue #305 Option A).
|
|
3
|
-
*
|
|
4
|
-
* The resolver picks which sub-agent's row body should host the
|
|
5
|
-
* progress_update narrative when called from a sub-agent context.
|
|
6
|
-
* Resolution priority:
|
|
7
|
-
* 1. agentIdHint → exact match on subagents.jsonl_agent_id
|
|
8
|
-
* 2. toolUseIdHint → exact match on subagents.id
|
|
9
|
-
* 3. Heuristic → most-recently-started running sub-agent in the active turn
|
|
10
|
-
*
|
|
11
|
-
* Tests run against an in-memory bun:sqlite DB with the same schema that
|
|
12
|
-
* production uses (turns + subagents tables). Run via:
|
|
13
|
-
* bun test telegram-plugin/tests/resolve-calling-subagent.test.ts
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
|
|
17
|
-
import {
|
|
18
|
-
openSubagentsDbInMemory,
|
|
19
|
-
recordSubagentStart,
|
|
20
|
-
} from '../registry/subagents-schema.js'
|
|
21
|
-
import { resolveCallingSubagent } from '../gateway/resolve-calling-subagent.js'
|
|
22
|
-
|
|
23
|
-
type Db = ReturnType<typeof openSubagentsDbInMemory>
|
|
24
|
-
|
|
25
|
-
function insertOpenTurn(db: Db, turnKey: string, chatId: string, startedAt: number): void {
|
|
26
|
-
db.prepare(`
|
|
27
|
-
INSERT INTO turns
|
|
28
|
-
(turn_key, chat_id, thread_id, started_at, last_user_msg_id, created_at, updated_at)
|
|
29
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
30
|
-
`).run(turnKey, chatId, null, startedAt, 'msg-1', startedAt, startedAt)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('resolveCallingSubagent', () => {
|
|
34
|
-
let db: Db
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
db = openSubagentsDbInMemory()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
afterEach(() => {
|
|
41
|
-
db.close()
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('returns null when db is null', () => {
|
|
45
|
-
const result = resolveCallingSubagent({
|
|
46
|
-
db: null,
|
|
47
|
-
chatId: 'c1',
|
|
48
|
-
agentIdHint: 'jsonl-1',
|
|
49
|
-
toolUseIdHint: null,
|
|
50
|
-
})
|
|
51
|
-
expect(result).toBeNull()
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('matches by agentIdHint (jsonl_agent_id)', () => {
|
|
55
|
-
insertOpenTurn(db, 'c1:1', 'c1', 1000)
|
|
56
|
-
recordSubagentStart(db, {
|
|
57
|
-
id: 'toolu_alpha',
|
|
58
|
-
parentTurnKey: 'c1:1',
|
|
59
|
-
jsonlAgentId: 'jsonl-alpha',
|
|
60
|
-
background: false,
|
|
61
|
-
startedAt: 1100,
|
|
62
|
-
})
|
|
63
|
-
recordSubagentStart(db, {
|
|
64
|
-
id: 'toolu_beta',
|
|
65
|
-
parentTurnKey: 'c1:1',
|
|
66
|
-
jsonlAgentId: 'jsonl-beta',
|
|
67
|
-
background: false,
|
|
68
|
-
startedAt: 1200,
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
const result = resolveCallingSubagent({
|
|
72
|
-
db,
|
|
73
|
-
chatId: 'c1',
|
|
74
|
-
agentIdHint: 'jsonl-alpha',
|
|
75
|
-
toolUseIdHint: null,
|
|
76
|
-
})
|
|
77
|
-
expect(result).toEqual({ agentId: 'jsonl-alpha' })
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('agentIdHint miss on completed sub-agent does NOT match (only running rows)', () => {
|
|
81
|
-
insertOpenTurn(db, 'c1:1', 'c1', 1000)
|
|
82
|
-
recordSubagentStart(db, {
|
|
83
|
-
id: 'toolu_done',
|
|
84
|
-
parentTurnKey: 'c1:1',
|
|
85
|
-
jsonlAgentId: 'jsonl-done',
|
|
86
|
-
background: false,
|
|
87
|
-
startedAt: 1100,
|
|
88
|
-
})
|
|
89
|
-
// Mark as completed via direct SQL.
|
|
90
|
-
db.prepare("UPDATE subagents SET status = 'completed', ended_at = 2000 WHERE id = ?")
|
|
91
|
-
.run('toolu_done')
|
|
92
|
-
|
|
93
|
-
const result = resolveCallingSubagent({
|
|
94
|
-
db,
|
|
95
|
-
chatId: 'c1',
|
|
96
|
-
agentIdHint: 'jsonl-done',
|
|
97
|
-
toolUseIdHint: null,
|
|
98
|
-
})
|
|
99
|
-
expect(result).toBeNull()
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('matches by toolUseIdHint (subagents.id) when agentIdHint absent', () => {
|
|
103
|
-
insertOpenTurn(db, 'c1:1', 'c1', 1000)
|
|
104
|
-
recordSubagentStart(db, {
|
|
105
|
-
id: 'toolu_via_id',
|
|
106
|
-
parentTurnKey: 'c1:1',
|
|
107
|
-
jsonlAgentId: 'jsonl-via-id',
|
|
108
|
-
background: false,
|
|
109
|
-
startedAt: 1100,
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
const result = resolveCallingSubagent({
|
|
113
|
-
db,
|
|
114
|
-
chatId: 'c1',
|
|
115
|
-
agentIdHint: null,
|
|
116
|
-
toolUseIdHint: 'toolu_via_id',
|
|
117
|
-
})
|
|
118
|
-
expect(result).toEqual({ agentId: 'jsonl-via-id' })
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('heuristic fallback: returns most-recently-started running sub-agent in active turn', () => {
|
|
122
|
-
insertOpenTurn(db, 'c1:1', 'c1', 1000)
|
|
123
|
-
recordSubagentStart(db, {
|
|
124
|
-
id: 'toolu_old',
|
|
125
|
-
parentTurnKey: 'c1:1',
|
|
126
|
-
jsonlAgentId: 'jsonl-old',
|
|
127
|
-
background: false,
|
|
128
|
-
startedAt: 1100,
|
|
129
|
-
})
|
|
130
|
-
recordSubagentStart(db, {
|
|
131
|
-
id: 'toolu_new',
|
|
132
|
-
parentTurnKey: 'c1:1',
|
|
133
|
-
jsonlAgentId: 'jsonl-new',
|
|
134
|
-
background: false,
|
|
135
|
-
startedAt: 1500, // strictly newer
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
const result = resolveCallingSubagent({
|
|
139
|
-
db,
|
|
140
|
-
chatId: 'c1',
|
|
141
|
-
agentIdHint: null,
|
|
142
|
-
toolUseIdHint: null,
|
|
143
|
-
})
|
|
144
|
-
expect(result).toEqual({ agentId: 'jsonl-new' })
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('heuristic skips sub-agents without jsonl_agent_id', () => {
|
|
148
|
-
insertOpenTurn(db, 'c1:1', 'c1', 1000)
|
|
149
|
-
recordSubagentStart(db, {
|
|
150
|
-
id: 'toolu_nojson',
|
|
151
|
-
parentTurnKey: 'c1:1',
|
|
152
|
-
jsonlAgentId: null,
|
|
153
|
-
background: false,
|
|
154
|
-
startedAt: 1500,
|
|
155
|
-
})
|
|
156
|
-
recordSubagentStart(db, {
|
|
157
|
-
id: 'toolu_withjson',
|
|
158
|
-
parentTurnKey: 'c1:1',
|
|
159
|
-
jsonlAgentId: 'jsonl-resolved',
|
|
160
|
-
background: false,
|
|
161
|
-
startedAt: 1100,
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
const result = resolveCallingSubagent({
|
|
165
|
-
db,
|
|
166
|
-
chatId: 'c1',
|
|
167
|
-
agentIdHint: null,
|
|
168
|
-
toolUseIdHint: null,
|
|
169
|
-
})
|
|
170
|
-
expect(result).toEqual({ agentId: 'jsonl-resolved' })
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('heuristic returns null when no active turn exists', () => {
|
|
174
|
-
// Insert an ENDED turn.
|
|
175
|
-
db.prepare(`
|
|
176
|
-
INSERT INTO turns
|
|
177
|
-
(turn_key, chat_id, thread_id, started_at, ended_at, last_user_msg_id, created_at, updated_at)
|
|
178
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
179
|
-
`).run('c1:1', 'c1', null, 1000, 2000, 'msg-1', 1000, 2000)
|
|
180
|
-
recordSubagentStart(db, {
|
|
181
|
-
id: 'toolu_orphan',
|
|
182
|
-
parentTurnKey: 'c1:1',
|
|
183
|
-
jsonlAgentId: 'jsonl-orphan',
|
|
184
|
-
background: false,
|
|
185
|
-
startedAt: 1100,
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
const result = resolveCallingSubagent({
|
|
189
|
-
db,
|
|
190
|
-
chatId: 'c1',
|
|
191
|
-
agentIdHint: null,
|
|
192
|
-
toolUseIdHint: null,
|
|
193
|
-
})
|
|
194
|
-
expect(result).toBeNull()
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('heuristic returns null when no running sub-agents in active turn', () => {
|
|
198
|
-
insertOpenTurn(db, 'c1:1', 'c1', 1000)
|
|
199
|
-
// No sub-agents inserted.
|
|
200
|
-
const result = resolveCallingSubagent({
|
|
201
|
-
db,
|
|
202
|
-
chatId: 'c1',
|
|
203
|
-
agentIdHint: null,
|
|
204
|
-
toolUseIdHint: null,
|
|
205
|
-
})
|
|
206
|
-
expect(result).toBeNull()
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('heuristic ignores sub-agents from a different chat', () => {
|
|
210
|
-
insertOpenTurn(db, 'c1:1', 'c1', 1000)
|
|
211
|
-
insertOpenTurn(db, 'c2:1', 'c2', 1000)
|
|
212
|
-
recordSubagentStart(db, {
|
|
213
|
-
id: 'toolu_other',
|
|
214
|
-
parentTurnKey: 'c2:1',
|
|
215
|
-
jsonlAgentId: 'jsonl-other-chat',
|
|
216
|
-
background: false,
|
|
217
|
-
startedAt: 1100,
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
const result = resolveCallingSubagent({
|
|
221
|
-
db,
|
|
222
|
-
chatId: 'c1',
|
|
223
|
-
agentIdHint: null,
|
|
224
|
-
toolUseIdHint: null,
|
|
225
|
-
})
|
|
226
|
-
expect(result).toBeNull()
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('agentIdHint takes priority over toolUseIdHint and heuristic', () => {
|
|
230
|
-
insertOpenTurn(db, 'c1:1', 'c1', 1000)
|
|
231
|
-
recordSubagentStart(db, {
|
|
232
|
-
id: 'toolu_priority',
|
|
233
|
-
parentTurnKey: 'c1:1',
|
|
234
|
-
jsonlAgentId: 'jsonl-priority',
|
|
235
|
-
background: false,
|
|
236
|
-
startedAt: 1100,
|
|
237
|
-
})
|
|
238
|
-
recordSubagentStart(db, {
|
|
239
|
-
id: 'toolu_other',
|
|
240
|
-
parentTurnKey: 'c1:1',
|
|
241
|
-
jsonlAgentId: 'jsonl-other',
|
|
242
|
-
background: false,
|
|
243
|
-
startedAt: 1500, // newer; would win heuristic
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
const result = resolveCallingSubagent({
|
|
247
|
-
db,
|
|
248
|
-
chatId: 'c1',
|
|
249
|
-
agentIdHint: 'jsonl-priority',
|
|
250
|
-
toolUseIdHint: 'toolu_other',
|
|
251
|
-
})
|
|
252
|
-
expect(result).toEqual({ agentId: 'jsonl-priority' })
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
it('returns null on SQL error (broken db.prepare)', () => {
|
|
256
|
-
const brokenDb = {
|
|
257
|
-
prepare: (): never => {
|
|
258
|
-
throw new Error('boom')
|
|
259
|
-
},
|
|
260
|
-
}
|
|
261
|
-
const result = resolveCallingSubagent({
|
|
262
|
-
db: brokenDb as unknown as Parameters<typeof resolveCallingSubagent>[0]['db'],
|
|
263
|
-
chatId: 'c1',
|
|
264
|
-
agentIdHint: 'whatever',
|
|
265
|
-
toolUseIdHint: null,
|
|
266
|
-
})
|
|
267
|
-
expect(result).toBeNull()
|
|
268
|
-
})
|
|
269
|
-
})
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
// Import directly from the helper module (not server.js), so this test
|
|
4
|
-
// doesn't boot the full MCP server — server.ts has top-level side
|
|
5
|
-
// effects (env load, TELEGRAM_BOT_TOKEN check, session-tail spawn) that
|
|
6
|
-
// kill the test process when run under bun-test without a real env.
|
|
7
|
-
import { guardSilentReply, isSilentReplyMarker } from '../silent-reply.js'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Regression coverage for sprint1 review finding #6: the reply /
|
|
11
|
-
* stream_reply tool handlers must call `assertAllowedChat(chat_id)`
|
|
12
|
-
* BEFORE returning the silent-reply ack, so an unauthorised chat
|
|
13
|
-
* cannot bypass the outbound allowlist by having the agent emit
|
|
14
|
-
* `NO_REPLY` / `HEARTBEAT_OK`.
|
|
15
|
-
*
|
|
16
|
-
* We exercise the extracted `guardSilentReply` helper directly rather
|
|
17
|
-
* than the full MCP tool handler; the handler scaffolding (IPC + Bot
|
|
18
|
-
* harness + history DB) is heavy, and the ordering contract — "assert
|
|
19
|
-
* first, then ack" — lives entirely inside the helper.
|
|
20
|
-
*/
|
|
21
|
-
describe('guardSilentReply — allowlist ordering', () => {
|
|
22
|
-
it('asserts the allowlist BEFORE returning a silent ack (NO_REPLY)', () => {
|
|
23
|
-
const assertAllowed = vi.fn()
|
|
24
|
-
const result = guardSilentReply({
|
|
25
|
-
chat_id: '123',
|
|
26
|
-
text: 'NO_REPLY',
|
|
27
|
-
hasFiles: false,
|
|
28
|
-
assertAllowed,
|
|
29
|
-
})
|
|
30
|
-
expect(assertAllowed).toHaveBeenCalledOnce()
|
|
31
|
-
expect(assertAllowed).toHaveBeenCalledWith('123')
|
|
32
|
-
expect(result).toEqual({ kind: 'silent', markerText: 'NO_REPLY' })
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('recognises HEARTBEAT_OK as a silent marker and still gates on allowlist', () => {
|
|
36
|
-
const assertAllowed = vi.fn()
|
|
37
|
-
const result = guardSilentReply({
|
|
38
|
-
chat_id: '456',
|
|
39
|
-
text: ' heartbeat_ok ',
|
|
40
|
-
hasFiles: false,
|
|
41
|
-
assertAllowed,
|
|
42
|
-
})
|
|
43
|
-
expect(assertAllowed).toHaveBeenCalledWith('456')
|
|
44
|
-
expect(result).toEqual({ kind: 'silent', markerText: 'heartbeat_ok' })
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('throws (does NOT return a silent ack) when the chat is disallowed', () => {
|
|
48
|
-
// This is the exact bypass the fix prevents: an unauthorised chat_id
|
|
49
|
-
// using NO_REPLY must NOT receive a successful silent-reply ack.
|
|
50
|
-
const assertAllowed = vi.fn((chat_id: string) => {
|
|
51
|
-
throw new Error(`chat ${chat_id} is not allowlisted`)
|
|
52
|
-
})
|
|
53
|
-
expect(() =>
|
|
54
|
-
guardSilentReply({
|
|
55
|
-
chat_id: '999',
|
|
56
|
-
text: 'NO_REPLY',
|
|
57
|
-
hasFiles: false,
|
|
58
|
-
assertAllowed,
|
|
59
|
-
}),
|
|
60
|
-
).toThrow(/not allowlisted/)
|
|
61
|
-
expect(assertAllowed).toHaveBeenCalledOnce()
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('does not short-circuit (or call assertAllowed) when files are attached', () => {
|
|
65
|
-
// Silent-reply semantics are text-only; a NO_REPLY payload with files
|
|
66
|
-
// is a real send and must flow through the normal allowlist path
|
|
67
|
-
// inside the send codepath, not the silent short-circuit.
|
|
68
|
-
const assertAllowed = vi.fn()
|
|
69
|
-
const result = guardSilentReply({
|
|
70
|
-
chat_id: '123',
|
|
71
|
-
text: 'NO_REPLY',
|
|
72
|
-
hasFiles: true,
|
|
73
|
-
assertAllowed,
|
|
74
|
-
})
|
|
75
|
-
expect(assertAllowed).not.toHaveBeenCalled()
|
|
76
|
-
expect(result).toEqual({ kind: 'continue' })
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('does not short-circuit for normal prose that merely mentions the marker', () => {
|
|
80
|
-
const assertAllowed = vi.fn()
|
|
81
|
-
const result = guardSilentReply({
|
|
82
|
-
chat_id: '123',
|
|
83
|
-
text: 'the agent suggested NO_REPLY earlier',
|
|
84
|
-
hasFiles: false,
|
|
85
|
-
assertAllowed,
|
|
86
|
-
})
|
|
87
|
-
expect(assertAllowed).not.toHaveBeenCalled()
|
|
88
|
-
expect(result).toEqual({ kind: 'continue' })
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('does not short-circuit on undefined/empty text', () => {
|
|
92
|
-
const assertAllowed = vi.fn()
|
|
93
|
-
expect(
|
|
94
|
-
guardSilentReply({
|
|
95
|
-
chat_id: '123',
|
|
96
|
-
text: undefined,
|
|
97
|
-
hasFiles: false,
|
|
98
|
-
assertAllowed,
|
|
99
|
-
}),
|
|
100
|
-
).toEqual({ kind: 'continue' })
|
|
101
|
-
expect(
|
|
102
|
-
guardSilentReply({
|
|
103
|
-
chat_id: '123',
|
|
104
|
-
text: '',
|
|
105
|
-
hasFiles: false,
|
|
106
|
-
assertAllowed,
|
|
107
|
-
}),
|
|
108
|
-
).toEqual({ kind: 'continue' })
|
|
109
|
-
expect(assertAllowed).not.toHaveBeenCalled()
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
// Sanity: the helper the guard delegates to is exported for direct
|
|
113
|
-
// marker-recognition testing. Keep a thin smoke assertion so future
|
|
114
|
-
// marker-set tweaks break here rather than at the ordering layer.
|
|
115
|
-
it('isSilentReplyMarker recognises the documented marker set', () => {
|
|
116
|
-
expect(isSilentReplyMarker('NO_REPLY')).toBe(true)
|
|
117
|
-
expect(isSilentReplyMarker('HEARTBEAT_OK')).toBe(true)
|
|
118
|
-
expect(isSilentReplyMarker('no_reply')).toBe(true)
|
|
119
|
-
expect(isSilentReplyMarker('hello')).toBe(false)
|
|
120
|
-
expect(isSilentReplyMarker(undefined)).toBe(false)
|
|
121
|
-
})
|
|
122
|
-
})
|