switchroom 0.18.32 → 0.19.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/auth-broker/index.js +17 -1
- package/dist/cli/switchroom.js +847 -729
- package/dist/host-control/main.js +18 -2
- package/dist/vault/approvals/kernel-server.js +17 -1
- package/dist/vault/broker/server.js +44 -2
- package/package.json +2 -2
- package/profiles/_base/start.sh.hbs +105 -18
- package/telegram-plugin/dist/gateway/gateway.js +60612 -56998
- 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/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/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/gateway.ts +1347 -6758
- 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-wiring.ts +333 -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/render/render.ts +25 -1
- package/telegram-plugin/status-no-truncate.ts +13 -0
- package/telegram-plugin/subagent-watcher.ts +186 -3
- 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/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 +15 -6
- package/telegram-plugin/tests/busy-ack-wiring.test.ts +6 -1
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +18 -9
- package/telegram-plugin/tests/callback-query-handlers.test.ts +101 -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 +47 -10
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +27 -9
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +30 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +39 -18
- package/telegram-plugin/tests/gateway-boot-smoke.test.ts +160 -0
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +3 -7
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +44 -29
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +8 -2
- 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-emit-after-intercepts.test.ts +14 -3
- package/telegram-plugin/tests/inbound-message-types.test.ts +52 -16
- 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 +27 -9
- 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/pending-card-durability-wiring.test.ts +16 -7
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +32 -8
- package/telegram-plugin/tests/photo-message-handler.test.ts +114 -0
- 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/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 +28 -18
- 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 +14 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +35 -3
- package/telegram-plugin/tests/turn-flush-safety.test.ts +183 -5
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +9 -4
- package/telegram-plugin/tests/vault-approval-posture.test.ts +8 -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 +110 -20
- package/telegram-plugin/tests/worker-feed-resume-guard.test.ts +86 -0
- package/telegram-plugin/tool-activity-summary.ts +83 -35
- 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 +10 -4
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime boot-smoke pin (finding M-1, full-stack adversarial review).
|
|
3
|
+
*
|
|
4
|
+
* The source-parse pin `gateway-boot-side-effect-gating.test.ts` proves the
|
|
5
|
+
* module-load side effects are `isGatewayMain`-guarded, but it NEVER actually
|
|
6
|
+
* boots the gateway — so it is structurally blind to a temporal-dead-zone
|
|
7
|
+
* ReferenceError inside a guarded builder. That exact class shipped on main:
|
|
8
|
+
* PR-C3 (#3392) made `gatewayLivenessWiringDeps()` eagerly read
|
|
9
|
+
* `pendingInboundBuffer` (a `const` declared ~200 lines LATER than the
|
|
10
|
+
* module-load `silencePoke.startTimer(...)` call that invokes the builder),
|
|
11
|
+
* crash-looping every real gateway boot with
|
|
12
|
+
* `uncaughtException: ReferenceError: Cannot access 'pendingInboundBuffer'
|
|
13
|
+
* before initialization`
|
|
14
|
+
* right after the boot lock — while CI stayed green because `isGatewayMain`
|
|
15
|
+
* is false under vitest (argv[1] is the runner).
|
|
16
|
+
*
|
|
17
|
+
* This test closes that blind spot the only way a source parse can't: it
|
|
18
|
+
* SPAWNS `bun telegram-plugin/gateway/gateway.ts` as the process entrypoint
|
|
19
|
+
* (so `isGatewayMain` is TRUE and every guarded module-load side effect
|
|
20
|
+
* actually runs), in a hermetic TELEGRAM_STATE_DIR with a fake bot token, and
|
|
21
|
+
* asserts the gateway reaches a POST-module-eval stderr marker
|
|
22
|
+
* (`ipc — listening`, emitted when `createIpcServer` binds its UDS at
|
|
23
|
+
* gateway.ts ~10454, well past the TDZ site) with NO
|
|
24
|
+
* `uncaughtException: ReferenceError` on the way. The 401-Unauthorized the
|
|
25
|
+
* fake token later triggers happens INSIDE `startGateway()` — after module
|
|
26
|
+
* eval — so it never precedes the marker and is not a boot-order failure.
|
|
27
|
+
*
|
|
28
|
+
* Parameterized over the two kill-switch postures so a future eager-read
|
|
29
|
+
* introduced on either the router-v2 or turn-end-funnel-v2 path is also
|
|
30
|
+
* caught:
|
|
31
|
+
* - both flags unset (production default — now BOTH v2 ON after the flip)
|
|
32
|
+
* - SWITCHROOM_INBOUND_ROUTER_V2=1 + SWITCHROOM_TURN_END_FUNNEL_V2=1
|
|
33
|
+
* - both flags =0 (the escape-hatch legacy v1 posture — kept so the legacy
|
|
34
|
+
* module-eval paths stay boot-smoked until the legacy delete)
|
|
35
|
+
*
|
|
36
|
+
* Bun is the prod + CI gateway runtime (`bun test` is a required check), so
|
|
37
|
+
* spawning it here is CI-native, not CI-hostile.
|
|
38
|
+
*
|
|
39
|
+
* VERIFIED to red on pre-fix main: reverting the getter fix in the working
|
|
40
|
+
* tree makes both parameterizations fail — the child dies on the ReferenceError
|
|
41
|
+
* and never emits the marker (mutation evidence in the PR body).
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { describe, it, expect } from 'vitest'
|
|
45
|
+
import { spawn } from 'node:child_process'
|
|
46
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
47
|
+
import { tmpdir } from 'node:os'
|
|
48
|
+
import { join, resolve } from 'node:path'
|
|
49
|
+
import { fileURLToPath } from 'node:url'
|
|
50
|
+
import { dirname } from 'node:path'
|
|
51
|
+
|
|
52
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
53
|
+
const GATEWAY_PATH = resolve(__dirname, '..', 'gateway', 'gateway.ts')
|
|
54
|
+
|
|
55
|
+
// The marker is emitted synchronously by createIpcServer() at gateway.ts
|
|
56
|
+
// (~10454), which is AFTER the module-load silence-poke wiring (~9653) that
|
|
57
|
+
// held the TDZ read. Reaching it proves module eval cleared the crash site.
|
|
58
|
+
const POST_EVAL_MARKER = 'ipc — listening'
|
|
59
|
+
// A fake bot token: valid Bot API shape (<digits>:<35 chars>) so grammy's
|
|
60
|
+
// constructor accepts it; the 401 only lands later during startGateway().
|
|
61
|
+
const FAKE_BOT_TOKEN = '123456:' + 'A'.repeat(35)
|
|
62
|
+
const BOOT_TIMEOUT_MS = 45_000
|
|
63
|
+
|
|
64
|
+
interface BootResult {
|
|
65
|
+
reachedMarker: boolean
|
|
66
|
+
sawReferenceError: boolean
|
|
67
|
+
stderr: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function bootGateway(extraEnv: Record<string, string>): Promise<BootResult> {
|
|
71
|
+
return new Promise<BootResult>((resolvePromise, rejectPromise) => {
|
|
72
|
+
const stateDir = mkdtempSync(join(tmpdir(), 'gw-boot-smoke-'))
|
|
73
|
+
const child = spawn('bun', [GATEWAY_PATH], {
|
|
74
|
+
env: {
|
|
75
|
+
...process.env,
|
|
76
|
+
TELEGRAM_STATE_DIR: stateDir,
|
|
77
|
+
SWITCHROOM_GATEWAY_PID_FILE: join(stateDir, 'gateway.pid.json'),
|
|
78
|
+
TELEGRAM_BOT_TOKEN: FAKE_BOT_TOKEN,
|
|
79
|
+
SWITCHROOM_AGENT_NAME: 'boot-smoke',
|
|
80
|
+
// Scrub the two kill switches to a known-OFF baseline so the
|
|
81
|
+
// "unset" posture is genuinely unset even if the CI shell exports
|
|
82
|
+
// them; the "enabled" posture re-sets them via extraEnv below.
|
|
83
|
+
SWITCHROOM_INBOUND_ROUTER_V2: '',
|
|
84
|
+
SWITCHROOM_TURN_END_FUNNEL_V2: '',
|
|
85
|
+
...extraEnv,
|
|
86
|
+
},
|
|
87
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
let stderr = ''
|
|
91
|
+
let settled = false
|
|
92
|
+
const finish = (): void => {
|
|
93
|
+
if (settled) return
|
|
94
|
+
settled = true
|
|
95
|
+
clearTimeout(timer)
|
|
96
|
+
try { child.kill('SIGKILL') } catch { /* already gone */ }
|
|
97
|
+
try { rmSync(stateDir, { recursive: true, force: true }) } catch { /* best-effort */ }
|
|
98
|
+
resolvePromise({
|
|
99
|
+
reachedMarker: stderr.includes(POST_EVAL_MARKER),
|
|
100
|
+
sawReferenceError: /uncaughtException:\s*ReferenceError/.test(stderr),
|
|
101
|
+
stderr,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const timer = setTimeout(finish, BOOT_TIMEOUT_MS)
|
|
106
|
+
|
|
107
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
108
|
+
stderr += chunk.toString()
|
|
109
|
+
// As soon as we see the marker OR a boot-order ReferenceError, we have
|
|
110
|
+
// our verdict — no need to keep the child alive.
|
|
111
|
+
if (stderr.includes(POST_EVAL_MARKER) || /uncaughtException:\s*ReferenceError/.test(stderr)) {
|
|
112
|
+
finish()
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
child.on('error', (err) => {
|
|
116
|
+
if (settled) return
|
|
117
|
+
settled = true
|
|
118
|
+
clearTimeout(timer)
|
|
119
|
+
try { rmSync(stateDir, { recursive: true, force: true }) } catch { /* best-effort */ }
|
|
120
|
+
rejectPromise(err)
|
|
121
|
+
})
|
|
122
|
+
// If the process exits before the marker (e.g. the TDZ crash aborts the
|
|
123
|
+
// process), resolve with whatever we captured so the assertion can fail
|
|
124
|
+
// with the real stderr.
|
|
125
|
+
child.on('exit', finish)
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const POSTURES: Array<{ name: string; env: Record<string, string> }> = [
|
|
130
|
+
// Flags unset is now the DEFAULT-ON posture (both `!== '0'`) after the
|
|
131
|
+
// v0.18.33 canary flip — the v2 router + v2 turn-end funnel are live.
|
|
132
|
+
{ name: 'both kill switches unset (prod default — now BOTH v2 ON)', env: {} },
|
|
133
|
+
{
|
|
134
|
+
name: 'router-v2 + turn-end-funnel-v2 explicitly enabled',
|
|
135
|
+
env: { SWITCHROOM_INBOUND_ROUTER_V2: '1', SWITCHROOM_TURN_END_FUNNEL_V2: '1' },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
// Explicit legacy posture: with both flags defaulting ON, the only way to
|
|
139
|
+
// boot-smoke the legacy (v1) module-eval paths is the `=0` escape hatch.
|
|
140
|
+
name: 'both kill switches disabled (=0 escape hatch — legacy v1 paths)',
|
|
141
|
+
env: { SWITCHROOM_INBOUND_ROUTER_V2: '0', SWITCHROOM_TURN_END_FUNNEL_V2: '0' },
|
|
142
|
+
},
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
describe('gateway boot-smoke (M-1): real bun boot reaches post-module-eval marker with no TDZ ReferenceError', () => {
|
|
146
|
+
it.each(POSTURES)('boots cleanly with %s', async ({ env }) => {
|
|
147
|
+
const result = await bootGateway(env)
|
|
148
|
+
// The load-bearing assertion: no temporal-dead-zone crash during module
|
|
149
|
+
// eval. If this reds, the stderr tail names the offending binding.
|
|
150
|
+
expect(
|
|
151
|
+
result.sawReferenceError,
|
|
152
|
+
`gateway boot emitted a ReferenceError before module eval finished:\n${result.stderr.slice(-2000)}`,
|
|
153
|
+
).toBe(false)
|
|
154
|
+
// And it actually got past the TDZ site to the IPC-listening marker.
|
|
155
|
+
expect(
|
|
156
|
+
result.reachedMarker,
|
|
157
|
+
`gateway boot never reached "${POST_EVAL_MARKER}"; stderr tail:\n${result.stderr.slice(-2000)}`,
|
|
158
|
+
).toBe(true)
|
|
159
|
+
}, BOOT_TIMEOUT_MS + 15_000)
|
|
160
|
+
})
|
|
@@ -53,15 +53,11 @@ const GATEWAY_SRC = readFileSync(GATEWAY_PATH, 'utf8')
|
|
|
53
53
|
*/
|
|
54
54
|
const GOLDEN_REGISTRATIONS: readonly string[] = [
|
|
55
55
|
'use',
|
|
56
|
-
'
|
|
57
|
-
'command:help',
|
|
58
|
-
'command:status',
|
|
59
|
-
'command:agents',
|
|
56
|
+
'helper:registerStartInfoCommands',
|
|
60
57
|
'command:inject',
|
|
61
58
|
'command:compact',
|
|
62
59
|
'command:clear',
|
|
63
|
-
'
|
|
64
|
-
'command:effort',
|
|
60
|
+
'helper:registerModelEffortCommands',
|
|
65
61
|
'command:agentstart',
|
|
66
62
|
'command:agentstop',
|
|
67
63
|
'command:stop',
|
|
@@ -122,7 +118,7 @@ const GOLDEN_REGISTRATIONS: readonly string[] = [
|
|
|
122
118
|
* every `message:*` handler (grammY no-double-handling; see
|
|
123
119
|
* catch-all-unhandled-message.test.ts) and before `message_reaction`.
|
|
124
120
|
*/
|
|
125
|
-
const HELPER_REGISTRARS = new Set(['registerOpsInfoCommands', 'installUnhandledMessageCatchAll'])
|
|
121
|
+
const HELPER_REGISTRARS = new Set(['registerOpsInfoCommands', 'registerStartInfoCommands', 'registerModelEffortCommands', 'installUnhandledMessageCatchAll'])
|
|
126
122
|
|
|
127
123
|
const sourceFile = ts.createSourceFile(
|
|
128
124
|
GATEWAY_PATH,
|
|
@@ -3,42 +3,50 @@ import { readFileSync } from 'node:fs'
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Structural integration test for the loopback-paste fail-safe redaction in
|
|
6
|
-
* the
|
|
6
|
+
* the inbound intercept gauntlet (security audit #3084, F2/F3).
|
|
7
7
|
*
|
|
8
|
-
* Why structural:
|
|
9
|
-
* pure-functional invocation would require end-to-end harnessing of
|
|
10
|
-
* Bot/Grammy/Context. The decision predicate (shouldConsumeLoopbackPaste, incl.
|
|
8
|
+
* Why structural: the decision predicate (shouldConsumeLoopbackPaste, incl.
|
|
11
9
|
* the malformed-paste fail-safe) is exhaustively unit-tested in
|
|
12
10
|
* `auth-loopback-relay.test.ts`, and the redaction primitive
|
|
13
11
|
* (redactAuthCodeMessage) in `auth-code-redact.test.ts`. What's left to assert
|
|
14
|
-
* here is that the
|
|
15
|
-
* that is malformed, or arrives just after the intercept TTL, or has no
|
|
16
|
-
* flow, is redacted + dropped rather than forwarded to the agent.
|
|
12
|
+
* here is that the intercept path actually WIRES a fail-safe so a code-bearing
|
|
13
|
+
* paste that is malformed, or arrives just after the intercept TTL, or has no
|
|
14
|
+
* active flow, is redacted + dropped rather than forwarded to the agent.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
16
|
+
* P7 PR-6 (#2996): the intercept body (pending-flow branch + fail-safe tail)
|
|
17
|
+
* moved verbatim into gateway/inbound-interceptors.ts
|
|
18
|
+
* (`interceptLoopbackRelay`); gateway.ts keeps the ordered delegation. The
|
|
19
|
+
* invariants are unchanged — only the file that owns each literal moved:
|
|
19
20
|
* 1. The stale (TTL-expired) loopback branch does NOT let the paste fall
|
|
20
|
-
* through to the agent — it flows into the fail-safe below.
|
|
21
|
-
* 2. A fail-safe `if (shouldConsumeLoopbackPaste(text))` block exists that
|
|
22
|
-
* redacts (redactAuthCodeMessage) and returns
|
|
23
|
-
* pending-flow branch
|
|
24
|
-
*
|
|
21
|
+
* through to the agent — it flows into the fail-safe below it.
|
|
22
|
+
* 2. A fail-safe `if (shouldConsumeLoopbackPaste(p.text))` block exists that
|
|
23
|
+
* redacts (deps.redactAuthCode → redactAuthCodeMessage) and returns
|
|
24
|
+
* handled, sitting AFTER the pending-flow branch; the gateway invokes the
|
|
25
|
+
* loopback intercept BEFORE the generic auth-code / reauth intercept.
|
|
25
26
|
*/
|
|
26
27
|
describe('gateway loopback-paste fail-safe redaction — structural wiring', () => {
|
|
27
|
-
const
|
|
28
|
+
const gatewaySrc = readFileSync(
|
|
28
29
|
new URL('../gateway/gateway.ts', import.meta.url),
|
|
29
30
|
'utf8',
|
|
30
31
|
)
|
|
32
|
+
const interceptorsSrc = readFileSync(
|
|
33
|
+
new URL('../gateway/inbound-interceptors.ts', import.meta.url),
|
|
34
|
+
'utf8',
|
|
35
|
+
)
|
|
31
36
|
|
|
32
|
-
it('
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
it('wires the consume predicate + redaction helper', () => {
|
|
38
|
+
// The intercept module owns the predicate; the gateway binds the
|
|
39
|
+
// redaction closure (deps.redactAuthCode → redactAuthCodeMessage).
|
|
40
|
+
expect(interceptorsSrc).toMatch(/shouldConsumeLoopbackPaste/)
|
|
41
|
+
expect(gatewaySrc).toMatch(/redactAuthCodeMessage/)
|
|
42
|
+
expect(gatewaySrc).toMatch(/interceptLoopbackRelay\(/)
|
|
35
43
|
})
|
|
36
44
|
|
|
37
45
|
it('has a fail-safe redact-and-return block guarded by shouldConsumeLoopbackPaste', () => {
|
|
38
|
-
// Isolate the fail-safe block: `if (shouldConsumeLoopbackPaste(text)) { ... }`
|
|
46
|
+
// Isolate the fail-safe block: `if (shouldConsumeLoopbackPaste(p.text)) { ... }`
|
|
39
47
|
// that is NOT part of the `pendingLoop && ...` pending-flow branch.
|
|
40
|
-
const failSafe =
|
|
41
|
-
/if \(shouldConsumeLoopbackPaste\(text\)\) \{[\s\S]{0,
|
|
48
|
+
const failSafe = interceptorsSrc.match(
|
|
49
|
+
/if \(shouldConsumeLoopbackPaste\(p\.text\)\) \{[\s\S]{0,700}?\n {2}\}/g,
|
|
42
50
|
)
|
|
43
51
|
expect(failSafe).not.toBeNull()
|
|
44
52
|
// The standalone fail-safe (the one NOT preceded by `pendingLoop &&`).
|
|
@@ -46,21 +54,28 @@ describe('gateway loopback-paste fail-safe redaction — structural wiring', ()
|
|
|
46
54
|
(b) => !b.startsWith('if (pendingLoop'),
|
|
47
55
|
)
|
|
48
56
|
expect(standalone).toBeDefined()
|
|
49
|
-
expect(standalone!).toMatch(/
|
|
50
|
-
expect(standalone!).toMatch(/return/)
|
|
57
|
+
expect(standalone!).toMatch(/deps\.redactAuthCode/)
|
|
58
|
+
expect(standalone!).toMatch(/return \{ handled: true \}/)
|
|
51
59
|
})
|
|
52
60
|
|
|
53
61
|
it('the stale loopback branch falls through to the fail-safe (does not forward)', () => {
|
|
54
|
-
// The
|
|
55
|
-
//
|
|
56
|
-
// auth-code intercept (`pendingReauthFlows`).
|
|
57
|
-
const failSafeIdx =
|
|
62
|
+
// The fail-safe block must appear AFTER the pending-loop branch inside the
|
|
63
|
+
// intercept module; and the gateway must invoke the loopback intercept
|
|
64
|
+
// BEFORE the generic auth-code intercept (`pendingReauthFlows`).
|
|
65
|
+
const failSafeIdx = interceptorsSrc.indexOf(
|
|
58
66
|
'Fail-safe redaction (security audit #3084, F2/F3)',
|
|
59
67
|
)
|
|
60
|
-
const pendingLoopIdx =
|
|
61
|
-
|
|
68
|
+
const pendingLoopIdx = interceptorsSrc.indexOf(
|
|
69
|
+
'const pendingLoop = pendingLoopbackFlows.get',
|
|
70
|
+
)
|
|
62
71
|
expect(failSafeIdx).toBeGreaterThan(-1)
|
|
63
72
|
expect(failSafeIdx).toBeGreaterThan(pendingLoopIdx)
|
|
64
|
-
|
|
73
|
+
|
|
74
|
+
const loopbackCallIdx = gatewaySrc.indexOf('interceptLoopbackRelay(')
|
|
75
|
+
// P7 PR-7: the reauth intercept body also moved; its gateway call site is
|
|
76
|
+
// the ordering anchor.
|
|
77
|
+
const reauthIdx = gatewaySrc.indexOf('interceptReauth(')
|
|
78
|
+
expect(loopbackCallIdx).toBeGreaterThan(-1)
|
|
79
|
+
expect(reauthIdx).toBeGreaterThan(loopbackCallIdx)
|
|
65
80
|
})
|
|
66
81
|
})
|
|
@@ -29,6 +29,12 @@ describe('gateway outbound secret-scrub — structural wiring', () => {
|
|
|
29
29
|
new URL('../gateway/outbound-send-path.ts', import.meta.url),
|
|
30
30
|
'utf8',
|
|
31
31
|
)
|
|
32
|
+
// #2996 P4-A: the turn-flush backstop scrub lives in handleSessionEvent,
|
|
33
|
+
// which moved VERBATIM to stream-render.ts.
|
|
34
|
+
const streamSrc = readFileSync(
|
|
35
|
+
new URL('../gateway/stream-render.ts', import.meta.url),
|
|
36
|
+
'utf8',
|
|
37
|
+
)
|
|
32
38
|
|
|
33
39
|
it('imports the shared redactor', () => {
|
|
34
40
|
expect(src).toMatch(/import \{ redact \} from '\.\.\/secret-detect\/redact\.js'/)
|
|
@@ -66,8 +72,8 @@ describe('gateway outbound secret-scrub — structural wiring', () => {
|
|
|
66
72
|
it('turn-flush backstop: scrubs the model terminal prose before send', () => {
|
|
67
73
|
// Turn-flush delivers the model's answer when it skipped reply/stream_reply
|
|
68
74
|
// — arbitrary agent free-text that hits the wire + stderr preview.
|
|
69
|
-
const redactIdx =
|
|
70
|
-
const scrubSiteIdx =
|
|
75
|
+
const redactIdx = streamSrc.indexOf(`redactOutboundText(capturedText, 'turn_flush')`)
|
|
76
|
+
const scrubSiteIdx = streamSrc.indexOf(`site: 'turn_flush'`)
|
|
71
77
|
expect(redactIdx).toBeGreaterThan(0)
|
|
72
78
|
expect(scrubSiteIdx).toBeGreaterThan(redactIdx) // mask BEFORE the voice scrub + send
|
|
73
79
|
})
|
|
@@ -15,6 +15,10 @@ import { readFileSync } from 'node:fs'
|
|
|
15
15
|
*/
|
|
16
16
|
describe('request_secret — gateway wiring', () => {
|
|
17
17
|
const gw = readFileSync(new URL('../gateway/gateway.ts', import.meta.url), 'utf8')
|
|
18
|
+
// executeRequestSecret moved verbatim to card-tool-handlers.ts (#2996 P5-tail);
|
|
19
|
+
// the handler-body pins below read that module. The dispatch case + the
|
|
20
|
+
// secure-capture path (captureProvidedSecret / handleInbound) stay in gateway.ts.
|
|
21
|
+
const cardTool = readFileSync(new URL('../gateway/card-tool-handlers.ts', import.meta.url), 'utf8')
|
|
18
22
|
const bridge = readFileSync(new URL('../bridge/bridge.ts', import.meta.url), 'utf8')
|
|
19
23
|
|
|
20
24
|
it('declares the MCP tool with required {chat_id,key} and NO value arg', () => {
|
|
@@ -30,7 +34,7 @@ describe('request_secret — gateway wiring', () => {
|
|
|
30
34
|
|
|
31
35
|
it('is allow-listed and dispatched', () => {
|
|
32
36
|
expect(gw).toMatch(/'request_secret',\n/)
|
|
33
|
-
expect(gw).toMatch(/case 'request_secret':\s*\n\s*return executeRequestSecret\(args\)/)
|
|
37
|
+
expect(gw).toMatch(/case 'request_secret':\s*\n\s*return cardToolHandlers\.executeRequestSecret\(args\)/)
|
|
34
38
|
})
|
|
35
39
|
|
|
36
40
|
it('routes the vsp: callback', () => {
|
|
@@ -71,8 +75,8 @@ describe('request_secret — gateway wiring', () => {
|
|
|
71
75
|
})
|
|
72
76
|
|
|
73
77
|
it('dedupes to one open request per (chat,key)', () => {
|
|
74
|
-
const idx =
|
|
75
|
-
const body =
|
|
78
|
+
const idx = cardTool.indexOf('async function executeRequestSecret(')
|
|
79
|
+
const body = cardTool.slice(idx, idx + 1800)
|
|
76
80
|
expect(body).toMatch(/p\.chat_id === chat_id && p\.key === key/)
|
|
77
81
|
})
|
|
78
82
|
})
|
|
@@ -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
|
})
|
|
@@ -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\(/)
|