switchroom 0.18.31 → 0.18.33
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 +4 -2
- package/dist/auth-broker/index.js +21 -3
- package/dist/cli/notion-write-pretool.mjs +4 -2
- package/dist/cli/switchroom.js +1410 -852
- package/dist/host-control/main.js +22 -4
- package/dist/vault/approvals/kernel-server.js +21 -3
- package/dist/vault/broker/server.js +48 -4
- package/package.json +4 -3
- package/profiles/_base/start.sh.hbs +148 -23
- package/telegram-plugin/dist/gateway/gateway.js +62726 -58282
- package/telegram-plugin/gateway/agent-button-callback-handler.ts +237 -0
- package/telegram-plugin/gateway/ask-callback-handler.ts +92 -0
- package/telegram-plugin/gateway/attachment-message-handlers.ts +152 -0
- package/telegram-plugin/gateway/backstop-delivery.ts +223 -23
- package/telegram-plugin/gateway/boot-card.ts +169 -1
- package/telegram-plugin/gateway/bot-commands-model-effort.ts +209 -0
- package/telegram-plugin/gateway/bot-commands-start-info.ts +108 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +124 -0
- package/telegram-plugin/gateway/captured-answer-resume.ts +259 -0
- package/telegram-plugin/gateway/card-approval-keyboards.test.ts +28 -0
- package/telegram-plugin/gateway/card-tool-handlers.ts +639 -0
- package/telegram-plugin/gateway/checklist-message-handler.ts +107 -0
- package/telegram-plugin/gateway/delivery-confirm-wiring.ts +133 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +6 -44
- package/telegram-plugin/gateway/gateway-import-clean.test.ts +188 -0
- package/telegram-plugin/gateway/gateway.ts +6403 -13456
- package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +7 -15
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +35 -68
- package/telegram-plugin/gateway/inbound-interceptors.ts +1133 -0
- package/telegram-plugin/gateway/inbound-router.ts +400 -0
- package/telegram-plugin/gateway/liveness-wiring.ts +440 -0
- package/telegram-plugin/gateway/media-message-handlers.ts +256 -0
- package/telegram-plugin/gateway/mental-model-propose-card.ts +16 -0
- package/telegram-plugin/gateway/model-command.ts +23 -0
- package/telegram-plugin/gateway/narrative-lane.ts +865 -0
- package/telegram-plugin/gateway/obligation-ledger.ts +42 -0
- package/telegram-plugin/gateway/obligation-store.ts +37 -1
- package/telegram-plugin/gateway/obligation-wiring.ts +333 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +2012 -0
- package/telegram-plugin/gateway/photo-message-handler.ts +80 -0
- package/telegram-plugin/gateway/pinned-message-handler.ts +86 -0
- package/telegram-plugin/gateway/secret-request-card.test.ts +46 -0
- package/telegram-plugin/gateway/secret-request-card.ts +45 -0
- package/telegram-plugin/gateway/stream-render.ts +2166 -0
- package/telegram-plugin/gateway/turn-end.ts +606 -0
- package/telegram-plugin/gateway/turn-start-surfaces.ts +298 -0
- package/telegram-plugin/gateway/vault-request-access-card.ts +16 -0
- package/telegram-plugin/gateway/vault-request-save-card.test.ts +49 -0
- package/telegram-plugin/gateway/vault-request-save-card.ts +52 -0
- package/telegram-plugin/gateway/voice-message-handler.ts +123 -0
- package/telegram-plugin/gateway/voice-ondemand-callback-handler.ts +204 -0
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +40 -0
- package/telegram-plugin/narrative-dedup.ts +24 -1
- package/telegram-plugin/narrative-flush.ts +2 -2
- package/telegram-plugin/pending-user-notice.ts +59 -13
- package/telegram-plugin/render/render.ts +25 -1
- package/telegram-plugin/status-no-truncate.ts +13 -0
- package/telegram-plugin/subagent-watcher.ts +297 -31
- package/telegram-plugin/tests/activity-card-wiring.test.ts +8 -3
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +18 -3
- package/telegram-plugin/tests/agent-button-callback-handler.test.ts +149 -0
- package/telegram-plugin/tests/ask-callback-handler.test.ts +118 -0
- package/telegram-plugin/tests/attachment-message-handlers.test.ts +135 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +167 -0
- package/telegram-plugin/tests/backstop-readback-probe.test.ts +144 -0
- package/telegram-plugin/tests/boot-card-routing.test.ts +139 -0
- package/telegram-plugin/tests/bot-commands-model-effort.test.ts +189 -0
- package/telegram-plugin/tests/bot-commands-start-info.test.ts +240 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +28 -9
- package/telegram-plugin/tests/busy-ack-wiring.test.ts +6 -1
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +21 -12
- package/telegram-plugin/tests/callback-query-handlers.test.ts +101 -0
- package/telegram-plugin/tests/captured-answer-resume.test.ts +358 -0
- package/telegram-plugin/tests/card-tool-handlers.test.ts +497 -0
- package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +5 -2
- package/telegram-plugin/tests/checklist-message-handler.test.ts +160 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +76 -29
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +4 -1
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +45 -16
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +30 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +270 -0
- package/telegram-plugin/tests/gateway-boot-smoke.test.ts +150 -0
- package/telegram-plugin/tests/gateway-bot-construction-deferral.test.ts +251 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -128
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +299 -0
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +44 -29
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +18 -5
- package/telegram-plugin/tests/gateway-request-secret.test.ts +7 -3
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +20 -10
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +8 -2
- package/telegram-plugin/tests/inbound-delivery-cutover-flip.test.ts +54 -150
- package/telegram-plugin/tests/inbound-delivery-cutover-gate.test.ts +10 -14
- package/telegram-plugin/tests/inbound-delivery-dispatch-equivalence.test.ts +6 -7
- package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +0 -16
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +18 -7
- package/telegram-plugin/tests/inbound-message-types.test.ts +52 -16
- package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +69 -14
- package/telegram-plugin/tests/media-message-handlers.test.ts +276 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -4
- package/telegram-plugin/tests/model-command.test.ts +30 -0
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +36 -12
- package/telegram-plugin/tests/narrative-dedup.test.ts +32 -0
- package/telegram-plugin/tests/narrative-flush.test.ts +6 -2
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +458 -0
- package/telegram-plugin/tests/no-reply-bounded-drain.test.ts +14 -3
- package/telegram-plugin/tests/obligation-ledger.test.ts +40 -0
- package/telegram-plugin/tests/obligation-store.test.ts +43 -0
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +18 -8
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +32 -8
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +9 -6
- package/telegram-plugin/tests/photo-message-handler.test.ts +114 -0
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +5 -2
- package/telegram-plugin/tests/pinned-message-handler.test.ts +108 -0
- package/telegram-plugin/tests/render/render.test.ts +42 -0
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -2
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +8 -4
- package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +38 -28
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +31 -20
- package/telegram-plugin/tests/send-reply-golden.test.ts +571 -0
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +22 -8
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +42 -49
- package/telegram-plugin/tests/stop-command.test.ts +22 -12
- package/telegram-plugin/tests/stream-render-golden.test.ts +424 -0
- package/telegram-plugin/tests/subagent-watcher-boot-skip-dead.test.ts +218 -0
- package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +305 -0
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +32 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +35 -3
- package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +8 -12
- package/telegram-plugin/tests/turn-flush-safety.test.ts +191 -11
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +117 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +8 -2
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +2 -2
- package/telegram-plugin/tests/vault-grant-union.test.ts +4 -1
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +16 -5
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +10 -5
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +4 -1
- package/telegram-plugin/tests/vault-subcommands.test.ts +6 -1
- package/telegram-plugin/tests/voice-message-handler.test.ts +111 -0
- package/telegram-plugin/tests/voice-ondemand-callback-handler.test.ts +140 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +86 -19
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +236 -20
- package/telegram-plugin/tests/worker-feed-resume-guard.test.ts +86 -0
- package/telegram-plugin/tool-activity-summary.ts +110 -38
- package/telegram-plugin/turn-flush-safety.ts +80 -14
- package/telegram-plugin/uat/restart-capability.ts +76 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +14 -4
- package/telegram-plugin/uat/scenarios/bridge-flap-resilience-dm.test.ts +11 -1
- package/telegram-plugin/uat/scenarios/cross-turn-pending-progress-dm.test.ts +19 -2
- package/telegram-plugin/uat/scenarios/jtbd-always-on-after-restart-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-deliberate-restart-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-interrupted-turn-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-multipart-render-dm.test.ts +47 -13
- package/telegram-plugin/worker-activity-feed.ts +34 -4
- package/telegram-plugin/gateway/busy-key-reaper.ts +0 -113
- package/telegram-plugin/gateway/gate-parity-probe.ts +0 -102
- package/telegram-plugin/tests/busy-key-reaper.test.ts +0 -192
- package/telegram-plugin/tests/fixtures/cutover-killswitch-probe.ts +0 -75
- package/telegram-plugin/tests/gate-parity-probe.test.ts +0 -171
- package/telegram-plugin/tests/parallel-turns-deadlock-fix.test.ts +0 -217
|
@@ -26,10 +26,12 @@ import { readFileSync } from 'node:fs'
|
|
|
26
26
|
* rather than live.
|
|
27
27
|
*/
|
|
28
28
|
describe('gateway secret-detect intercept — structural wiring', () => {
|
|
29
|
-
const src =
|
|
30
|
-
new URL('../gateway/gateway.ts', import.meta.url),
|
|
31
|
-
'
|
|
32
|
-
|
|
29
|
+
const src =
|
|
30
|
+
readFileSync(new URL('../gateway/gateway.ts', import.meta.url), 'utf8') +
|
|
31
|
+
'\n' +
|
|
32
|
+
// P7 PR-8 (#2996): the vault pending-op intercept (incl. the deferred-
|
|
33
|
+
// secret passphrase branch) moved into gateway/inbound-interceptors.ts.
|
|
34
|
+
readFileSync(new URL('../gateway/inbound-interceptors.ts', import.meta.url), 'utf8')
|
|
33
35
|
|
|
34
36
|
it('imports the pipeline + staging modules', () => {
|
|
35
37
|
expect(src).toMatch(/from '\.\.\/secret-detect\/pipeline\.js'/)
|
|
@@ -54,7 +56,9 @@ describe('gateway secret-detect intercept — structural wiring', () => {
|
|
|
54
56
|
})
|
|
55
57
|
|
|
56
58
|
it('runPipeline call sits BEFORE recordInbound() and ipcServer.broadcast(inboundMsg)', () => {
|
|
57
|
-
|
|
59
|
+
// P7 PR-9: runPipeline lives in interceptSecretDetectPipeline; the
|
|
60
|
+
// gateway CALL SITE is the ordering anchor against the sinks.
|
|
61
|
+
const pipelineIdx = src.indexOf('await interceptSecretDetectPipeline(')
|
|
58
62
|
const recordIdx = src.indexOf('recordInbound(', pipelineIdx)
|
|
59
63
|
const broadcastIdx = src.indexOf('ipcServer.broadcast(inboundMsg)', pipelineIdx)
|
|
60
64
|
expect(pipelineIdx).toBeGreaterThan(0)
|
|
@@ -74,7 +78,7 @@ describe('gateway secret-detect intercept — structural wiring', () => {
|
|
|
74
78
|
// swallowed. See
|
|
75
79
|
// tests/secret-detect-delete-must-surface-failures.test.ts for
|
|
76
80
|
// the full contract pin.
|
|
77
|
-
expect(tail).toMatch(/deleteSensitiveMessage\(chat_id, msgId, 'detected secret'\)/)
|
|
81
|
+
expect(tail).toMatch(/deleteSensitiveMessage\((?:p\.)?chat_id, (?:p\.)?msgId, 'detected secret'\)/)
|
|
78
82
|
// Tells the user what was captured (masked).
|
|
79
83
|
expect(tail).toMatch(/captured \$\{pipeRes\.stored\.length\} secret/)
|
|
80
84
|
// Surfaces the masked form (s.masked is computed via maskToken in the pipeline).
|
|
@@ -102,7 +106,7 @@ describe('gateway secret-detect intercept — structural wiring', () => {
|
|
|
102
106
|
// migrated to deleteSensitiveMessage so failures surface to the
|
|
103
107
|
// operator via in-chat warning. See
|
|
104
108
|
// tests/secret-detect-delete-must-surface-failures.test.ts.
|
|
105
|
-
expect(tail).toMatch(/deleteSensitiveMessage\(chat_id, msgId, 'detected secret'\)/)
|
|
109
|
+
expect(tail).toMatch(/deleteSensitiveMessage\((?:p\.)?chat_id, (?:p\.)?msgId, 'detected secret'\)/)
|
|
106
110
|
// 4. The new inline keyboard helper is used in lieu of the legacy
|
|
107
111
|
// plain-text "run /vault list" warning.
|
|
108
112
|
expect(tail).toMatch(/buildDeferredSecretKeyboard\(/)
|
|
@@ -134,9 +138,15 @@ describe('gateway secret-detect intercept — structural wiring', () => {
|
|
|
134
138
|
// pre-staging body, not a precise offset — it has grown before (#3020's
|
|
135
139
|
// stop-keyword block pushed it past the old 30k) and may grow again;
|
|
136
140
|
// bump it when new early-return branches land above the staging block.
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
// P7 PR-9: the parser moved to interceptSecretStagingCommand
|
|
142
|
+
// (inbound-interceptors.ts), still invoked from handleInbound's
|
|
143
|
+
// inbound-text path (same allowFrom gating) — anchor on the function.
|
|
144
|
+
const stagingFnIdx = src.indexOf('export async function interceptSecretStagingCommand(')
|
|
145
|
+
expect(stagingFnIdx).toBeGreaterThan(0)
|
|
146
|
+
const tail = src.slice(stagingFnIdx, stagingFnIdx + 6000)
|
|
139
147
|
expect(tail).toMatch(/\(stash\|ignore\|rename\|forget\)/)
|
|
140
|
-
expect(tail).toMatch(/secretStaging\.latestForChat\(chat_id\)/)
|
|
148
|
+
expect(tail).toMatch(/secretStaging\.latestForChat\((?:p\.)?chat_id\)/)
|
|
149
|
+
// And handleInbound must actually delegate to it.
|
|
150
|
+
expect(src).toMatch(/await interceptSecretStagingCommand\(/)
|
|
141
151
|
})
|
|
142
152
|
})
|
|
@@ -122,9 +122,15 @@ describe('gateway: the live callback dispatcher routes every switch tap to the h
|
|
|
122
122
|
})
|
|
123
123
|
|
|
124
124
|
it('the typed dispatcher relays the handler reply directly (no recordTypedModelSwitch)', () => {
|
|
125
|
-
|
|
125
|
+
// #2996 P6: the typed /model handler moved to bot-commands-model-effort.ts
|
|
126
|
+
// (registerModelEffortCommands); scrape the extracted module instead.
|
|
127
|
+
const MODULE_SRC = readFileSync(
|
|
128
|
+
resolve(__dirname, '..', 'gateway', 'bot-commands-model-effort.ts'),
|
|
129
|
+
'utf8',
|
|
130
|
+
)
|
|
131
|
+
const idx = MODULE_SRC.indexOf('const reply = await handleModelCommand(parsed, cmdDeps)')
|
|
126
132
|
expect(idx).toBeGreaterThan(0)
|
|
127
|
-
const win =
|
|
133
|
+
const win = MODULE_SRC.slice(idx, idx + 400)
|
|
128
134
|
expect(win).not.toContain('recordTypedModelSwitch')
|
|
129
135
|
expect(win).toContain('switchroomReply(ctx, reply.text')
|
|
130
136
|
})
|
|
@@ -1,37 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Machine-authoritative inbound routing (#2794 cutover; kill switch
|
|
3
|
+
* removed in #2996 P1).
|
|
3
4
|
*
|
|
4
|
-
* `handleInbound`
|
|
5
|
+
* `handleInbound` dispatches the machine's captured `inbound` effects
|
|
5
6
|
* through `dispatchEffects` as the AUTHORITATIVE deliver-vs-buffer routing.
|
|
6
|
-
* These tests pin the
|
|
7
|
+
* These tests pin the delivery outcomes that routing rests on: fresh turn →
|
|
8
|
+
* setTurnStarted + deliverToBridge (real send); mid-turn → buffer (no send,
|
|
9
|
+
* drainable); steer mid-turn → deliverToBridge without setTurnStarted.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Plus the `onDeliverResult` observer: the gateway's machine-deliver path
|
|
12
|
+
* branches on the send outcome (delivered → steer-ack + delivery-confirm
|
|
13
|
+
* tracking; miss → durable-buffer + restart notice), so the callback
|
|
14
|
+
* contract (fires once per deliverToBridge, with the real ok, and never
|
|
15
|
+
* breaks delivery when the observer throws) is load-bearing.
|
|
12
16
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* false from `isDeliveryCutoverEnabled`) and `dispatchEffects` is a
|
|
16
|
-
* total no-op, so the imperative twin in gateway.ts is the only
|
|
17
|
-
* executor. Env is read at module load, so the kill-switch tests use
|
|
18
|
-
* vi.resetModules + a fresh dynamic import.
|
|
19
|
-
*
|
|
20
|
-
* Plus the `onDeliverResult` observer added for the flip: the gateway's
|
|
21
|
-
* machine-deliver path branches on the send outcome (delivered → busy-mark
|
|
22
|
-
* + delivery-confirm tracking; miss → release + durable-buffer + restart
|
|
23
|
-
* notice), so the callback contract (fires once per deliverToBridge, with
|
|
24
|
-
* the real ok, and never breaks delivery when the observer throws) is
|
|
25
|
-
* load-bearing.
|
|
17
|
+
* The `SWITCHROOM_DELIVERY_MACHINE_CUTOVER=0` kill-switch subprocess suite
|
|
18
|
+
* and its fixture probe were deleted in #2996 P1 with the switch itself.
|
|
26
19
|
*/
|
|
27
20
|
|
|
28
21
|
import { describe, expect, it, vi } from 'vitest'
|
|
29
|
-
import {
|
|
30
|
-
import { join } from 'node:path'
|
|
31
|
-
import {
|
|
32
|
-
dispatchEffects,
|
|
33
|
-
isDispatchEnabled,
|
|
34
|
-
} from '../gateway/inbound-delivery-machine-dispatch'
|
|
22
|
+
import { dispatchEffects } from '../gateway/inbound-delivery-machine-dispatch'
|
|
35
23
|
import type { DispatchCtx } from '../gateway/inbound-delivery-machine-dispatch'
|
|
36
24
|
import { createPendingInboundBuffer } from '../gateway/pending-inbound-buffer'
|
|
37
25
|
import { createPendingPermissionBuffer } from '../gateway/pending-permission-decisions'
|
|
@@ -41,9 +29,6 @@ import {
|
|
|
41
29
|
type ChatKey,
|
|
42
30
|
type State,
|
|
43
31
|
} from '../gateway/inbound-delivery-machine'
|
|
44
|
-
import {
|
|
45
|
-
isDeliveryCutoverEnabled,
|
|
46
|
-
} from '../gateway/inbound-delivery-machine-shadow'
|
|
47
32
|
|
|
48
33
|
const KEY = '111:_' as ChatKey
|
|
49
34
|
|
|
@@ -87,13 +72,7 @@ function aliveIdle(): State {
|
|
|
87
72
|
return transition(initialState(), { kind: 'bridgeUp', at: 1000 }).state
|
|
88
73
|
}
|
|
89
74
|
|
|
90
|
-
describe('
|
|
91
|
-
it('dispatcher and gate are enabled by default (no kill-switch in test env)', () => {
|
|
92
|
-
expect(process.env.SWITCHROOM_DELIVERY_MACHINE_CUTOVER).toBeUndefined()
|
|
93
|
-
expect(isDispatchEnabled()).toBe(true)
|
|
94
|
-
expect(isDeliveryCutoverEnabled()).toBe(true)
|
|
95
|
-
})
|
|
96
|
-
|
|
75
|
+
describe('machine-authoritative routing — delivery outcomes', () => {
|
|
97
76
|
it('fresh-turn inbound: machine effects execute setTurnStarted + deliverToBridge', () => {
|
|
98
77
|
const state = aliveIdle()
|
|
99
78
|
const msg = ipcMsg()
|
|
@@ -240,79 +219,71 @@ describe('onDeliverResult observer contract (the flip’s post-send branch sourc
|
|
|
240
219
|
})
|
|
241
220
|
})
|
|
242
221
|
|
|
243
|
-
describe('machine deliver path —
|
|
244
|
-
//
|
|
245
|
-
// the
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
222
|
+
describe('machine deliver path — steer vs fresh-turn setTurnStarted contract', () => {
|
|
223
|
+
// The gateway's machine-deliver wiring keys "was this a fresh turn?" off
|
|
224
|
+
// the setTurnStarted effect: a mid-turn steer emits deliverToBridge
|
|
225
|
+
// WITHOUT setTurnStarted (it amends the running turn, it does not open
|
|
226
|
+
// one). A machine change that started emitting setTurnStarted for steers
|
|
227
|
+
// would mis-classify steers as fresh turns at the live call site.
|
|
228
|
+
it('steer mid-turn emits NO setTurnStarted even on a send-miss', () => {
|
|
229
|
+
const s1 = transition(aliveIdle(), {
|
|
230
|
+
kind: 'inbound',
|
|
231
|
+
key: KEY,
|
|
232
|
+
msg: machineMsg(ipcMsg()),
|
|
233
|
+
at: 2000,
|
|
234
|
+
}).state
|
|
235
|
+
const { effects } = transition(s1, {
|
|
236
|
+
kind: 'inbound',
|
|
237
|
+
key: KEY,
|
|
238
|
+
msg: machineMsg(ipcMsg(), true),
|
|
239
|
+
at: 3000,
|
|
240
|
+
})
|
|
241
|
+
expect(effects.some((e) => e.kind === 'setTurnStarted')).toBe(false)
|
|
254
242
|
let stamped: string | null = null
|
|
255
|
-
let delivered =
|
|
243
|
+
let delivered = true
|
|
256
244
|
const { ctx } = makeCtx({ ipcServer: { sendToAgent: vi.fn(() => false) } as never })
|
|
257
245
|
dispatchEffects(effects, {
|
|
258
246
|
...ctx,
|
|
259
247
|
onSetTurnStarted: (k) => {
|
|
260
248
|
stamped = k
|
|
261
|
-
busyKeys.add(k)
|
|
262
249
|
},
|
|
263
250
|
onDeliverResult: (_k, ok) => {
|
|
264
251
|
delivered = ok
|
|
265
252
|
},
|
|
266
253
|
})
|
|
254
|
+
expect(stamped).toBeNull()
|
|
267
255
|
expect(delivered).toBe(false)
|
|
268
|
-
|
|
269
|
-
if (stamped != null) busyKeys.delete(stamped)
|
|
270
|
-
return { stamped }
|
|
271
|
-
}
|
|
256
|
+
})
|
|
272
257
|
|
|
273
|
-
it('fresh
|
|
274
|
-
const busyKeys = new Set<string>()
|
|
258
|
+
it('fresh turn fires setTurnStarted exactly once, before the send', () => {
|
|
275
259
|
const { effects } = transition(aliveIdle(), {
|
|
276
260
|
kind: 'inbound',
|
|
277
261
|
key: KEY,
|
|
278
262
|
msg: machineMsg(ipcMsg()),
|
|
279
263
|
at: 2000,
|
|
280
264
|
})
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
it('steer miss: no setTurnStarted fired, the ORIGINAL turn\'s key survives', () => {
|
|
287
|
-
// Original fresh turn holds the busy key.
|
|
288
|
-
const busyKeys = new Set<string>([KEY])
|
|
289
|
-
const s1 = transition(aliveIdle(), {
|
|
290
|
-
kind: 'inbound',
|
|
291
|
-
key: KEY,
|
|
292
|
-
msg: machineMsg(ipcMsg()),
|
|
293
|
-
at: 2000,
|
|
294
|
-
}).state
|
|
295
|
-
const { effects } = transition(s1, {
|
|
296
|
-
kind: 'inbound',
|
|
297
|
-
key: KEY,
|
|
298
|
-
msg: machineMsg(ipcMsg(), true),
|
|
299
|
-
at: 3000,
|
|
265
|
+
const order: string[] = []
|
|
266
|
+
const sendToAgent = vi.fn(() => {
|
|
267
|
+
order.push('send')
|
|
268
|
+
return true
|
|
300
269
|
})
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
270
|
+
const { ctx } = makeCtx({ ipcServer: { sendToAgent } as never })
|
|
271
|
+
dispatchEffects(effects, {
|
|
272
|
+
...ctx,
|
|
273
|
+
onSetTurnStarted: () => order.push('setTurnStarted'),
|
|
274
|
+
})
|
|
275
|
+
expect(order).toEqual(['setTurnStarted', 'send'])
|
|
306
276
|
})
|
|
307
277
|
})
|
|
308
278
|
|
|
309
279
|
describe('carve-out routing anchors (gateway keys off these machine facts)', () => {
|
|
310
280
|
it('PIN: bridge-dead inbound emits the exact `inbound_bridge_dead_buffer` trace stage', () => {
|
|
311
281
|
// gateway.ts's machineBridgeDead carve-out matches this string verbatim
|
|
312
|
-
// to route bridge-dead inbounds to the imperative
|
|
313
|
-
// silently reroutes them to the machine's buffer+persist
|
|
314
|
-
//
|
|
315
|
-
// pin makes the rename loud. Deleted
|
|
282
|
+
// to route bridge-dead inbounds to the imperative delivery body. A
|
|
283
|
+
// rename here silently reroutes them to the machine's buffer+persist
|
|
284
|
+
// (losing the shouldTrackDelivery drop semantics + restart notice) —
|
|
285
|
+
// this pin makes the rename loud. Deleted with the carve-out when the
|
|
286
|
+
// machine grows bridge_dead send semantics (#2794 PR4 remainder).
|
|
316
287
|
const dead = initialState() // bridge_dead is the initial global state
|
|
317
288
|
const { effects } = transition(dead, {
|
|
318
289
|
kind: 'inbound',
|
|
@@ -349,70 +320,3 @@ describe('carve-out routing anchors (gateway keys off these machine facts)', ()
|
|
|
349
320
|
expect(effects.some((e) => e.kind === 'deliverToBridge')).toBe(false)
|
|
350
321
|
})
|
|
351
322
|
})
|
|
352
|
-
|
|
353
|
-
// The env var is read at MODULE LOAD in the dispatch + shadow modules, and
|
|
354
|
-
// bun's `vi` shim has no resetModules/stubEnv — so the kill-switch/default
|
|
355
|
-
// assertions run the fixture probe in a SUBPROCESS with a controlled env
|
|
356
|
-
// (same pattern as bridge-anonymous-refuse.test.ts).
|
|
357
|
-
describe('kill switch — SWITCHROOM_DELIVERY_MACHINE_CUTOVER=0 restores legacy (subprocess)', () => {
|
|
358
|
-
const PROBE = join(__dirname, 'fixtures', 'cutover-killswitch-probe.ts')
|
|
359
|
-
|
|
360
|
-
function bunBin(): string | null {
|
|
361
|
-
const r = spawnSync('which', ['bun'], { encoding: 'utf-8' })
|
|
362
|
-
const p = r.status === 0 ? r.stdout.trim() : ''
|
|
363
|
-
return p !== '' ? p : null
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function runProbe(env: Record<string, string | undefined>) {
|
|
367
|
-
const bun = bunBin()
|
|
368
|
-
if (bun == null) return null
|
|
369
|
-
const r = spawnSync(bun, ['run', PROBE], {
|
|
370
|
-
encoding: 'utf-8',
|
|
371
|
-
env: { ...process.env, ...env },
|
|
372
|
-
timeout: 30_000,
|
|
373
|
-
})
|
|
374
|
-
expect(r.status).toBe(0)
|
|
375
|
-
const line = r.stdout.trim().split('\n').pop() ?? ''
|
|
376
|
-
return JSON.parse(line) as {
|
|
377
|
-
dispatchEnabled: boolean
|
|
378
|
-
cutoverEnabled: boolean
|
|
379
|
-
shadowEnabled: boolean
|
|
380
|
-
sendCalls: number
|
|
381
|
-
buffered: number
|
|
382
|
-
setTurnStartedCalls: number
|
|
383
|
-
deliverResults: boolean[]
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
it('default (no env): machine authoritative — dispatcher executes the deliver', () => {
|
|
388
|
-
const v = runProbe({ SWITCHROOM_DELIVERY_MACHINE_CUTOVER: undefined })
|
|
389
|
-
if (v == null) return // bun not on PATH — covered by the in-process default tests above
|
|
390
|
-
expect(v.dispatchEnabled).toBe(true)
|
|
391
|
-
expect(v.cutoverEnabled).toBe(true)
|
|
392
|
-
expect(v.sendCalls).toBe(1)
|
|
393
|
-
expect(v.setTurnStartedCalls).toBe(1)
|
|
394
|
-
expect(v.deliverResults).toEqual([true])
|
|
395
|
-
expect(v.buffered).toBe(0)
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
it('=0: dispatcher is a total no-op and the gate reverts to legacy; shadow stays on', () => {
|
|
399
|
-
const v = runProbe({ SWITCHROOM_DELIVERY_MACHINE_CUTOVER: '0' })
|
|
400
|
-
if (v == null) return
|
|
401
|
-
expect(v.dispatchEnabled).toBe(false)
|
|
402
|
-
expect(v.cutoverEnabled).toBe(false)
|
|
403
|
-
// Shadow trace continues for the bake; only authoritative reads flip off.
|
|
404
|
-
expect(v.shadowEnabled).toBe(true)
|
|
405
|
-
expect(v.sendCalls).toBe(0)
|
|
406
|
-
expect(v.buffered).toBe(0)
|
|
407
|
-
expect(v.setTurnStartedCalls).toBe(0)
|
|
408
|
-
expect(v.deliverResults).toEqual([])
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
it('=1 (explicit on): identical to the default', () => {
|
|
412
|
-
const v = runProbe({ SWITCHROOM_DELIVERY_MACHINE_CUTOVER: '1' })
|
|
413
|
-
if (v == null) return
|
|
414
|
-
expect(v.dispatchEnabled).toBe(true)
|
|
415
|
-
expect(v.cutoverEnabled).toBe(true)
|
|
416
|
-
expect(v.sendCalls).toBe(1)
|
|
417
|
-
})
|
|
418
|
-
})
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* The turn-in-flight GATE reads the delivery state machine
|
|
3
|
+
* (`isMachineInTurn`) — since #2996 P1 the machine is the SOLE authority
|
|
4
|
+
* (the legacy `claudeBusyKeys` set was deleted).
|
|
4
5
|
*
|
|
5
|
-
* The bug
|
|
6
|
-
* per-delivery
|
|
7
|
-
* exactly one. When a turn-end
|
|
8
|
-
* key) the set
|
|
9
|
-
* subsequent inbound
|
|
10
|
-
* framework-fallback force-
|
|
6
|
+
* The bug the machine gate closes (gymbro/clerk, 2026-05-28): the legacy
|
|
7
|
+
* set was per-delivery — every delivery `.add`ed a key, but turn-end
|
|
8
|
+
* `.delete`d exactly one. When a turn-end was missed (or fired under a
|
|
9
|
+
* non-matching key) the set kept an orphan, `size > 0` read true forever,
|
|
10
|
+
* and EVERY subsequent inbound buffered as "held mid-turn" until the 5-min
|
|
11
|
+
* framework-fallback force-drained it.
|
|
11
12
|
*
|
|
12
13
|
* The machine cannot accumulate orphans: global state holds ONE
|
|
13
14
|
* `activeTurn`, so any matching turnEnd returns it to idle, and the TTL
|
|
@@ -19,7 +20,6 @@ import { describe, expect, it, beforeEach } from 'vitest'
|
|
|
19
20
|
import {
|
|
20
21
|
shadowEmit,
|
|
21
22
|
isMachineInTurn,
|
|
22
|
-
isDeliveryCutoverEnabled,
|
|
23
23
|
__shadowResetForTests,
|
|
24
24
|
} from '../gateway/inbound-delivery-machine-shadow.js'
|
|
25
25
|
import { TURN_TTL_MS, type ChatKey } from '../gateway/inbound-delivery-machine.js'
|
|
@@ -31,13 +31,9 @@ function inbound(key: ChatKey, at: number, msgId = 1) {
|
|
|
31
31
|
shadowEmit({ kind: 'inbound', key, msg: { msgId, isSteering: false, payload: null }, at })
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
describe('
|
|
34
|
+
describe('machine turn-in-flight gate accessors (sole authority, #2996 P1)', () => {
|
|
35
35
|
beforeEach(() => __shadowResetForTests())
|
|
36
36
|
|
|
37
|
-
it('enabled by default (shadow on, no kill-switch in test env)', () => {
|
|
38
|
-
expect(isDeliveryCutoverEnabled()).toBe(true)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
37
|
it('reads idle before any turn (bridge alive)', () => {
|
|
42
38
|
shadowEmit({ kind: 'bridgeUp', at: 1000 })
|
|
43
39
|
expect(isMachineInTurn()).toBe(false)
|
|
@@ -18,13 +18,12 @@
|
|
|
18
18
|
* 3. Invariant #1 at the dispatch layer: an inbound is delivered XOR
|
|
19
19
|
* (buffered AND persisted) — never both, never neither.
|
|
20
20
|
*
|
|
21
|
-
* It does NOT invoke the imperative
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* protocol — do NOT cite this file as that proof.
|
|
21
|
+
* It does NOT invoke the remaining imperative delivery body (the twin
|
|
22
|
+
* carve-outs for `!`-interrupt / bridge_dead, `dispatchPermissionVerdict`,
|
|
23
|
+
* the silence-poke ladder), so it is NOT a proof that machine path and
|
|
24
|
+
* imperative path produce identical I/O. (#2996 P1 deleted the legacy
|
|
25
|
+
* gate/claudeBusyKeys twins after the cutover bake; the carve-out body's
|
|
26
|
+
* eventual deletion — the #2794 PR4 remainder — needs its own evidence.)
|
|
28
27
|
*
|
|
29
28
|
* What it DOES gate: if a future edit adds an effect kind the
|
|
30
29
|
* dispatcher doesn't execute, the "no unhandled effect" assertion fails
|
|
@@ -17,7 +17,6 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
|
17
17
|
import {
|
|
18
18
|
__dispatchOneForTests,
|
|
19
19
|
dispatchEffects,
|
|
20
|
-
isDispatchEnabled,
|
|
21
20
|
} from '../gateway/inbound-delivery-machine-dispatch'
|
|
22
21
|
import type { DispatchCtx } from '../gateway/inbound-delivery-machine-dispatch'
|
|
23
22
|
import { createPendingInboundBuffer } from '../gateway/pending-inbound-buffer'
|
|
@@ -50,12 +49,6 @@ function makeCtx(overrides?: Partial<DispatchCtx>): {
|
|
|
50
49
|
return { ctx, logs, sendToAgent, clientSend, inbound, perm }
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
describe('isDispatchEnabled', () => {
|
|
54
|
-
it('defaults to true when the kill-switch env-var is unset', () => {
|
|
55
|
-
expect(isDispatchEnabled()).toBe(true)
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
52
|
describe('dispatchEffects — drainBuffer', () => {
|
|
60
53
|
it('redelivers buffered inbound to the registered client', () => {
|
|
61
54
|
const { ctx, clientSend, inbound } = makeCtx()
|
|
@@ -175,15 +168,6 @@ describe('dispatchEffects — bridgeUp composite (all three effects in order)',
|
|
|
175
168
|
})
|
|
176
169
|
})
|
|
177
170
|
|
|
178
|
-
describe('dispatchEffects — kill switch', () => {
|
|
179
|
-
// The kill switch is checked at module-load time so we can't toggle
|
|
180
|
-
// it inside the test process easily. We validate the contract that
|
|
181
|
-
// the helper exists and respects the env-var — see isDispatchEnabled.
|
|
182
|
-
it('exposes the kill-switch check', () => {
|
|
183
|
-
expect(typeof isDispatchEnabled()).toBe('boolean')
|
|
184
|
-
})
|
|
185
|
-
})
|
|
186
|
-
|
|
187
171
|
describe('dispatchEffects — inbound-routing effects (PR3c wiring)', () => {
|
|
188
172
|
const k = 'c1:_' as never
|
|
189
173
|
const ipcMsg = { type: 'inbound' as const, id: 'm1', chat_id: '1', text: 'hi', meta: { source: 'telegram' } }
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* — before the permission-reply / `/auth` paste-back / interrupt-empty /
|
|
10
10
|
* secret-detect-drop intercepts — an intercepted message (e.g. an approval
|
|
11
11
|
* reply) drove the machine idle→`bridge_alive_in_turn`, then early-returned
|
|
12
|
-
* as an intercept: no delivery,
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* the
|
|
12
|
+
* as an intercept: no delivery, and critically no `turnEnd`. The machine
|
|
13
|
+
* held the gate closed until the 5-min TTL tick force-cleared it, buffering
|
|
14
|
+
* every inbound (including `/usage`) meanwhile — the dangerous over-hold
|
|
15
|
+
* direction the bake-era gate-parity probe flagged.
|
|
16
16
|
*
|
|
17
17
|
* The behaviour lives inside the un-exported `handleInbound` closure, so —
|
|
18
18
|
* mirroring the other gateway-*.test.ts source-pins — we assert on source
|
|
@@ -28,6 +28,10 @@ import { dirname, resolve } from 'node:path'
|
|
|
28
28
|
|
|
29
29
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
30
30
|
const GATEWAY_SRC = readFileSync(resolve(__dirname, '..', 'gateway', 'gateway.ts'), 'utf8')
|
|
31
|
+
// P8 PR-D: the isSteering classification body moved verbatim to
|
|
32
|
+
// turn-start-surfaces.ts; the gateway call site (armTurnStartSurfaces) is the
|
|
33
|
+
// ordering anchor now.
|
|
34
|
+
const TURN_START_SRC = readFileSync(resolve(__dirname, '..', 'gateway', 'turn-start-surfaces.ts'), 'utf8')
|
|
31
35
|
|
|
32
36
|
/** Byte offset of the sole `shadowEmit({ kind: 'inbound' ... })` call. */
|
|
33
37
|
function inboundEmitOffset(): number {
|
|
@@ -50,21 +54,28 @@ describe('gateway: inbound machine-emit is deferred past the intercepts', () =>
|
|
|
50
54
|
const emitIdx = inboundEmitOffset()
|
|
51
55
|
// The permission-reply intercept is one of the early-return paths that
|
|
52
56
|
// must NOT drive the machine into a turn.
|
|
53
|
-
|
|
57
|
+
// P7 PR-4: the intercept body moved to inbound-interceptors.ts; the
|
|
58
|
+
// gateway call site is the ordering anchor now.
|
|
59
|
+
const permIdx = GATEWAY_SRC.indexOf('interceptPermissionReply({ text, chat_id, msgId }')
|
|
54
60
|
expect(permIdx).toBeGreaterThan(0)
|
|
55
61
|
expect(emitIdx).toBeGreaterThan(permIdx)
|
|
56
62
|
})
|
|
57
63
|
|
|
58
64
|
it('emits AFTER the `/auth add` paste-back intercept', () => {
|
|
59
65
|
const emitIdx = inboundEmitOffset()
|
|
60
|
-
|
|
66
|
+
// P7 PR-5: the intercept body moved to inbound-interceptors.ts; the
|
|
67
|
+
// gateway call site is the ordering anchor now.
|
|
68
|
+
const authIdx = GATEWAY_SRC.indexOf('interceptAuthAdd(')
|
|
61
69
|
expect(authIdx).toBeGreaterThan(0)
|
|
62
70
|
expect(emitIdx).toBeGreaterThan(authIdx)
|
|
63
71
|
})
|
|
64
72
|
|
|
65
73
|
it('emits AFTER isSteering is classified, and passes the real value (not a hardcoded false)', () => {
|
|
66
74
|
const emitIdx = inboundEmitOffset()
|
|
67
|
-
|
|
75
|
+
// P8 PR-D: classification lives in turn-start-surfaces.ts; the gateway
|
|
76
|
+
// ordering anchor is the armTurnStartSurfaces call that computes it.
|
|
77
|
+
expect(TURN_START_SRC).toContain('isSteering = priorTurnInFlight && isSteerPrefix')
|
|
78
|
+
const steerIdx = GATEWAY_SRC.indexOf('turnStartSurfaces().armTurnStartSurfaces({')
|
|
68
79
|
expect(steerIdx).toBeGreaterThan(0)
|
|
69
80
|
expect(emitIdx).toBeGreaterThan(steerIdx)
|
|
70
81
|
// The emit's msg object must forward the live `isSteering` binding, so a
|
|
@@ -39,30 +39,62 @@ const SRC = readFileSync(
|
|
|
39
39
|
'utf8',
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
+
// The media-envelope handlers were extracted out of gateway.ts into their own
|
|
43
|
+
// module (switchroom#2996 P6 cluster A). gateway.ts keeps the `bot.on(…)`
|
|
44
|
+
// registration lines (their order is a load-bearing invariant) but each now
|
|
45
|
+
// delegates to a `handle<Kind>Message(ctx, mediaEnvelopeDeps)` function that
|
|
46
|
+
// lives here. The structural assertions below still need to see the real
|
|
47
|
+
// handler body, so we resolve a delegation to the extracted source.
|
|
48
|
+
const MEDIA_SRC = readFileSync(
|
|
49
|
+
new URL('../gateway/media-message-handlers.ts', import.meta.url),
|
|
50
|
+
'utf8',
|
|
51
|
+
)
|
|
52
|
+
|
|
42
53
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
43
54
|
|
|
44
55
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `
|
|
48
|
-
* AST parse.
|
|
56
|
+
* Return the substring of `src` starting at `header` up to the matching
|
|
57
|
+
* closing brace of the block it opens. Naive depth count of {/} from the
|
|
58
|
+
* first `{` — good enough for grepping, not a full AST parse.
|
|
49
59
|
*/
|
|
50
|
-
function
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Find the matching close — naive depth count of {/} from the first `{`.
|
|
55
|
-
const firstBrace = SRC.indexOf('{', start)
|
|
60
|
+
function braceBlock(src: string, header: string, label: string): string {
|
|
61
|
+
const start = src.indexOf(header)
|
|
62
|
+
expect(start, `${label} not found`).toBeGreaterThan(0)
|
|
63
|
+
const firstBrace = src.indexOf('{', start)
|
|
56
64
|
let depth = 0
|
|
57
|
-
for (let i = firstBrace; i <
|
|
58
|
-
const c =
|
|
65
|
+
for (let i = firstBrace; i < src.length; i++) {
|
|
66
|
+
const c = src[i]
|
|
59
67
|
if (c === '{') depth++
|
|
60
68
|
else if (c === '}') {
|
|
61
69
|
depth--
|
|
62
|
-
if (depth === 0) return
|
|
70
|
+
if (depth === 0) return src.slice(start, i + 1)
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
|
-
throw new Error(`could not find end of
|
|
73
|
+
throw new Error(`could not find end of ${label}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract the body of a `bot.on('message:<kind>', …)` handler. If the
|
|
78
|
+
* registration delegates to an extracted `handle<Kind>Message(ctx,
|
|
79
|
+
* mediaEnvelopeDeps)` function, the returned body is that function's source
|
|
80
|
+
* (from media-message-handlers.ts). Otherwise it is the inline gateway.ts
|
|
81
|
+
* handler block. Either way callers see the real handler logic.
|
|
82
|
+
*/
|
|
83
|
+
function handlerBody(kind: string): string {
|
|
84
|
+
const needle = `bot.on('message:${kind}'`
|
|
85
|
+
const start = SRC.indexOf(needle)
|
|
86
|
+
expect(start, `handler bot.on('message:${kind}') not found`).toBeGreaterThan(0)
|
|
87
|
+
const lineEnd = SRC.indexOf('\n', start)
|
|
88
|
+
const regLine = SRC.slice(start, lineEnd < 0 ? SRC.length : lineEnd)
|
|
89
|
+
const delegate = regLine.match(/(handle\w+Message)\(ctx,\s*mediaEnvelopeDeps\)/)
|
|
90
|
+
if (delegate) {
|
|
91
|
+
return braceBlock(
|
|
92
|
+
MEDIA_SRC,
|
|
93
|
+
`export async function ${delegate[1]}(`,
|
|
94
|
+
`extracted handler ${delegate[1]}`,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
return braceBlock(SRC, needle, `handler ${kind}`)
|
|
66
98
|
}
|
|
67
99
|
|
|
68
100
|
// ─── Registration completeness ───────────────────────────────────────────
|
|
@@ -108,7 +140,9 @@ describe('inbound message-type handlers: forwarding decisions', () => {
|
|
|
108
140
|
it(`${kind} forwards via handleInbound and logs to stderr`, () => {
|
|
109
141
|
const body = handlerBody(kind)
|
|
110
142
|
expect(body).toMatch(/handleInbound\(ctx,/)
|
|
111
|
-
|
|
143
|
+
// Logs the event — directly (inline handlers) or via the injected
|
|
144
|
+
// `deps.log` stderr sink (extracted media-envelope handlers).
|
|
145
|
+
expect(body).toMatch(/process\.stderr\.write|deps\.log\(/)
|
|
112
146
|
// Must not divert to the ack-only path — that would silently
|
|
113
147
|
// hide the payload from the agent.
|
|
114
148
|
expect(body).not.toMatch(/handleAckOnly\(/)
|
|
@@ -148,7 +182,9 @@ describe('inbound message-type handlers: ack-only decisions', () => {
|
|
|
148
182
|
it(`${kind} uses handleAckOnly (no forward to agent)`, () => {
|
|
149
183
|
const body = handlerBody(kind)
|
|
150
184
|
expect(body).toMatch(/handleAckOnly\(/)
|
|
151
|
-
|
|
185
|
+
// Logs the event — directly (inline handlers) or via the injected
|
|
186
|
+
// `deps.log` stderr sink (extracted media-envelope handlers).
|
|
187
|
+
expect(body).toMatch(/process\.stderr\.write|deps\.log\(/)
|
|
152
188
|
// Ack-only must NOT call handleInbound — the whole point is
|
|
153
189
|
// we don't bother the agent for these.
|
|
154
190
|
expect(body).not.toMatch(/handleInbound\(/)
|