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.
- package/dist/agent-scheduler/index.js +4 -2
- package/dist/auth-broker/index.js +4 -2
- package/dist/cli/notion-write-pretool.mjs +4 -2
- package/dist/cli/switchroom.js +708 -255
- package/dist/host-control/main.js +5 -3
- package/dist/vault/approvals/kernel-server.js +4 -2
- package/dist/vault/broker/server.js +4 -2
- package/package.json +3 -2
- package/profiles/_base/start.sh.hbs +142 -7
- package/telegram-plugin/dist/gateway/gateway.js +25870 -25007
- package/telegram-plugin/gateway/backstop-delivery.ts +223 -23
- package/telegram-plugin/gateway/captured-answer-resume.ts +259 -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 +5479 -7069
- 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/obligation-ledger.ts +42 -0
- package/telegram-plugin/gateway/obligation-store.ts +37 -1
- package/telegram-plugin/gateway/outbound-send-path.ts +2012 -0
- package/telegram-plugin/gateway/turn-flush-suppression.ts +82 -0
- package/telegram-plugin/pending-user-notice.ts +59 -13
- package/telegram-plugin/subagent-watcher.ts +111 -28
- 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/buffer-gate-broadened.test.ts +16 -6
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +3 -3
- package/telegram-plugin/tests/captured-answer-resume.test.ts +358 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +29 -19
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +4 -1
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +18 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +249 -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 +303 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +10 -3
- 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 +4 -4
- package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +69 -14
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +9 -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 +2 -1
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +9 -6
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +5 -2
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -2
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +3 -2
- package/telegram-plugin/tests/send-reply-golden.test.ts +571 -0
- package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +291 -0
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +32 -0
- package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +8 -12
- package/telegram-plugin/tests/turn-flush-safety.test.ts +8 -6
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +112 -0
- package/telegram-plugin/tests/turn-flush-suppression.test.ts +90 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +2 -2
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +126 -0
- package/telegram-plugin/tool-activity-summary.ts +27 -3
- package/telegram-plugin/worker-activity-feed.ts +24 -0
- 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
|
@@ -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
|
+
})
|