switchroom 0.19.48 → 0.20.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.
Files changed (50) hide show
  1. package/dist/agent-scheduler/index.js +18 -1
  2. package/dist/auth-broker/index.js +19 -2
  3. package/dist/buzz-gateway/index.js +9207 -0
  4. package/dist/cli/notion-write-pretool.mjs +18 -1
  5. package/dist/cli/switchroom.js +63 -4
  6. package/dist/host-control/main.js +20 -3
  7. package/dist/vault/approvals/kernel-server.js +19 -2
  8. package/dist/vault/broker/server.js +19 -2
  9. package/package.json +4 -3
  10. package/profiles/_base/start.sh.hbs +78 -1
  11. package/profiles/default/CLAUDE.md.hbs +1 -1
  12. package/skills/dev-protocol/SKILL.md +30 -1
  13. package/skills/switchroom-architecture/SKILL.md +5 -0
  14. package/skills/switchroom-cli/SKILL.md +1 -1
  15. package/telegram-plugin/dist/bridge/bridge.js +7 -4
  16. package/telegram-plugin/dist/gateway/gateway.js +1149 -247
  17. package/telegram-plugin/dist/server.js +7 -4
  18. package/telegram-plugin/gateway/boot-briefing-builder.ts +458 -0
  19. package/telegram-plugin/gateway/boot-briefing-wiring.ts +170 -0
  20. package/telegram-plugin/gateway/buzz-mirror.ts +329 -0
  21. package/telegram-plugin/gateway/buzz-type-guards.ts +34 -0
  22. package/telegram-plugin/gateway/channel-route.ts +272 -0
  23. package/telegram-plugin/gateway/gateway.ts +73 -81
  24. package/telegram-plugin/gateway/inbound-spool.ts +33 -1
  25. package/telegram-plugin/gateway/ipc-protocol.ts +81 -2
  26. package/telegram-plugin/gateway/ipc-server.ts +197 -2
  27. package/telegram-plugin/gateway/outbound-send-path.ts +37 -1
  28. package/telegram-plugin/gateway/pending-turn-env.ts +61 -0
  29. package/telegram-plugin/gateway/stream-render.ts +21 -0
  30. package/telegram-plugin/gateway/subagent-handback-marker.ts +12 -0
  31. package/telegram-plugin/gateway/user-failure-notices.ts +172 -0
  32. package/telegram-plugin/history.ts +15 -0
  33. package/telegram-plugin/llm-error-present.ts +9 -4
  34. package/telegram-plugin/model-unavailable.ts +4 -0
  35. package/telegram-plugin/operator-events.fixtures.json +12 -12
  36. package/telegram-plugin/operator-events.ts +81 -9
  37. package/telegram-plugin/session-tail.ts +7 -1
  38. package/telegram-plugin/tests/boot-briefing-builder.test.ts +604 -0
  39. package/telegram-plugin/tests/buzz-mirror.test.ts +242 -0
  40. package/telegram-plugin/tests/buzz-origin-stamp-gate.test.ts +159 -0
  41. package/telegram-plugin/tests/channel-route.test.ts +306 -0
  42. package/telegram-plugin/tests/inbound-spool.test.ts +47 -0
  43. package/telegram-plugin/tests/ipc-server-buzz-dedup.test.ts +124 -0
  44. package/telegram-plugin/tests/ipc-server-buzz-peer.test.ts +269 -0
  45. package/telegram-plugin/tests/operator-events-session-tail.test.ts +63 -0
  46. package/telegram-plugin/tests/operator-events.test.ts +71 -7
  47. package/telegram-plugin/tests/user-failure-notices.test.ts +165 -0
  48. package/telegram-plugin/voice-normalize-text.ts +5 -0
  49. package/vendor/hindsight-memory/scripts/directive_verify.py +4 -0
  50. package/vendor/hindsight-memory/scripts/recall.py +7 -2
@@ -0,0 +1,242 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import {
3
+ initBuzzMirror,
4
+ getBuzzMirror,
5
+ maybeBootBuzzMirror,
6
+ __resetBuzzMirrorForTests,
7
+ CORRECTION_DEBOUNCE_MS,
8
+ } from '../gateway/buzz-mirror.js'
9
+ import type { OutboundToBuzzMessage } from '../gateway/ipc-protocol.js'
10
+
11
+ // Hub-side mirror behaviour (Phase 2b). The mirror is DOWNSTREAM of a delivered
12
+ // Telegram copy; a publish is emitted via the attached peer sender. These tests
13
+ // spy the sender to assert WHETHER and WITH WHAT a publish is issued.
14
+
15
+ function mirrorWith(sender: (m: OutboundToBuzzMessage) => boolean, opts?: { defaultChannelId?: string }) {
16
+ const m = initBuzzMirror({
17
+ mode: 'both',
18
+ agentName: 'klanker',
19
+ defaultChannelId: opts?.defaultChannelId ?? 'default-chan',
20
+ })
21
+ m.attachSender(sender)
22
+ return m
23
+ }
24
+
25
+ const BUZZ_COORDS = { channelId: 'chan-A', eventId: 'evt-1', threadRoot: 'root-1' }
26
+
27
+ describe('BuzzMirror.mirrorReplyDelivered — routing + S1 owner guard', () => {
28
+ beforeEach(() => __resetBuzzMirrorForTests())
29
+
30
+ it('T-6b (hub): a live buzz owner + un-echoed reply + a recent different-origin turn ⇒ NO publish', () => {
31
+ const sender = vi.fn(() => true)
32
+ const m = mirrorWith(sender)
33
+ m.mirrorReplyDelivered({
34
+ scrubbedText: 'answer',
35
+ ownerOriginChannel: 'buzz',
36
+ ownerBuzzCoords: BUZZ_COORDS,
37
+ ownerEchoed: false, // the reply did NOT echo this buzz turn's id
38
+ hasRecentDifferentOriginTurn: true, // a prior Telegram DM turn is live/recent
39
+ telegramMessageKeys: ['555:1001'],
40
+ })
41
+ // The S1 guard must refuse the ambiguous threaded publish — Telegram-only.
42
+ expect(sender).not.toHaveBeenCalled()
43
+ })
44
+
45
+ it('publishes a THREADED reply when the buzz owner id was echoed', () => {
46
+ const sender = vi.fn(() => true)
47
+ const m = mirrorWith(sender)
48
+ m.mirrorReplyDelivered({
49
+ scrubbedText: 'answer',
50
+ ownerOriginChannel: 'buzz',
51
+ ownerBuzzCoords: BUZZ_COORDS,
52
+ ownerEchoed: true,
53
+ hasRecentDifferentOriginTurn: true, // irrelevant once echoed
54
+ telegramMessageKeys: ['555:1001'],
55
+ })
56
+ expect(sender).toHaveBeenCalledTimes(1)
57
+ const msg = sender.mock.calls[0][0]
58
+ expect(msg.type).toBe('outbound_to_buzz')
59
+ expect(msg.channelId).toBe(BUZZ_COORDS.channelId)
60
+ expect(msg.replyToEventId).toBe(BUZZ_COORDS.eventId)
61
+ expect(msg.threadRootId).toBe(BUZZ_COORDS.threadRoot)
62
+ expect(msg.payload).toEqual({ kind: 'message', text: 'answer' })
63
+ expect(msg.agentName).toBe('klanker')
64
+ })
65
+
66
+ it('publishes a buzz-origin threaded reply when un-echoed but NO different-origin turn exists', () => {
67
+ const sender = vi.fn(() => true)
68
+ const m = mirrorWith(sender)
69
+ m.mirrorReplyDelivered({
70
+ scrubbedText: 'answer',
71
+ ownerOriginChannel: 'buzz',
72
+ ownerBuzzCoords: BUZZ_COORDS,
73
+ ownerEchoed: false,
74
+ hasRecentDifferentOriginTurn: false,
75
+ telegramMessageKeys: ['555:1001'],
76
+ })
77
+ expect(sender).toHaveBeenCalledTimes(1)
78
+ })
79
+
80
+ it('mirrors a TELEGRAM-origin answer as a fresh top-level post to defaultChannelId', () => {
81
+ const sender = vi.fn(() => true)
82
+ const m = mirrorWith(sender, { defaultChannelId: 'grp-top' })
83
+ m.mirrorReplyDelivered({
84
+ scrubbedText: 'tele answer',
85
+ ownerOriginChannel: 'telegram',
86
+ ownerEchoed: false,
87
+ hasRecentDifferentOriginTurn: false,
88
+ telegramMessageKeys: ['555:2002'],
89
+ })
90
+ expect(sender).toHaveBeenCalledTimes(1)
91
+ const msg = sender.mock.calls[0][0]
92
+ expect(msg.channelId).toBe('grp-top')
93
+ expect(msg.replyToEventId).toBeUndefined() // top-level, not a thread reply
94
+ expect(msg.threadRootId).toBeUndefined()
95
+ })
96
+
97
+ it('drops a telegram-origin mirror when no default channel is configured', () => {
98
+ const sender = vi.fn(() => true)
99
+ const m = mirrorWith(sender, { defaultChannelId: '' })
100
+ m.mirrorReplyDelivered({
101
+ scrubbedText: 'x',
102
+ ownerOriginChannel: 'telegram',
103
+ ownerEchoed: false,
104
+ hasRecentDifferentOriginTurn: false,
105
+ telegramMessageKeys: ['555:3003'],
106
+ })
107
+ expect(sender).not.toHaveBeenCalled()
108
+ })
109
+
110
+ it('never throws even if the sender throws (the Telegram copy is already delivered)', () => {
111
+ const sender = vi.fn(() => { throw new Error('peer exploded') })
112
+ const m = mirrorWith(sender)
113
+ expect(() =>
114
+ m.mirrorReplyDelivered({
115
+ scrubbedText: 'x',
116
+ ownerOriginChannel: 'telegram',
117
+ ownerEchoed: false,
118
+ hasRecentDifferentOriginTurn: false,
119
+ telegramMessageKeys: ['555:4004'],
120
+ }),
121
+ ).not.toThrow()
122
+ })
123
+ })
124
+
125
+ describe('BuzzMirror.mirrorCorrection — debounced, only for mirrored messages', () => {
126
+ beforeEach(() => {
127
+ __resetBuzzMirrorForTests()
128
+ vi.useFakeTimers()
129
+ })
130
+ // restore real timers after each via afterEach-less pattern
131
+ it('no-ops for a Telegram message that was never mirrored to Buzz', () => {
132
+ const sender = vi.fn(() => true)
133
+ const m = mirrorWith(sender)
134
+ m.mirrorCorrection({ telegramMessageKey: '555:9999', scrubbedText: 'fixed' })
135
+ vi.advanceTimersByTime(CORRECTION_DEBOUNCE_MS + 1000)
136
+ expect(sender).not.toHaveBeenCalled()
137
+ vi.useRealTimers()
138
+ })
139
+
140
+ it('publishes a debounced correction for a message recorded via onPublishResult', () => {
141
+ const sender = vi.fn(() => true)
142
+ const m = mirrorWith(sender)
143
+ // First: a delivered buzz publish, then its OK result records the mapping.
144
+ m.mirrorReplyDelivered({
145
+ scrubbedText: 'answer',
146
+ ownerOriginChannel: 'buzz',
147
+ ownerBuzzCoords: BUZZ_COORDS,
148
+ ownerEchoed: true,
149
+ hasRecentDifferentOriginTurn: false,
150
+ telegramMessageKeys: ['555:1001'],
151
+ })
152
+ const correlationId = sender.mock.calls[0][0].correlationId
153
+ m.onPublishResult({ correlationId, ok: true, eventId: 'published-evt' })
154
+ sender.mockClear()
155
+
156
+ m.mirrorCorrection({ telegramMessageKey: '555:1001', scrubbedText: 'corrected' })
157
+ // Debounced: nothing before the window elapses.
158
+ vi.advanceTimersByTime(CORRECTION_DEBOUNCE_MS - 10)
159
+ expect(sender).not.toHaveBeenCalled()
160
+ vi.advanceTimersByTime(20)
161
+ expect(sender).toHaveBeenCalledTimes(1)
162
+ const corr = sender.mock.calls[0][0]
163
+ expect(corr.payload).toEqual({ kind: 'correction', text: 'corrected', targetEventId: 'published-evt' })
164
+ vi.useRealTimers()
165
+ })
166
+
167
+ it('S4/F6: two rapid edits COALESCE to a SINGLE published correction (last-write-wins)', () => {
168
+ const sender = vi.fn(() => true)
169
+ const m = mirrorWith(sender)
170
+ // Record a mirrored message so there is a Buzz event to correct.
171
+ m.mirrorReplyDelivered({
172
+ scrubbedText: 'answer',
173
+ ownerOriginChannel: 'buzz',
174
+ ownerBuzzCoords: BUZZ_COORDS,
175
+ ownerEchoed: true,
176
+ hasRecentDifferentOriginTurn: false,
177
+ telegramMessageKeys: ['555:1001'],
178
+ })
179
+ const correlationId = sender.mock.calls[0][0].correlationId
180
+ m.onPublishResult({ correlationId, ok: true, eventId: 'published-evt' })
181
+ sender.mockClear()
182
+
183
+ // Edit #1 at t=0 arms a timer for t=DEBOUNCE. Edit #2 arrives INSIDE the
184
+ // window and must CLEAR edit #1's timer (buzz-mirror.ts:192-193) so only the
185
+ // latest text is ever published, and only once.
186
+ m.mirrorCorrection({ telegramMessageKey: '555:1001', scrubbedText: 'first edit' })
187
+ vi.advanceTimersByTime(CORRECTION_DEBOUNCE_MS - 5_000) // still inside the window
188
+ m.mirrorCorrection({ telegramMessageKey: '555:1001', scrubbedText: 'second edit' })
189
+
190
+ // Step PAST edit #1's original deadline. If coalescing were broken, edit #1's
191
+ // timer would fire here — it must NOT (it was cleared).
192
+ vi.advanceTimersByTime(6_000)
193
+ expect(sender).not.toHaveBeenCalled()
194
+
195
+ // Reach edit #2's deadline: EXACTLY ONE correction, carrying the LATEST text.
196
+ vi.advanceTimersByTime(CORRECTION_DEBOUNCE_MS)
197
+ expect(sender).toHaveBeenCalledTimes(1)
198
+ expect(sender.mock.calls[0][0].payload).toEqual({
199
+ kind: 'correction',
200
+ text: 'second edit',
201
+ targetEventId: 'published-evt',
202
+ })
203
+ vi.useRealTimers()
204
+ })
205
+ })
206
+
207
+ describe('maybeBootBuzzMirror — dark by default (S2)', () => {
208
+ beforeEach(() => __resetBuzzMirrorForTests())
209
+
210
+ it('stays dark (returns null, getBuzzMirror null) when BUZZ_ENABLED is unset', () => {
211
+ const booted = maybeBootBuzzMirror(() => true, {})
212
+ expect(booted).toBeNull()
213
+ expect(getBuzzMirror()).toBeNull()
214
+ })
215
+
216
+ it('stays dark when enabled but mode is off/origin (S2 degrade)', () => {
217
+ expect(maybeBootBuzzMirror(() => true, { BUZZ_ENABLED: '1', BUZZ_MIRROR: 'off' })).toBeNull()
218
+ expect(maybeBootBuzzMirror(() => true, { BUZZ_ENABLED: '1', BUZZ_MIRROR: 'origin' })).toBeNull()
219
+ expect(getBuzzMirror()).toBeNull()
220
+ })
221
+
222
+ it('boots when enabled AND mode both, wiring the sender', () => {
223
+ const sender = vi.fn(() => true)
224
+ const booted = maybeBootBuzzMirror(sender, {
225
+ BUZZ_ENABLED: '1',
226
+ BUZZ_MIRROR: 'both',
227
+ SWITCHROOM_AGENT_NAME: 'klanker',
228
+ BUZZ_CHANNEL_IDS: 'grp-top',
229
+ })
230
+ expect(booted).not.toBeNull()
231
+ expect(getBuzzMirror()).toBe(booted)
232
+ // Sender is wired: a telegram-origin mirror flows to it.
233
+ booted!.mirrorReplyDelivered({
234
+ scrubbedText: 'x',
235
+ ownerOriginChannel: 'telegram',
236
+ ownerEchoed: false,
237
+ hasRecentDifferentOriginTurn: false,
238
+ telegramMessageKeys: ['555:1'],
239
+ })
240
+ expect(sender).toHaveBeenCalledTimes(1)
241
+ })
242
+ })
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { tmpdir } from 'node:os'
3
+ import {
4
+ handleSessionEvent,
5
+ type StreamRenderDeps,
6
+ } from '../gateway/stream-render.js'
7
+ import { OutboundDedupCache } from '../recent-outbound-dedup.js'
8
+ import { FlushedTurnSupersedeRegistry } from '../flushed-turn-supersede.js'
9
+ import { BackstopDeliveryLedger } from '../gateway/backstop-delivery.js'
10
+ import { redact } from '../secret-detect/redact.js'
11
+ import type { CurrentTurn } from '../gateway/gateway.js'
12
+
13
+ /**
14
+ * Buzz co-channel — Phase 2a MINOR-1 gate guard (behavioural, non-mocked).
15
+ *
16
+ * `stream-render.ts` computes `BUZZ_ORIGIN_STAMP_ACTIVE` at import time from
17
+ * `BUZZ_ENABLED` + the routing kill switch; when it is false the turn ctor
18
+ * stamps a plain Telegram origin WITHOUT calling `parseChannelOrigin`, so the
19
+ * Telegram-only hot path is byte-identical.
20
+ *
21
+ * This runs with `BUZZ_ENABLED` UNSET (the default for every Telegram-only
22
+ * agent), i.e. the gate is OFF. The OUTCOME it pins: a turn whose rawContent
23
+ * carries a REAL buzz `<channel source="buzz" …>` envelope is STILL stamped
24
+ * `telegram` with no coords — proving the parser was not consulted. If someone
25
+ * deleted the gate, the parser would run on this same input and stamp
26
+ * `originChannel: 'buzz'` with coords, and this test would fail. That makes it a
27
+ * true regression guard for the flag-off invariant, not a code-path assertion.
28
+ */
29
+
30
+ // A real captured buzz double-wrap envelope (same fixture as channel-route T-1c).
31
+ const CHAN = '6d18fdfe-601b-4e6c-82b5-aed8ac002dd4'
32
+ const EVT = '5e472ba250c45ea6f609f71d05e979a6992670ab12fd07781096bdcee6458c6b'
33
+ const PUB = 'fc97c126b783147458e8ea640cd714af5f2a2dd1dc39b27afc3b013df24faf1b'
34
+ const BUZZ_RAW =
35
+ `<channel source="switchroom-telegram" source="buzz" buzz_channel_id="${CHAN}" ` +
36
+ `buzz_event_id="${EVT}" buzz_pubkey="${PUB}" buzz_thread_root="${EVT}" user="buzz:fc97…af1b">\n` +
37
+ `<channel source="buzz" buzz_channel_id="${CHAN}" buzz_event_id="${EVT}" buzz_pubkey="${PUB}" ` +
38
+ `buzz_thread_root="${EVT}" user="buzz:fc97…af1b">[canary] inbound-path test</channel>\n</channel>`
39
+
40
+ const CHAT = '1001'
41
+
42
+ function makeStreamDeps(): { deps: StreamRenderDeps; getTurn: () => CurrentTurn | null } {
43
+ let curTurn: CurrentTurn | null = null
44
+ const key = (c: string, t?: number | null) => `${c}:${t ?? 'main'}`
45
+ const noop = () => {}
46
+ const fakeEA = {
47
+ claimOrDowngradePing: (_i: unknown, _s: unknown, _a: unknown, disabled: () => void) => disabled(),
48
+ markSubstantiveFinalDelivered: (fn: () => void) => fn(),
49
+ finalizeCard: (fn: () => void) => fn(),
50
+ }
51
+ const deps = {
52
+ ANSWER_LANE: { visibleEnabled: false } as unknown,
53
+ CAPTURED_PROSE_DELIVERY_ENABLED: false,
54
+ CONTEXT_EXHAUSTION_COOLDOWN_MS: 600000,
55
+ DELIVERY_CONFIRM_ENABLED: false,
56
+ FEED_REOPEN_AFTER_ACK_ENABLED: false,
57
+ HANDBACK_PRETURN_ENABLED: false,
58
+ HISTORY_ENABLED: false,
59
+ LIVENESS_TERMINAL_HONESTY: true,
60
+ OBLIGATION_LEDGER_ENABLED: false,
61
+ ORPHANED_REPLY_STREAM_WINDOW_MS: 120000,
62
+ SILENCE_LIVENESS_PRODUCTION: false,
63
+ STATE_DIR: tmpdir(),
64
+ TURN_FLUSH_SAFETY_ENABLED: true,
65
+ TURN_PREVIEW_MAX: 200,
66
+ activeDraftStreams: new Map(),
67
+ activeStatusReactions: new Map(),
68
+ activeTurnStartedAt: new Map(),
69
+ backstopDeliveryLedger: new BackstopDeliveryLedger(),
70
+ bot: { api: {} },
71
+ flushedTurnSupersede: new FlushedTurnSupersedeRegistry(),
72
+ idleTracker: { noteEvent: noop },
73
+ lastPtyPreviewByChat: new Map(),
74
+ obligationLedger: { close: noop, noteTurnEnded: noop },
75
+ outboundDedup: new OutboundDedupCache(),
76
+ pendingCrossTurnGate: new Map(),
77
+ preambleSuppressor: { dropNow: noop, flushNow: noop, onText: noop, onTool: noop, reset: noop },
78
+ progressDriver: null,
79
+ reactionTransitionCounts: new Map(),
80
+ suppressPtyPreview: new Set(),
81
+ toolFlightTracker: { inFlightCount: () => 0 },
82
+ deliveryQueue: {},
83
+ handbackPreturnSignal: { tryAdopt: () => null },
84
+ sessionModelSource: { noteTranscriptModel: noop },
85
+ typingWrapper: { drainAll: noop, onToolResult: noop, onToolUse: noop },
86
+ robustApiCall: (fn: () => Promise<unknown>) => fn(),
87
+ swallowingApiCall: async (fn: () => Promise<unknown>) => { try { return await fn() } catch { return undefined } },
88
+ getCurrentTurn: () => curTurn,
89
+ getLastContextExhaustionWarningAt: () => 0,
90
+ setLastContextExhaustionWarningAt: noop,
91
+ getPendingPtyPartial: () => null,
92
+ setPendingPtyPartial: noop,
93
+ setCurrentTurn: (t: CurrentTurn) => { curTurn = t },
94
+ cardDrainGate: (_t: unknown, _ea: unknown, run: () => void) => run(),
95
+ clearActivitySummary: noop,
96
+ clearAnswerReadyFlushTimeout: noop,
97
+ closeActivityLane: noop,
98
+ closeProgressLane: noop,
99
+ completeProgressCardTurn: null,
100
+ composeTurnActivity: () => null,
101
+ confirmMemoryLegibility: noop,
102
+ deliverAnswer: async () => ({ sentIds: [4242], chunkCount: 1, delivered: true, exhausted: false }),
103
+ deliverCapturedProse: async () => {},
104
+ drainActivitySummary: async () => {},
105
+ emissionAuthorityFor: () => fakeEA,
106
+ emitTurnRecord: noop,
107
+ endCurrentTurnAtomic: () => null,
108
+ extractUserPromptPreview: () => null,
109
+ finalizeStatusReaction: noop,
110
+ flushPendingNarrativeAtTurnEnd: noop,
111
+ getPinnedProgressCardMessageId: null,
112
+ handlePtyPartial: noop,
113
+ isDmChatId: () => true,
114
+ isLegitimatelyWorking: () => false,
115
+ makeNarrativeGate: () => ({ show: noop, stage: noop, resolveOnTool: noop, flushAtTurnEnd: noop, teardown: noop }),
116
+ promoteQueuedStatus: noop,
117
+ purgeReactionTracking: noop,
118
+ redactOutboundText: (t: string) => redact(t),
119
+ rememberRecentTurn: noop,
120
+ resetAnswerReadyFlushTimeout: noop,
121
+ resetOrphanedReplyTimeout: noop,
122
+ resolvePendingNarrativeOnTool: noop,
123
+ scheduleEarlyLivenessOpen: noop,
124
+ stagePendingNarrative: noop,
125
+ startTurnTypingLoop: noop,
126
+ statusKey: key,
127
+ streamKey: key,
128
+ surfaceMemoryLegibility: noop,
129
+ turnLiveForItsTopic: () => true,
130
+ turnsDb: null,
131
+ unpinProgressCardForChat: null,
132
+ } as unknown as StreamRenderDeps
133
+ return { deps, getTurn: () => curTurn }
134
+ }
135
+
136
+ describe('MINOR-1 — origin stamp gate OFF (default): parser is not consulted', () => {
137
+ it('stamps a buzz-enveloped turn as TELEGRAM when BUZZ_ENABLED is unset', () => {
138
+ // Guard: the process must actually be on the flag-off path for this to mean
139
+ // what it claims. (Every Telegram-only agent runs exactly here.)
140
+ expect(process.env.BUZZ_ENABLED === '1' || process.env.BUZZ_ENABLED === 'true').toBe(false)
141
+
142
+ const { deps, getTurn } = makeStreamDeps()
143
+ handleSessionEvent(deps, {
144
+ kind: 'enqueue',
145
+ chatId: CHAT,
146
+ messageId: null,
147
+ threadId: null,
148
+ rawContent: BUZZ_RAW,
149
+ } as unknown as Parameters<typeof handleSessionEvent>[1])
150
+
151
+ const turn = getTurn()
152
+ expect(turn).not.toBeNull()
153
+ // The OUTCOME: despite a real buzz envelope, the gate-off ctor defaulted to
154
+ // telegram WITHOUT calling parseChannelOrigin. Removing the gate would make
155
+ // this 'buzz' with coords — the regression this test exists to catch.
156
+ expect(turn!.originChannel).toBe('telegram')
157
+ expect(turn!.buzzCoords).toBeUndefined()
158
+ })
159
+ })