switchroom 0.18.30 → 0.18.32

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 (67) hide show
  1. package/dist/agent-scheduler/index.js +4 -2
  2. package/dist/auth-broker/index.js +4 -2
  3. package/dist/cli/notion-write-pretool.mjs +4 -2
  4. package/dist/cli/switchroom.js +708 -255
  5. package/dist/host-control/main.js +5 -3
  6. package/dist/vault/approvals/kernel-server.js +4 -2
  7. package/dist/vault/broker/server.js +4 -2
  8. package/package.json +3 -2
  9. package/profiles/_base/start.sh.hbs +142 -7
  10. package/telegram-plugin/dist/gateway/gateway.js +25870 -25007
  11. package/telegram-plugin/gateway/backstop-delivery.ts +223 -23
  12. package/telegram-plugin/gateway/captured-answer-resume.ts +259 -0
  13. package/telegram-plugin/gateway/disconnect-flush.ts +6 -44
  14. package/telegram-plugin/gateway/gateway-import-clean.test.ts +188 -0
  15. package/telegram-plugin/gateway/gateway.ts +5479 -7069
  16. package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +7 -15
  17. package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +35 -68
  18. package/telegram-plugin/gateway/obligation-ledger.ts +42 -0
  19. package/telegram-plugin/gateway/obligation-store.ts +37 -1
  20. package/telegram-plugin/gateway/outbound-send-path.ts +2012 -0
  21. package/telegram-plugin/gateway/turn-flush-suppression.ts +82 -0
  22. package/telegram-plugin/pending-user-notice.ts +59 -13
  23. package/telegram-plugin/subagent-watcher.ts +111 -28
  24. package/telegram-plugin/tests/backstop-delivery.test.ts +167 -0
  25. package/telegram-plugin/tests/backstop-readback-probe.test.ts +144 -0
  26. package/telegram-plugin/tests/buffer-gate-broadened.test.ts +16 -6
  27. package/telegram-plugin/tests/button-tap-turn-gated.test.ts +3 -3
  28. package/telegram-plugin/tests/captured-answer-resume.test.ts +358 -0
  29. package/telegram-plugin/tests/emission-authority-facade.test.ts +29 -19
  30. package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +4 -1
  31. package/telegram-plugin/tests/emission-determinism-wiring.test.ts +18 -7
  32. package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +249 -0
  33. package/telegram-plugin/tests/gateway-bot-construction-deferral.test.ts +251 -0
  34. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -128
  35. package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +303 -0
  36. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +10 -3
  37. package/telegram-plugin/tests/inbound-delivery-cutover-flip.test.ts +54 -150
  38. package/telegram-plugin/tests/inbound-delivery-cutover-gate.test.ts +10 -14
  39. package/telegram-plugin/tests/inbound-delivery-dispatch-equivalence.test.ts +6 -7
  40. package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +0 -16
  41. package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +4 -4
  42. package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +69 -14
  43. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +9 -3
  44. package/telegram-plugin/tests/obligation-ledger.test.ts +40 -0
  45. package/telegram-plugin/tests/obligation-store.test.ts +43 -0
  46. package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +2 -1
  47. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +9 -6
  48. package/telegram-plugin/tests/photo-reroute-wiring.test.ts +5 -2
  49. package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -2
  50. package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +3 -2
  51. package/telegram-plugin/tests/send-reply-golden.test.ts +571 -0
  52. package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +291 -0
  53. package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +32 -0
  54. package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +8 -12
  55. package/telegram-plugin/tests/turn-flush-safety.test.ts +8 -6
  56. package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +112 -0
  57. package/telegram-plugin/tests/turn-flush-suppression.test.ts +90 -0
  58. package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +2 -2
  59. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +126 -0
  60. package/telegram-plugin/tool-activity-summary.ts +27 -3
  61. package/telegram-plugin/worker-activity-feed.ts +24 -0
  62. package/telegram-plugin/gateway/busy-key-reaper.ts +0 -113
  63. package/telegram-plugin/gateway/gate-parity-probe.ts +0 -102
  64. package/telegram-plugin/tests/busy-key-reaper.test.ts +0 -192
  65. package/telegram-plugin/tests/fixtures/cutover-killswitch-probe.ts +0 -75
  66. package/telegram-plugin/tests/gate-parity-probe.test.ts +0 -171
  67. package/telegram-plugin/tests/parallel-turns-deadlock-fix.test.ts +0 -217
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { readFileSync } from 'node:fs'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { dirname, join } from 'node:path'
5
+
6
+ /**
7
+ * #2996 P0e — gateway.ts import-cleanliness.
8
+ *
9
+ * Prior phases (P0a/P0b/P0c) could only pin import-cleanliness with AST/source
10
+ * checks because importing gateway.ts under vitest was *impossible*: the
11
+ * module-scope `createIpcServer(...)` calls `Bun.listen` at construction, and
12
+ * `Bun` is undefined under Node/vitest, so a bare `import('./gateway.js')`
13
+ * threw `ReferenceError: Bun is not defined` before any assertion could run.
14
+ *
15
+ * P0e removes the last import-time side effects. This test therefore uses the
16
+ * thing prior phases could not: an ACTUAL runtime import of gateway.ts as the
17
+ * oracle for the import-clean half, and deterministic source-pins for the
18
+ * boot-fires-under-`isGatewayMain` half (which cannot be runtime-exercised
19
+ * under vitest — the prod boot path needs Bun, a real Bot token, and the
20
+ * vault, none of which exist here).
21
+ */
22
+
23
+ const here = dirname(fileURLToPath(import.meta.url))
24
+ const GATEWAY_SRC = readFileSync(join(here, 'gateway.ts'), 'utf8')
25
+
26
+ // The process-signal / lifecycle events gateway.ts wires on the prod boot path.
27
+ // Under a vitest import (isGatewayMain === false) it must register NONE of them.
28
+ const GATEWAY_SIGNALS = ['SIGTERM', 'SIGINT', 'beforeExit'] as const
29
+
30
+ function signalListenerCounts(): Record<string, number> {
31
+ const out: Record<string, number> = {}
32
+ for (const sig of GATEWAY_SIGNALS) out[sig] = process.listenerCount(sig)
33
+ return out
34
+ }
35
+
36
+ describe('gateway.ts import-cleanliness (#2996 P0e)', () => {
37
+ it('runtime-imports with zero side effects: resolves, no process.exit, no gateway signal handlers', async () => {
38
+ const before = signalListenerCounts()
39
+
40
+ // Any reachable process.exit during import is a hard failure ("no exit
41
+ // reachable on import"). Throw instead of exiting so the harness survives
42
+ // and the assertion is legible.
43
+ const exitSpy = vi
44
+ .spyOn(process, 'exit')
45
+ .mockImplementation(((code?: number) => {
46
+ throw new Error(`process.exit(${code}) was reached during import()`)
47
+ }) as never)
48
+
49
+ // Positive oracle that no UDS server is bound at import: if the gate
50
+ // regressed and `createIpcServer` ran, it would call this Bun.listen fake.
51
+ // With the gate intact it is never invoked. (Also proves the import no
52
+ // longer depends on a real Bun global — it resolves whether or not one
53
+ // exists.)
54
+ //
55
+ // Dual-runner note: this file runs under BOTH vitest (Node) and `bun test`
56
+ // (the plugin bun suite globs every *.test.ts). Under Node `globalThis.Bun`
57
+ // is undefined and the fake installs cleanly — that run holds the no-bind
58
+ // oracle. Under the Bun runtime the `Bun` global is READONLY (assignment
59
+ // throws "Attempted to assign to readonly property"), so the swap is
60
+ // best-effort there; the bind oracle is then carried by the vitest run and
61
+ // the source pins below, while the remaining outcome assertions (resolve /
62
+ // no exit / no listeners) still run in both runtimes.
63
+ const listenSpy = vi.fn(() => {
64
+ throw new Error('Bun.listen was called during import() — an IPC server was bound')
65
+ })
66
+ const priorBun = (globalThis as { Bun?: unknown }).Bun
67
+ let bunSwapped = false
68
+ try {
69
+ ;(globalThis as { Bun?: unknown }).Bun = { listen: listenSpy }
70
+ bunSwapped = true
71
+ } catch {
72
+ // Bun runtime — readonly global; see dual-runner note above.
73
+ }
74
+
75
+ let mod: Record<string, unknown>
76
+ try {
77
+ mod = (await import('./gateway.js')) as Record<string, unknown>
78
+ } finally {
79
+ if (bunSwapped) {
80
+ if (priorBun === undefined) delete (globalThis as { Bun?: unknown }).Bun
81
+ else (globalThis as { Bun?: unknown }).Bun = priorBun
82
+ }
83
+ exitSpy.mockRestore()
84
+ }
85
+
86
+ // Import resolved (did not throw). Under P0c-and-earlier this line was
87
+ // unreachable — the import rejected with `Bun is not defined`.
88
+ expect(mod).toBeTruthy()
89
+
90
+ // No IPC server bound (Node run; under Bun the swap is best-effort and the
91
+ // spy simply stays uninstalled + uncalled).
92
+ expect(listenSpy).not.toHaveBeenCalled()
93
+
94
+ // No exit reachable on import.
95
+ expect(exitSpy).not.toHaveBeenCalled()
96
+
97
+ // No gateway-owned SIGTERM / SIGINT / beforeExit handlers registered.
98
+ // (vitest owns a baseline uncaughtException/unhandledRejection listener of
99
+ // its own, so those two are asserted via the delta only.)
100
+ const after = signalListenerCounts()
101
+ for (const sig of GATEWAY_SIGNALS) {
102
+ // Runner-agnostic single-arg expect (bun's expect has no message arg):
103
+ // encode the signal name in the compared value so a failure is legible.
104
+ expect(`${sig}:+${after[sig] - before[sig]}`).toBe(`${sig}:+0`)
105
+ }
106
+ }, 60_000)
107
+
108
+ it('does not add uncaughtException / unhandledRejection handlers on import', async () => {
109
+ // Distinct from the signal test: gateway.ts registers TWO families of these
110
+ // (installGlobalErrorHandlers @ ~910 and its own shutdown-routing handlers
111
+ // @ ~29926/29935). Both call process.exit, both are gated on P0e. Assert the
112
+ // delta the import contributed is zero (vitest's own baseline is subtracted).
113
+ const before = {
114
+ uncaughtException: process.listenerCount('uncaughtException'),
115
+ unhandledRejection: process.listenerCount('unhandledRejection'),
116
+ }
117
+ const mod = await import('./gateway.js')
118
+ expect(mod).toBeTruthy()
119
+ const after = {
120
+ uncaughtException: process.listenerCount('uncaughtException'),
121
+ unhandledRejection: process.listenerCount('unhandledRejection'),
122
+ }
123
+ expect(after.uncaughtException - before.uncaughtException).toBe(0)
124
+ expect(after.unhandledRejection - before.unhandledRejection).toBe(0)
125
+ }, 60_000)
126
+
127
+ describe('boot wiring is gated on isGatewayMain (source pins — the prod path still fires)', () => {
128
+ // These pin, deterministically, that each side-effecting construct remains
129
+ // (a) gated so the import stays clean, and (b) present so the prod boot path
130
+ // (isGatewayMain === true) still constructs it. They guard against a future
131
+ // edit silently ungating one of them (re-inflating the import surface) OR
132
+ // deleting the construction (breaking boot).
133
+
134
+ it('IPC server construction is deferred behind isGatewayMain', () => {
135
+ expect(GATEWAY_SRC).toMatch(/let ipcServer!: IpcServer/)
136
+ expect(GATEWAY_SRC).toMatch(/if \(isGatewayMain\) ipcServer = createIpcServer\(\{/)
137
+ // No ungated `const ipcServer = createIpcServer` remains.
138
+ expect(GATEWAY_SRC).not.toMatch(/const ipcServer[^\n]*= createIpcServer/)
139
+ })
140
+
141
+ it('inbound spool construction is deferred behind isGatewayMain', () => {
142
+ expect(GATEWAY_SRC).toMatch(
143
+ /let inboundSpool: ReturnType<typeof createInboundSpool> \| undefined/,
144
+ )
145
+ expect(GATEWAY_SRC).toMatch(/if \(isGatewayMain\) inboundSpool = STATIC/)
146
+ expect(GATEWAY_SRC).not.toMatch(/const inboundSpool = STATIC/)
147
+ })
148
+
149
+ it('global error handlers + beforeExit are gated on isGatewayMain', () => {
150
+ // installPosthogErrorHandlers must not sit ungated at module scope.
151
+ expect(GATEWAY_SRC).not.toMatch(/^installPosthogErrorHandlers\(\)/m)
152
+ expect(GATEWAY_SRC).toMatch(
153
+ /if \(isGatewayMain\) \{\n {2}installPosthogErrorHandlers\(\)/,
154
+ )
155
+ })
156
+
157
+ it('signal + crash handlers are gated on isGatewayMain', () => {
158
+ // No ungated (column-0) process.on wiring survives.
159
+ expect(GATEWAY_SRC).not.toMatch(/^process\.on\('SIGTERM'/m)
160
+ expect(GATEWAY_SRC).not.toMatch(/^process\.on\('SIGINT'/m)
161
+ expect(GATEWAY_SRC).not.toMatch(/^process\.on\('unhandledRejection'/m)
162
+ expect(GATEWAY_SRC).not.toMatch(/^process\.on\('uncaughtException'/m)
163
+ // The gateway's beforeExit analytics flush is likewise not at column 0.
164
+ expect(GATEWAY_SRC).not.toMatch(/^process\.on\('beforeExit'/m)
165
+ })
166
+
167
+ it('startup reaction sweep (module-load IO) is gated on isGatewayMain', () => {
168
+ expect(GATEWAY_SRC).toMatch(
169
+ /if \(isGatewayMain\) \{\n {2}const startupAgentDir = resolveAgentDirFromEnv\(\)/,
170
+ )
171
+ })
172
+
173
+ it('preserves prod construction order: inboundSpool before pendingInboundBuffer before ipcServer', () => {
174
+ const iSpool = GATEWAY_SRC.indexOf('if (isGatewayMain) inboundSpool = STATIC')
175
+ const iPending = GATEWAY_SRC.indexOf('const pendingInboundBuffer = createPendingInboundBuffer(')
176
+ const iIpc = GATEWAY_SRC.indexOf('if (isGatewayMain) ipcServer = createIpcServer({')
177
+ expect(iSpool).toBeGreaterThan(0)
178
+ expect(iPending).toBeGreaterThan(0)
179
+ expect(iIpc).toBeGreaterThan(0)
180
+ expect(iSpool).toBeLessThan(iPending)
181
+ expect(iPending).toBeLessThan(iIpc)
182
+ })
183
+
184
+ it('startGateway() remains the single guarded boot invocation', () => {
185
+ expect(GATEWAY_SRC).toMatch(/if \(isGatewayMain\) void startGateway\(\)/)
186
+ })
187
+ })
188
+ })