@vellumai/assistant 0.4.16 → 0.4.18

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 (536) hide show
  1. package/Dockerfile +6 -6
  2. package/README.md +1 -2
  3. package/eslint.config.mjs +2 -2
  4. package/package.json +1 -1
  5. package/src/__tests__/access-request-decision.test.ts +128 -120
  6. package/src/__tests__/account-registry.test.ts +121 -110
  7. package/src/__tests__/active-skill-tools.test.ts +200 -172
  8. package/src/__tests__/actor-token-service.test.ts +341 -274
  9. package/src/__tests__/agent-loop-thinking.test.ts +28 -19
  10. package/src/__tests__/agent-loop.test.ts +798 -378
  11. package/src/__tests__/anthropic-provider.test.ts +405 -247
  12. package/src/__tests__/app-builder-tool-scripts.test.ts +97 -97
  13. package/src/__tests__/app-bundler.test.ts +112 -79
  14. package/src/__tests__/app-executors.test.ts +205 -178
  15. package/src/__tests__/app-git-history.test.ts +90 -73
  16. package/src/__tests__/app-git-service.test.ts +67 -53
  17. package/src/__tests__/app-open-proxy.test.ts +29 -25
  18. package/src/__tests__/approval-conversation-turn.test.ts +100 -81
  19. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +45 -17
  20. package/src/__tests__/approval-message-composer.test.ts +119 -119
  21. package/src/__tests__/approval-primitive.test.ts +264 -233
  22. package/src/__tests__/approval-routes-http.test.ts +4 -3
  23. package/src/__tests__/asset-materialize-tool.test.ts +250 -178
  24. package/src/__tests__/asset-search-tool.test.ts +251 -191
  25. package/src/__tests__/assistant-attachment-directive.test.ts +187 -142
  26. package/src/__tests__/assistant-attachments.test.ts +254 -186
  27. package/src/__tests__/assistant-event-hub.test.ts +105 -63
  28. package/src/__tests__/assistant-event.test.ts +66 -58
  29. package/src/__tests__/assistant-events-sse-hardening.test.ts +113 -73
  30. package/src/__tests__/assistant-feature-flag-guard.test.ts +78 -52
  31. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +48 -45
  32. package/src/__tests__/assistant-feature-flags-integration.test.ts +118 -77
  33. package/src/__tests__/assistant-id-boundary-guard.test.ts +158 -104
  34. package/src/__tests__/attachments-store.test.ts +240 -183
  35. package/src/__tests__/attachments.test.ts +70 -62
  36. package/src/__tests__/audit-log-rotation.test.ts +50 -35
  37. package/src/__tests__/browser-fill-credential.test.ts +169 -101
  38. package/src/__tests__/browser-manager.test.ts +97 -75
  39. package/src/__tests__/browser-runtime-check.test.ts +16 -15
  40. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +12 -10
  41. package/src/__tests__/browser-skill-endstate.test.ts +97 -72
  42. package/src/__tests__/bundle-scanner.test.ts +47 -22
  43. package/src/__tests__/bundled-asset.test.ts +74 -47
  44. package/src/__tests__/call-constants.test.ts +19 -19
  45. package/src/__tests__/call-controller.test.ts +1073 -751
  46. package/src/__tests__/call-conversation-messages.test.ts +90 -65
  47. package/src/__tests__/call-domain.test.ts +149 -121
  48. package/src/__tests__/call-pointer-message-composer.test.ts +113 -83
  49. package/src/__tests__/call-pointer-messages.test.ts +213 -154
  50. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +9 -10
  51. package/src/__tests__/call-recovery.test.ts +232 -212
  52. package/src/__tests__/call-routes-http.test.ts +328 -279
  53. package/src/__tests__/call-start-guardian-guard.test.ts +32 -30
  54. package/src/__tests__/call-state-machine.test.ts +62 -51
  55. package/src/__tests__/call-state.test.ts +89 -75
  56. package/src/__tests__/call-store.test.ts +387 -316
  57. package/src/__tests__/callback-handoff-copy.test.ts +84 -82
  58. package/src/__tests__/canonical-guardian-store.test.ts +331 -280
  59. package/src/__tests__/channel-approval-routes.test.ts +1643 -1126
  60. package/src/__tests__/channel-approval.test.ts +139 -137
  61. package/src/__tests__/channel-approvals.test.ts +226 -182
  62. package/src/__tests__/channel-delivery-store.test.ts +232 -194
  63. package/src/__tests__/channel-guardian.test.ts +6 -3
  64. package/src/__tests__/channel-invite-transport.test.ts +107 -92
  65. package/src/__tests__/channel-policy.test.ts +42 -38
  66. package/src/__tests__/channel-readiness-service.test.ts +119 -102
  67. package/src/__tests__/channel-reply-delivery.test.ts +147 -118
  68. package/src/__tests__/channel-retry-sweep.test.ts +153 -110
  69. package/src/__tests__/checker.test.ts +3309 -1850
  70. package/src/__tests__/clarification-resolver.test.ts +91 -79
  71. package/src/__tests__/classifier.test.ts +64 -54
  72. package/src/__tests__/claude-code-skill-regression.test.ts +42 -37
  73. package/src/__tests__/claude-code-tool-profiles.test.ts +31 -29
  74. package/src/__tests__/clawhub.test.ts +92 -82
  75. package/src/__tests__/cli.test.ts +30 -30
  76. package/src/__tests__/clipboard.test.ts +53 -46
  77. package/src/__tests__/commit-guarantee.test.ts +59 -52
  78. package/src/__tests__/commit-message-enrichment-service.test.ts +203 -75
  79. package/src/__tests__/compaction.benchmark.test.ts +33 -31
  80. package/src/__tests__/computer-use-session-compaction.test.ts +60 -50
  81. package/src/__tests__/computer-use-session-lifecycle.test.ts +145 -117
  82. package/src/__tests__/computer-use-session-working-dir.test.ts +62 -48
  83. package/src/__tests__/computer-use-skill-baseline.test.ts +22 -19
  84. package/src/__tests__/computer-use-skill-endstate.test.ts +45 -31
  85. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +121 -88
  86. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +65 -42
  87. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +33 -18
  88. package/src/__tests__/computer-use-tools.test.ts +121 -98
  89. package/src/__tests__/config-schema.test.ts +443 -347
  90. package/src/__tests__/config-watcher.test.ts +96 -81
  91. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +148 -133
  92. package/src/__tests__/conflict-intent-tokenization.test.ts +96 -78
  93. package/src/__tests__/conflict-policy.test.ts +151 -80
  94. package/src/__tests__/conflict-store.test.ts +203 -157
  95. package/src/__tests__/connection-policy.test.ts +89 -59
  96. package/src/__tests__/contacts-tools.test.ts +247 -178
  97. package/src/__tests__/context-memory-e2e.test.ts +306 -214
  98. package/src/__tests__/context-token-estimator.test.ts +114 -74
  99. package/src/__tests__/context-window-manager.test.ts +269 -167
  100. package/src/__tests__/contradiction-checker.test.ts +161 -135
  101. package/src/__tests__/conversation-attention-store.test.ts +350 -290
  102. package/src/__tests__/conversation-attention-telegram.test.ts +156 -114
  103. package/src/__tests__/conversation-pairing.test.ts +220 -113
  104. package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
  105. package/src/__tests__/conversation-routes.test.ts +71 -41
  106. package/src/__tests__/conversation-store.test.ts +390 -235
  107. package/src/__tests__/credential-broker-browser-fill.test.ts +325 -250
  108. package/src/__tests__/credential-broker-server-use.test.ts +283 -243
  109. package/src/__tests__/credential-broker.test.ts +128 -74
  110. package/src/__tests__/credential-host-pattern-match.test.ts +64 -44
  111. package/src/__tests__/credential-metadata-store.test.ts +360 -311
  112. package/src/__tests__/credential-policy-validate.test.ts +81 -65
  113. package/src/__tests__/credential-resolve.test.ts +212 -145
  114. package/src/__tests__/credential-security-e2e.test.ts +144 -103
  115. package/src/__tests__/credential-security-invariants.test.ts +253 -208
  116. package/src/__tests__/credential-selection.test.ts +254 -146
  117. package/src/__tests__/credential-vault-unit.test.ts +531 -341
  118. package/src/__tests__/credential-vault.test.ts +761 -484
  119. package/src/__tests__/daemon-assistant-events.test.ts +91 -66
  120. package/src/__tests__/daemon-lifecycle.test.ts +258 -190
  121. package/src/__tests__/daemon-server-session-init.test.ts +257 -191
  122. package/src/__tests__/date-context.test.ts +314 -249
  123. package/src/__tests__/db-migration-rollback.test.ts +259 -130
  124. package/src/__tests__/db-schedule-syntax-migration.test.ts +78 -41
  125. package/src/__tests__/delete-managed-skill-tool.test.ts +77 -53
  126. package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -135
  127. package/src/__tests__/dictation-mode-detection.test.ts +77 -55
  128. package/src/__tests__/dictation-profile-store.test.ts +70 -56
  129. package/src/__tests__/dictation-text-processing.test.ts +53 -35
  130. package/src/__tests__/diff.test.ts +102 -98
  131. package/src/__tests__/domain-normalize.test.ts +54 -54
  132. package/src/__tests__/domain-policy.test.ts +71 -55
  133. package/src/__tests__/dynamic-page-surface.test.ts +31 -33
  134. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +69 -69
  135. package/src/__tests__/edit-engine.test.ts +56 -56
  136. package/src/__tests__/elevenlabs-client.test.ts +117 -91
  137. package/src/__tests__/elevenlabs-config.test.ts +32 -31
  138. package/src/__tests__/email-classifier.test.ts +15 -12
  139. package/src/__tests__/email-cli.test.ts +121 -108
  140. package/src/__tests__/emit-signal-routing-intent.test.ts +76 -69
  141. package/src/__tests__/encrypted-store.test.ts +180 -154
  142. package/src/__tests__/entity-extractor.test.ts +108 -87
  143. package/src/__tests__/entity-search.test.ts +664 -258
  144. package/src/__tests__/ephemeral-permissions.test.ts +224 -188
  145. package/src/__tests__/event-bus.test.ts +81 -77
  146. package/src/__tests__/extract-email.test.ts +51 -0
  147. package/src/__tests__/file-edit-tool.test.ts +62 -44
  148. package/src/__tests__/file-ops-service.test.ts +131 -114
  149. package/src/__tests__/file-read-tool.test.ts +48 -31
  150. package/src/__tests__/file-write-tool.test.ts +43 -37
  151. package/src/__tests__/filesystem-tools.test.ts +238 -209
  152. package/src/__tests__/followup-tools.test.ts +237 -162
  153. package/src/__tests__/forbidden-legacy-symbols.test.ts +19 -20
  154. package/src/__tests__/frontmatter.test.ts +96 -81
  155. package/src/__tests__/fuzzy-match-property.test.ts +75 -81
  156. package/src/__tests__/fuzzy-match.test.ts +71 -65
  157. package/src/__tests__/gateway-client-managed-outbound.test.ts +76 -57
  158. package/src/__tests__/gateway-only-enforcement.test.ts +467 -369
  159. package/src/__tests__/gateway-only-guard.test.ts +54 -56
  160. package/src/__tests__/gemini-image-service.test.ts +113 -100
  161. package/src/__tests__/gemini-provider.test.ts +297 -220
  162. package/src/__tests__/get-weather.test.ts +188 -114
  163. package/src/__tests__/gmail-integration.test.ts +47 -46
  164. package/src/__tests__/guardian-action-conversation-turn.test.ts +226 -171
  165. package/src/__tests__/guardian-action-copy-generator.test.ts +111 -93
  166. package/src/__tests__/guardian-action-followup-executor.test.ts +215 -151
  167. package/src/__tests__/guardian-action-followup-store.test.ts +199 -167
  168. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +297 -250
  169. package/src/__tests__/guardian-action-late-reply.test.ts +462 -316
  170. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +23 -18
  171. package/src/__tests__/guardian-action-store.test.ts +158 -109
  172. package/src/__tests__/guardian-action-sweep.test.ts +114 -100
  173. package/src/__tests__/guardian-actions-endpoint.test.ts +440 -256
  174. package/src/__tests__/guardian-control-plane-policy.test.ts +497 -331
  175. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +217 -215
  176. package/src/__tests__/guardian-dispatch.test.ts +316 -256
  177. package/src/__tests__/guardian-grant-minting.test.ts +247 -178
  178. package/src/__tests__/guardian-outbound-http.test.ts +337 -209
  179. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +99 -96
  180. package/src/__tests__/guardian-question-copy.test.ts +17 -17
  181. package/src/__tests__/guardian-question-mode.test.ts +134 -100
  182. package/src/__tests__/guardian-routing-invariants.test.ts +679 -613
  183. package/src/__tests__/guardian-routing-state.test.ts +256 -209
  184. package/src/__tests__/guardian-verification-intent-routing.test.ts +94 -88
  185. package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -41
  186. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +0 -1
  187. package/src/__tests__/handle-user-message-secret-resume.test.ts +43 -21
  188. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +92 -76
  189. package/src/__tests__/handlers-cu-observation-blob.test.ts +103 -70
  190. package/src/__tests__/handlers-ipc-blob-probe.test.ts +77 -51
  191. package/src/__tests__/handlers-slack-config.test.ts +63 -54
  192. package/src/__tests__/handlers-task-submit-slash.test.ts +18 -18
  193. package/src/__tests__/handlers-telegram-config.test.ts +662 -329
  194. package/src/__tests__/handlers-twitter-config.test.ts +525 -298
  195. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +270 -195
  196. package/src/__tests__/headless-browser-interactions.test.ts +444 -280
  197. package/src/__tests__/headless-browser-navigate.test.ts +116 -79
  198. package/src/__tests__/headless-browser-read-tools.test.ts +123 -86
  199. package/src/__tests__/headless-browser-snapshot.test.ts +71 -56
  200. package/src/__tests__/heartbeat-service.test.ts +76 -58
  201. package/src/__tests__/history-repair-observability.test.ts +14 -14
  202. package/src/__tests__/history-repair.test.ts +171 -167
  203. package/src/__tests__/home-base-bootstrap.test.ts +30 -27
  204. package/src/__tests__/hooks-blocking.test.ts +86 -37
  205. package/src/__tests__/hooks-cli.test.ts +104 -68
  206. package/src/__tests__/hooks-config.test.ts +81 -43
  207. package/src/__tests__/hooks-discovery.test.ts +106 -96
  208. package/src/__tests__/hooks-integration.test.ts +78 -72
  209. package/src/__tests__/hooks-manager.test.ts +99 -61
  210. package/src/__tests__/hooks-runner.test.ts +94 -71
  211. package/src/__tests__/hooks-settings.test.ts +69 -64
  212. package/src/__tests__/hooks-templates.test.ts +85 -54
  213. package/src/__tests__/hooks-ts-runner.test.ts +82 -45
  214. package/src/__tests__/hooks-watch.test.ts +32 -22
  215. package/src/__tests__/host-file-edit-tool.test.ts +190 -148
  216. package/src/__tests__/host-file-read-tool.test.ts +86 -63
  217. package/src/__tests__/host-file-write-tool.test.ts +98 -64
  218. package/src/__tests__/host-shell-tool.test.ts +342 -233
  219. package/src/__tests__/inbound-invite-redemption.test.ts +194 -152
  220. package/src/__tests__/ingress-member-store.test.ts +163 -159
  221. package/src/__tests__/ingress-reconcile.test.ts +183 -142
  222. package/src/__tests__/ingress-routes-http.test.ts +441 -356
  223. package/src/__tests__/ingress-url-consistency.test.ts +125 -64
  224. package/src/__tests__/integration-status.test.ts +93 -73
  225. package/src/__tests__/intent-routing.test.ts +148 -118
  226. package/src/__tests__/invite-redemption-service.test.ts +163 -121
  227. package/src/__tests__/ipc-blob-store.test.ts +104 -91
  228. package/src/__tests__/ipc-contract-inventory.test.ts +27 -15
  229. package/src/__tests__/ipc-contract.test.ts +24 -23
  230. package/src/__tests__/ipc-protocol.test.ts +52 -46
  231. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +61 -50
  232. package/src/__tests__/ipc-snapshot.test.ts +1135 -1056
  233. package/src/__tests__/ipc-validate.test.ts +240 -179
  234. package/src/__tests__/key-migration.test.ts +123 -90
  235. package/src/__tests__/keychain.test.ts +150 -123
  236. package/src/__tests__/lifecycle-docs-guard.test.ts +65 -64
  237. package/src/__tests__/llm-usage-store.test.ts +112 -87
  238. package/src/__tests__/managed-skill-lifecycle.test.ts +147 -108
  239. package/src/__tests__/managed-store.test.ts +411 -360
  240. package/src/__tests__/mcp-cli.test.ts +189 -123
  241. package/src/__tests__/mcp-health-check.test.ts +26 -21
  242. package/src/__tests__/media-generate-image.test.ts +122 -99
  243. package/src/__tests__/media-reuse-story.e2e.test.ts +282 -214
  244. package/src/__tests__/media-visibility-policy.test.ts +86 -38
  245. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +146 -100
  246. package/src/__tests__/memory-lifecycle-e2e.test.ts +385 -297
  247. package/src/__tests__/memory-query-builder.test.ts +32 -33
  248. package/src/__tests__/memory-recall-quality.test.ts +761 -407
  249. package/src/__tests__/memory-regressions.experimental.test.ts +443 -380
  250. package/src/__tests__/memory-regressions.test.ts +3725 -2642
  251. package/src/__tests__/memory-retrieval-budget.test.ts +7 -8
  252. package/src/__tests__/memory-retrieval.benchmark.test.ts +144 -109
  253. package/src/__tests__/memory-upsert-concurrency.test.ts +292 -201
  254. package/src/__tests__/messaging-send-tool.test.ts +36 -29
  255. package/src/__tests__/migration-cli-flows.test.ts +69 -53
  256. package/src/__tests__/migration-ordering.test.ts +103 -86
  257. package/src/__tests__/mime-builder.test.ts +55 -32
  258. package/src/__tests__/mock-signup-server.test.ts +384 -246
  259. package/src/__tests__/model-intents.test.ts +61 -37
  260. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +9 -12
  261. package/src/__tests__/no-is-trusted-guard.test.ts +24 -21
  262. package/src/__tests__/non-member-access-request.test.ts +294 -249
  263. package/src/__tests__/notification-broadcaster.test.ts +99 -81
  264. package/src/__tests__/notification-decision-fallback.test.ts +223 -178
  265. package/src/__tests__/notification-decision-strategy.test.ts +375 -337
  266. package/src/__tests__/notification-deep-link.test.ts +67 -61
  267. package/src/__tests__/notification-guardian-path.test.ts +248 -206
  268. package/src/__tests__/notification-routing-intent.test.ts +166 -93
  269. package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
  270. package/src/__tests__/notification-thread-candidate-validation.test.ts +78 -75
  271. package/src/__tests__/notification-thread-candidates.test.ts +64 -61
  272. package/src/__tests__/oauth-callback-registry.test.ts +40 -30
  273. package/src/__tests__/oauth-connect-handler.test.ts +109 -89
  274. package/src/__tests__/oauth-scope-policy.test.ts +63 -55
  275. package/src/__tests__/oauth2-gateway-transport.test.ts +252 -174
  276. package/src/__tests__/onboarding-starter-tasks.test.ts +93 -89
  277. package/src/__tests__/onboarding-template-contract.test.ts +93 -94
  278. package/src/__tests__/openai-provider.test.ts +366 -274
  279. package/src/__tests__/pairing-concurrent.test.ts +18 -12
  280. package/src/__tests__/pairing-routes.test.ts +45 -41
  281. package/src/__tests__/parallel-tool.benchmark.test.ts +108 -58
  282. package/src/__tests__/parser.test.ts +316 -226
  283. package/src/__tests__/path-classifier.test.ts +24 -25
  284. package/src/__tests__/path-policy.test.ts +187 -147
  285. package/src/__tests__/phone.test.ts +36 -36
  286. package/src/__tests__/platform-move-helper.test.ts +48 -40
  287. package/src/__tests__/platform-socket-path.test.ts +23 -24
  288. package/src/__tests__/platform-workspace-migration.test.ts +464 -414
  289. package/src/__tests__/platform.test.ts +61 -53
  290. package/src/__tests__/playbook-execution.test.ts +397 -265
  291. package/src/__tests__/playbook-tools.test.ts +267 -196
  292. package/src/__tests__/prebuilt-home-base-seed.test.ts +30 -27
  293. package/src/__tests__/pricing.test.ts +316 -136
  294. package/src/__tests__/profile-compiler.test.ts +206 -188
  295. package/src/__tests__/provider-commit-message-generator.test.ts +114 -106
  296. package/src/__tests__/provider-error-scenarios.test.ts +212 -158
  297. package/src/__tests__/provider-fail-open-selection.test.ts +51 -44
  298. package/src/__tests__/provider-registry-ollama.test.ts +13 -9
  299. package/src/__tests__/provider-streaming.benchmark.test.ts +232 -183
  300. package/src/__tests__/proxy-approval-callback.test.ts +180 -119
  301. package/src/__tests__/public-ingress-urls.test.ts +112 -94
  302. package/src/__tests__/qdrant-manager.test.ts +147 -98
  303. package/src/__tests__/ratelimit.test.ts +152 -82
  304. package/src/__tests__/recording-handler.test.ts +273 -151
  305. package/src/__tests__/recording-intent-fallback.test.ts +94 -75
  306. package/src/__tests__/recording-intent-handler.test.ts +422 -292
  307. package/src/__tests__/recording-intent.test.ts +578 -379
  308. package/src/__tests__/recording-state-machine.test.ts +530 -316
  309. package/src/__tests__/recurrence-engine-rruleset.test.ts +150 -92
  310. package/src/__tests__/recurrence-engine.test.ts +81 -41
  311. package/src/__tests__/recurrence-types.test.ts +63 -44
  312. package/src/__tests__/relay-server.test.ts +2131 -1602
  313. package/src/__tests__/reminder-store.test.ts +158 -80
  314. package/src/__tests__/reminder.test.ts +113 -109
  315. package/src/__tests__/remote-skill-policy.test.ts +96 -72
  316. package/src/__tests__/request-file-tool.test.ts +74 -67
  317. package/src/__tests__/response-tier.test.ts +131 -74
  318. package/src/__tests__/runtime-attachment-metadata.test.ts +107 -70
  319. package/src/__tests__/runtime-events-sse-parity.test.ts +167 -145
  320. package/src/__tests__/runtime-events-sse.test.ts +67 -51
  321. package/src/__tests__/sandbox-diagnostics.test.ts +66 -56
  322. package/src/__tests__/sandbox-host-parity.test.ts +377 -301
  323. package/src/__tests__/scaffold-managed-skill-tool.test.ts +213 -161
  324. package/src/__tests__/schedule-store.test.ts +268 -205
  325. package/src/__tests__/schedule-tools.test.ts +702 -524
  326. package/src/__tests__/scheduler-recurrence.test.ts +240 -130
  327. package/src/__tests__/scoped-approval-grants.test.ts +258 -168
  328. package/src/__tests__/scoped-grant-security-matrix.test.ts +160 -146
  329. package/src/__tests__/script-proxy-certs.test.ts +38 -35
  330. package/src/__tests__/script-proxy-connect-tunnel.test.ts +71 -46
  331. package/src/__tests__/script-proxy-decision-trace.test.ts +161 -84
  332. package/src/__tests__/script-proxy-http-forwarder.test.ts +146 -129
  333. package/src/__tests__/script-proxy-injection-runtime.test.ts +139 -113
  334. package/src/__tests__/script-proxy-mitm-handler.test.ts +226 -142
  335. package/src/__tests__/script-proxy-policy-runtime.test.ts +126 -86
  336. package/src/__tests__/script-proxy-policy.test.ts +308 -153
  337. package/src/__tests__/script-proxy-rewrite-specificity.test.ts +74 -62
  338. package/src/__tests__/script-proxy-router.test.ts +111 -77
  339. package/src/__tests__/script-proxy-session-manager.test.ts +156 -113
  340. package/src/__tests__/script-proxy-session-runtime.test.ts +28 -24
  341. package/src/__tests__/secret-allowlist.test.ts +105 -90
  342. package/src/__tests__/secret-ingress-handler.test.ts +41 -30
  343. package/src/__tests__/secret-onetime-send.test.ts +67 -50
  344. package/src/__tests__/secret-prompt-log-hygiene.test.ts +35 -31
  345. package/src/__tests__/secret-response-routing.test.ts +50 -41
  346. package/src/__tests__/secret-scanner-executor.test.ts +152 -111
  347. package/src/__tests__/secret-scanner.test.ts +495 -413
  348. package/src/__tests__/secure-keys.test.ts +132 -121
  349. package/src/__tests__/send-endpoint-busy.test.ts +313 -232
  350. package/src/__tests__/send-notification-tool.test.ts +43 -42
  351. package/src/__tests__/sensitive-output-placeholders.test.ts +72 -64
  352. package/src/__tests__/sequence-store.test.ts +335 -167
  353. package/src/__tests__/server-history-render.test.ts +341 -202
  354. package/src/__tests__/session-abort-tool-results.test.ts +133 -70
  355. package/src/__tests__/session-approval-overrides.test.ts +93 -91
  356. package/src/__tests__/session-confirmation-signals.test.ts +252 -160
  357. package/src/__tests__/session-conflict-gate.test.ts +775 -585
  358. package/src/__tests__/session-error.test.ts +222 -191
  359. package/src/__tests__/session-evictor.test.ts +79 -62
  360. package/src/__tests__/session-init.benchmark.test.ts +170 -108
  361. package/src/__tests__/session-load-history-repair.test.ts +273 -139
  362. package/src/__tests__/session-messaging-secret-redirect.test.ts +130 -90
  363. package/src/__tests__/session-pre-run-repair.test.ts +106 -59
  364. package/src/__tests__/session-profile-injection.test.ts +198 -130
  365. package/src/__tests__/session-provider-retry-repair.test.ts +223 -141
  366. package/src/__tests__/session-queue.test.ts +624 -321
  367. package/src/__tests__/session-runtime-assembly.test.ts +425 -329
  368. package/src/__tests__/session-runtime-workspace.test.ts +69 -61
  369. package/src/__tests__/session-skill-tools.test.ts +973 -678
  370. package/src/__tests__/session-slash-known.test.ts +185 -133
  371. package/src/__tests__/session-slash-queue.test.ts +147 -81
  372. package/src/__tests__/session-slash-unknown.test.ts +135 -90
  373. package/src/__tests__/session-surfaces-task-progress.test.ts +122 -87
  374. package/src/__tests__/session-tool-setup-app-refresh.test.ts +338 -177
  375. package/src/__tests__/session-tool-setup-memory-scope.test.ts +63 -40
  376. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +60 -37
  377. package/src/__tests__/session-tool-setup-tools-disabled.test.ts +28 -26
  378. package/src/__tests__/session-undo.test.ts +43 -30
  379. package/src/__tests__/session-workspace-cache-state.test.ts +108 -67
  380. package/src/__tests__/session-workspace-injection.test.ts +245 -117
  381. package/src/__tests__/session-workspace-tool-tracking.test.ts +260 -93
  382. package/src/__tests__/shared-filesystem-errors.test.ts +47 -47
  383. package/src/__tests__/shell-credential-ref.test.ts +126 -90
  384. package/src/__tests__/shell-identity.test.ts +134 -111
  385. package/src/__tests__/shell-parser-fuzz.test.ts +263 -179
  386. package/src/__tests__/shell-parser-property.test.ts +435 -288
  387. package/src/__tests__/shell-tool-proxy-mode.test.ts +142 -70
  388. package/src/__tests__/size-guard.test.ts +42 -44
  389. package/src/__tests__/skill-feature-flags-integration.test.ts +79 -52
  390. package/src/__tests__/skill-feature-flags.test.ts +75 -47
  391. package/src/__tests__/skill-include-graph.test.ts +143 -148
  392. package/src/__tests__/skill-load-feature-flag.test.ts +94 -59
  393. package/src/__tests__/skill-load-tool.test.ts +371 -199
  394. package/src/__tests__/skill-projection-feature-flag.test.ts +131 -88
  395. package/src/__tests__/skill-projection.benchmark.test.ts +93 -65
  396. package/src/__tests__/skill-script-runner-host.test.ts +460 -250
  397. package/src/__tests__/skill-script-runner-sandbox.test.ts +168 -108
  398. package/src/__tests__/skill-script-runner.test.ts +115 -74
  399. package/src/__tests__/skill-tool-factory.test.ts +140 -96
  400. package/src/__tests__/skill-tool-manifest.test.ts +306 -210
  401. package/src/__tests__/skill-version-hash.test.ts +70 -56
  402. package/src/__tests__/skills.test.ts +0 -1
  403. package/src/__tests__/slack-channel-config.test.ts +127 -84
  404. package/src/__tests__/slack-skill.test.ts +60 -47
  405. package/src/__tests__/slash-commands-catalog.test.ts +37 -31
  406. package/src/__tests__/slash-commands-parser.test.ts +71 -64
  407. package/src/__tests__/slash-commands-resolver.test.ts +143 -107
  408. package/src/__tests__/slash-commands-rewrite.test.ts +22 -22
  409. package/src/__tests__/sms-messaging-provider.test.ts +74 -47
  410. package/src/__tests__/speaker-identification.test.ts +28 -25
  411. package/src/__tests__/starter-bundle.test.ts +27 -23
  412. package/src/__tests__/starter-task-flow.test.ts +67 -52
  413. package/src/__tests__/subagent-manager-notify.test.ts +154 -108
  414. package/src/__tests__/subagent-tools.test.ts +311 -270
  415. package/src/__tests__/subagent-types.test.ts +40 -40
  416. package/src/__tests__/surface-mutex-cleanup.test.ts +42 -30
  417. package/src/__tests__/swarm-dag-pathological.test.ts +122 -111
  418. package/src/__tests__/swarm-orchestrator.test.ts +135 -101
  419. package/src/__tests__/swarm-plan-validator.test.ts +125 -73
  420. package/src/__tests__/swarm-recursion.test.ts +58 -46
  421. package/src/__tests__/swarm-router-planner.test.ts +99 -74
  422. package/src/__tests__/swarm-session-integration.test.ts +148 -91
  423. package/src/__tests__/swarm-tool.test.ts +65 -45
  424. package/src/__tests__/swarm-worker-backend.test.ts +59 -45
  425. package/src/__tests__/swarm-worker-runner.test.ts +133 -118
  426. package/src/__tests__/system-prompt.test.ts +290 -256
  427. package/src/__tests__/task-compiler.test.ts +176 -120
  428. package/src/__tests__/task-management-tools.test.ts +561 -456
  429. package/src/__tests__/task-memory-cleanup.test.ts +627 -362
  430. package/src/__tests__/task-runner.test.ts +117 -94
  431. package/src/__tests__/task-scheduler.test.ts +113 -84
  432. package/src/__tests__/task-tools.test.ts +349 -264
  433. package/src/__tests__/terminal-sandbox.test.ts +138 -108
  434. package/src/__tests__/terminal-tools.test.ts +350 -305
  435. package/src/__tests__/thread-seed-composer.test.ts +307 -180
  436. package/src/__tests__/tool-approval-handler.test.ts +238 -137
  437. package/src/__tests__/tool-audit-listener.test.ts +69 -69
  438. package/src/__tests__/tool-domain-event-publisher.test.ts +142 -132
  439. package/src/__tests__/tool-execution-abort-cleanup.test.ts +153 -146
  440. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +136 -105
  441. package/src/__tests__/tool-executor-lifecycle-events.test.ts +355 -239
  442. package/src/__tests__/tool-executor-redaction.test.ts +112 -109
  443. package/src/__tests__/tool-executor-shell-integration.test.ts +130 -79
  444. package/src/__tests__/tool-executor.test.ts +1274 -674
  445. package/src/__tests__/tool-grant-request-escalation.test.ts +401 -283
  446. package/src/__tests__/tool-metrics-listener.test.ts +97 -85
  447. package/src/__tests__/tool-notification-listener.test.ts +42 -25
  448. package/src/__tests__/tool-permission-simulate-handler.test.ts +137 -113
  449. package/src/__tests__/tool-policy.test.ts +44 -25
  450. package/src/__tests__/tool-profiling-listener.test.ts +99 -93
  451. package/src/__tests__/tool-result-truncation.test.ts +5 -4
  452. package/src/__tests__/tool-trace-listener.test.ts +131 -111
  453. package/src/__tests__/top-level-renderer.test.ts +62 -58
  454. package/src/__tests__/top-level-scanner.test.ts +68 -64
  455. package/src/__tests__/trace-emitter.test.ts +56 -56
  456. package/src/__tests__/trust-context-guards.test.ts +65 -65
  457. package/src/__tests__/trust-store.test.ts +1239 -806
  458. package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -275
  459. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -373
  460. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +264 -241
  461. package/src/__tests__/trusted-contact-multichannel.test.ts +182 -142
  462. package/src/__tests__/trusted-contact-verification.test.ts +251 -231
  463. package/src/__tests__/turn-commit.test.ts +259 -200
  464. package/src/__tests__/twilio-config.test.ts +49 -41
  465. package/src/__tests__/twilio-provider.test.ts +140 -126
  466. package/src/__tests__/twilio-rest.test.ts +22 -18
  467. package/src/__tests__/twilio-routes-elevenlabs.test.ts +188 -162
  468. package/src/__tests__/twilio-routes-twiml.test.ts +55 -55
  469. package/src/__tests__/twilio-routes.test.ts +389 -281
  470. package/src/__tests__/twitter-auth-handler.test.ts +184 -139
  471. package/src/__tests__/twitter-cli-error-shaping.test.ts +88 -73
  472. package/src/__tests__/twitter-cli-routing.test.ts +146 -99
  473. package/src/__tests__/twitter-oauth-client.test.ts +82 -65
  474. package/src/__tests__/update-bulletin-format.test.ts +69 -66
  475. package/src/__tests__/update-bulletin-state.test.ts +66 -60
  476. package/src/__tests__/update-bulletin.test.ts +150 -114
  477. package/src/__tests__/update-template-contract.test.ts +15 -10
  478. package/src/__tests__/url-safety.test.ts +288 -265
  479. package/src/__tests__/user-reference.test.ts +32 -32
  480. package/src/__tests__/view-image-tool.test.ts +118 -96
  481. package/src/__tests__/voice-invite-redemption.test.ts +111 -106
  482. package/src/__tests__/voice-quality.test.ts +117 -102
  483. package/src/__tests__/voice-scoped-grant-consumer.test.ts +204 -146
  484. package/src/__tests__/voice-session-bridge.test.ts +351 -216
  485. package/src/__tests__/weather-skill-regression.test.ts +170 -120
  486. package/src/__tests__/web-fetch.test.ts +664 -526
  487. package/src/__tests__/web-search.test.ts +379 -213
  488. package/src/__tests__/work-item-output.test.ts +90 -53
  489. package/src/__tests__/workspace-git-service.test.ts +437 -356
  490. package/src/__tests__/workspace-heartbeat-service.test.ts +125 -91
  491. package/src/__tests__/workspace-lifecycle.test.ts +98 -64
  492. package/src/__tests__/workspace-policy.test.ts +139 -71
  493. package/src/commands/__tests__/cc-command-registry.test.ts +142 -134
  494. package/src/config/__tests__/feature-flag-registry-guard.test.ts +48 -39
  495. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +44 -4
  496. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -1
  497. package/src/config/bundled-skills/messaging/SKILL.md +9 -7
  498. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +15 -5
  499. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +16 -5
  500. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +11 -7
  501. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +34 -32
  502. package/src/config/bundled-tool-registry.ts +2 -0
  503. package/src/config/env.ts +38 -29
  504. package/src/daemon/handlers/skills.ts +18 -10
  505. package/src/daemon/ipc-contract/messages.ts +1 -0
  506. package/src/daemon/ipc-contract/surfaces.ts +7 -1
  507. package/src/daemon/session-agent-loop-handlers.ts +5 -0
  508. package/src/daemon/session-agent-loop.ts +1 -1
  509. package/src/daemon/session-process.ts +1 -1
  510. package/src/daemon/session-surfaces.ts +42 -2
  511. package/src/memory/db-connection.ts +16 -10
  512. package/src/messaging/providers/gmail/adapter.ts +10 -3
  513. package/src/messaging/providers/gmail/client.ts +280 -72
  514. package/src/runtime/auth/__tests__/context.test.ts +75 -65
  515. package/src/runtime/auth/__tests__/credential-service.test.ts +137 -114
  516. package/src/runtime/auth/__tests__/guard-tests.test.ts +84 -90
  517. package/src/runtime/auth/__tests__/ipc-auth-context.test.ts +40 -40
  518. package/src/runtime/auth/__tests__/middleware.test.ts +80 -74
  519. package/src/runtime/auth/__tests__/policy.test.ts +9 -9
  520. package/src/runtime/auth/__tests__/route-policy.test.ts +76 -65
  521. package/src/runtime/auth/__tests__/scopes.test.ts +68 -60
  522. package/src/runtime/auth/__tests__/subject.test.ts +54 -54
  523. package/src/runtime/auth/__tests__/token-service.test.ts +115 -108
  524. package/src/runtime/auth/scopes.ts +3 -0
  525. package/src/runtime/auth/token-service.ts +78 -48
  526. package/src/runtime/auth/types.ts +2 -1
  527. package/src/runtime/http-server.ts +2 -1
  528. package/src/security/secure-keys.ts +103 -53
  529. package/src/sequence/reply-matcher.ts +10 -6
  530. package/src/skills/frontmatter.ts +9 -6
  531. package/src/tools/browser/__tests__/auth-cache.test.ts +69 -63
  532. package/src/tools/browser/__tests__/auth-detector.test.ts +218 -157
  533. package/src/tools/browser/__tests__/jit-auth.test.ts +83 -99
  534. package/src/tools/ui-surface/definitions.ts +2 -1
  535. package/src/util/platform.ts +0 -12
  536. package/docs/architecture/http-token-refresh.md +0 -274
@@ -1,61 +1,70 @@
1
- import { mkdtempSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ afterAll,
6
+ beforeEach,
7
+ describe,
8
+ expect,
9
+ mock,
10
+ spyOn,
11
+ test,
12
+ } from "bun:test";
4
13
 
5
- import { afterAll, beforeEach, describe, expect, mock, spyOn,test } from 'bun:test';
6
- import { eq } from 'drizzle-orm';
14
+ import { eq } from "drizzle-orm";
7
15
 
8
16
  // ---------------------------------------------------------------------------
9
17
  // Test isolation: in-memory SQLite via temp directory
10
18
  // ---------------------------------------------------------------------------
11
19
 
12
- const testDir = mkdtempSync(join(tmpdir(), 'channel-approval-routes-test-'));
20
+ const testDir = mkdtempSync(join(tmpdir(), "channel-approval-routes-test-"));
13
21
 
14
- mock.module('../util/platform.js', () => ({
22
+ mock.module("../util/platform.js", () => ({
15
23
  getRootDir: () => testDir,
16
24
  getDataDir: () => testDir,
17
- isMacOS: () => process.platform === 'darwin',
18
- isLinux: () => process.platform === 'linux',
19
- isWindows: () => process.platform === 'win32',
20
- getSocketPath: () => join(testDir, 'test.sock'),
21
- getPidPath: () => join(testDir, 'test.pid'),
22
- getDbPath: () => join(testDir, 'test.db'),
23
- getLogPath: () => join(testDir, 'test.log'),
25
+ isMacOS: () => process.platform === "darwin",
26
+ isLinux: () => process.platform === "linux",
27
+ isWindows: () => process.platform === "win32",
28
+ getSocketPath: () => join(testDir, "test.sock"),
29
+ getPidPath: () => join(testDir, "test.pid"),
30
+ getDbPath: () => join(testDir, "test.db"),
31
+ getLogPath: () => join(testDir, "test.log"),
24
32
  ensureDataDir: () => {},
25
33
  }));
26
34
 
27
- mock.module('../util/logger.js', () => ({
28
- getLogger: () => new Proxy({} as Record<string, unknown>, {
29
- get: () => () => {},
30
- }),
35
+ mock.module("../util/logger.js", () => ({
36
+ getLogger: () =>
37
+ new Proxy({} as Record<string, unknown>, {
38
+ get: () => () => {},
39
+ }),
31
40
  }));
32
41
 
33
42
  // Mock security check to always pass
34
- mock.module('../security/secret-ingress.js', () => ({
43
+ mock.module("../security/secret-ingress.js", () => ({
35
44
  checkIngressForSecrets: () => ({ blocked: false }),
36
45
  }));
37
46
 
38
47
  // Mock render to return the raw content as text
39
- mock.module('../daemon/handlers.js', () => ({
48
+ mock.module("../daemon/handlers.js", () => ({
40
49
  renderHistoryContent: (content: unknown) => ({
41
- text: typeof content === 'string' ? content : JSON.stringify(content),
50
+ text: typeof content === "string" ? content : JSON.stringify(content),
42
51
  }),
43
52
  }));
44
53
 
45
54
  // Mock ingress member store to return an active member for all lookups.
46
55
  // The ingress ACL is always-on and requires member records, but approval
47
56
  // route tests focus on approval orchestration, not ACL enforcement.
48
- mock.module('../memory/ingress-member-store.js', () => ({
57
+ mock.module("../memory/ingress-member-store.js", () => ({
49
58
  findMember: () => ({
50
- id: 'member-test-default',
51
- assistantId: 'self',
52
- sourceChannel: 'telegram',
53
- externalUserId: 'telegram-user-default',
59
+ id: "member-test-default",
60
+ assistantId: "self",
61
+ sourceChannel: "telegram",
62
+ externalUserId: "telegram-user-default",
54
63
  externalChatId: null,
55
64
  displayName: null,
56
65
  username: null,
57
- status: 'active',
58
- policy: 'allow',
66
+ status: "active",
67
+ policy: "allow",
59
68
  inviteId: null,
60
69
  createdBySessionId: null,
61
70
  revokedReason: null,
@@ -66,35 +75,42 @@ mock.module('../memory/ingress-member-store.js', () => ({
66
75
  }),
67
76
  updateLastSeen: () => {},
68
77
  }));
69
- import type { Session } from '../daemon/session.js';
78
+ import type { Session } from "../daemon/session.js";
70
79
  import {
71
80
  createCanonicalGuardianDelivery,
72
81
  createCanonicalGuardianRequest,
73
82
  getCanonicalGuardianRequest,
74
- } from '../memory/canonical-guardian-store.js';
75
- import * as channelDeliveryStore from '../memory/channel-delivery-store.js';
83
+ } from "../memory/canonical-guardian-store.js";
84
+ import * as channelDeliveryStore from "../memory/channel-delivery-store.js";
76
85
  import {
77
86
  createApprovalRequest,
78
87
  createBinding,
79
88
  getAllPendingApprovalsByGuardianChat,
80
- } from '../memory/channel-guardian-store.js';
81
- import { getDb, initializeDb, resetDb } from '../memory/db.js';
82
- import { conversations, externalConversationBindings } from '../memory/schema.js';
83
- import { initAuthSigningKey } from '../runtime/auth/token-service.js';
84
- import * as gatewayClient from '../runtime/gateway-client.js';
85
- import * as pendingInteractions from '../runtime/pending-interactions.js';
89
+ } from "../memory/channel-guardian-store.js";
90
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
91
+ import {
92
+ conversations,
93
+ externalConversationBindings,
94
+ } from "../memory/schema.js";
95
+ import { initAuthSigningKey } from "../runtime/auth/token-service.js";
96
+ import * as gatewayClient from "../runtime/gateway-client.js";
97
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
86
98
  import {
87
99
  _setTestPollMaxWait,
88
100
  handleChannelInbound,
89
101
  sweepExpiredGuardianApprovals,
90
- } from '../runtime/routes/channel-routes.js';
102
+ } from "../runtime/routes/channel-routes.js";
91
103
 
92
104
  initializeDb();
93
- initAuthSigningKey(Buffer.from('test-signing-key-at-least-32-bytes-long'));
105
+ initAuthSigningKey(Buffer.from("test-signing-key-at-least-32-bytes-long"));
94
106
 
95
107
  afterAll(() => {
96
108
  resetDb();
97
- try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
109
+ try {
110
+ rmSync(testDir, { recursive: true });
111
+ } catch {
112
+ /* best effort */
113
+ }
98
114
  });
99
115
 
100
116
  // ---------------------------------------------------------------------------
@@ -104,11 +120,13 @@ afterAll(() => {
104
120
  function ensureConversation(conversationId: string): void {
105
121
  const db = getDb();
106
122
  try {
107
- db.insert(conversations).values({
108
- id: conversationId,
109
- createdAt: Date.now(),
110
- updatedAt: Date.now(),
111
- }).run();
123
+ db.insert(conversations)
124
+ .values({
125
+ id: conversationId,
126
+ createdAt: Date.now(),
127
+ updatedAt: Date.now(),
128
+ })
129
+ .run();
112
130
  } catch {
113
131
  // already exists
114
132
  }
@@ -116,17 +134,17 @@ function ensureConversation(conversationId: string): void {
116
134
 
117
135
  function resetTables(): void {
118
136
  const db = getDb();
119
- db.run('DELETE FROM scoped_approval_grants');
120
- db.run('DELETE FROM canonical_guardian_deliveries');
121
- db.run('DELETE FROM canonical_guardian_requests');
122
- db.run('DELETE FROM channel_guardian_approval_requests');
123
- db.run('DELETE FROM channel_guardian_verification_challenges');
124
- db.run('DELETE FROM channel_guardian_bindings');
125
- db.run('DELETE FROM conversation_keys');
126
- db.run('DELETE FROM message_runs');
127
- db.run('DELETE FROM channel_inbound_events');
128
- db.run('DELETE FROM messages');
129
- db.run('DELETE FROM conversations');
137
+ db.run("DELETE FROM scoped_approval_grants");
138
+ db.run("DELETE FROM canonical_guardian_deliveries");
139
+ db.run("DELETE FROM canonical_guardian_requests");
140
+ db.run("DELETE FROM channel_guardian_approval_requests");
141
+ db.run("DELETE FROM channel_guardian_verification_challenges");
142
+ db.run("DELETE FROM channel_guardian_bindings");
143
+ db.run("DELETE FROM conversation_keys");
144
+ db.run("DELETE FROM message_runs");
145
+ db.run("DELETE FROM channel_inbound_events");
146
+ db.run("DELETE FROM messages");
147
+ db.run("DELETE FROM conversations");
130
148
  channelDeliveryStore.resetAllRunDeliveryClaims();
131
149
  pendingInteractions.clear();
132
150
  }
@@ -143,9 +161,13 @@ function registerPendingInteraction(
143
161
  input?: Record<string, unknown>;
144
162
  riskLevel?: string;
145
163
  persistentDecisionsAllowed?: boolean;
146
- allowlistOptions?: Array<{ label: string; description: string; pattern: string }>;
164
+ allowlistOptions?: Array<{
165
+ label: string;
166
+ description: string;
167
+ pattern: string;
168
+ }>;
147
169
  scopeOptions?: Array<{ label: string; scope: string }>;
148
- executionTarget?: 'sandbox' | 'host';
170
+ executionTarget?: "sandbox" | "host";
149
171
  },
150
172
  ): ReturnType<typeof mock> {
151
173
  const handleConfirmationResponse = mock(() => {});
@@ -156,16 +178,20 @@ function registerPendingInteraction(
156
178
  pendingInteractions.register(requestId, {
157
179
  session: mockSession,
158
180
  conversationId,
159
- kind: 'confirmation',
181
+ kind: "confirmation",
160
182
  confirmationDetails: {
161
183
  toolName,
162
- input: opts?.input ?? { command: 'rm -rf /tmp/test' },
163
- riskLevel: opts?.riskLevel ?? 'high',
184
+ input: opts?.input ?? { command: "rm -rf /tmp/test" },
185
+ riskLevel: opts?.riskLevel ?? "high",
164
186
  allowlistOptions: opts?.allowlistOptions ?? [
165
- { label: 'rm -rf /tmp/test', description: 'rm -rf /tmp/test', pattern: 'rm -rf /tmp/test' },
187
+ {
188
+ label: "rm -rf /tmp/test",
189
+ description: "rm -rf /tmp/test",
190
+ pattern: "rm -rf /tmp/test",
191
+ },
166
192
  ],
167
193
  scopeOptions: opts?.scopeOptions ?? [
168
- { label: 'everywhere', scope: 'everywhere' },
194
+ { label: "everywhere", scope: "everywhere" },
169
195
  ],
170
196
  persistentDecisionsAllowed: opts?.persistentDecisionsAllowed,
171
197
  executionTarget: opts?.executionTarget,
@@ -175,35 +201,30 @@ function registerPendingInteraction(
175
201
  return handleConfirmationResponse;
176
202
  }
177
203
 
178
- /** Legacy bearer token constant — retained for use in X-Gateway-Origin
179
- * headers in test request construction. Gateway-origin proof is now
180
- * handled by JWT auth; these headers are ignored by the handler. */
181
- const TEST_BEARER_TOKEN = 'token';
182
-
183
204
  function makeInboundRequest(overrides: Record<string, unknown> = {}): Request {
184
205
  const body: Record<string, unknown> = {
185
- sourceChannel: 'telegram',
186
- conversationExternalId: 'chat-123',
187
- actorExternalId: 'telegram-user-default',
206
+ sourceChannel: "telegram",
207
+ conversationExternalId: "chat-123",
208
+ actorExternalId: "telegram-user-default",
188
209
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
189
- content: 'hello',
190
- replyCallbackUrl: 'https://gateway.test/deliver',
210
+ content: "hello",
211
+ replyCallbackUrl: "https://gateway.test/deliver",
191
212
  ...overrides,
192
213
  };
193
- if (!Object.hasOwn(overrides, 'interface')) {
194
- body.interface = typeof body.sourceChannel === 'string' ? body.sourceChannel : 'telegram';
214
+ if (!Object.hasOwn(overrides, "interface")) {
215
+ body.interface =
216
+ typeof body.sourceChannel === "string" ? body.sourceChannel : "telegram";
195
217
  }
196
- return new Request('http://localhost/channels/inbound', {
197
- method: 'POST',
218
+ return new Request("http://localhost/channels/inbound", {
219
+ method: "POST",
198
220
  headers: {
199
- 'Content-Type': 'application/json',
200
- 'X-Gateway-Origin': TEST_BEARER_TOKEN,
221
+ "Content-Type": "application/json",
201
222
  },
202
223
  body: JSON.stringify(body),
203
224
  });
204
225
  }
205
226
 
206
- const noopProcessMessage = mock(async () => ({ messageId: 'msg-1' }));
227
+ const noopProcessMessage = mock(async () => ({ messageId: "msg-1" }));
207
228
 
208
229
  beforeEach(() => {
209
230
  resetTables();
@@ -214,26 +235,26 @@ beforeEach(() => {
214
235
  // 1. Stale callback handling without matching pending approval
215
236
  // ═══════════════════════════════════════════════════════════════════════════
216
237
 
217
- describe('stale callback handling without matching pending approval', () => {
218
- test('ignores stale callback payloads even when pending approvals exist', async () => {
219
- ensureConversation('conv-1');
238
+ describe("stale callback handling without matching pending approval", () => {
239
+ test("ignores stale callback payloads even when pending approvals exist", async () => {
240
+ ensureConversation("conv-1");
220
241
 
221
242
  // Register a pending interaction for this conversation
222
- registerPendingInteraction('req-abc', 'conv-1', 'shell');
243
+ registerPendingInteraction("req-abc", "conv-1", "shell");
223
244
 
224
245
  const req = makeInboundRequest({
225
- content: 'approve',
246
+ content: "approve",
226
247
  // Callback data references a DIFFERENT requestId than the one pending
227
- callbackData: 'apr:req-different:approve_once',
248
+ callbackData: "apr:req-different:approve_once",
228
249
  });
229
250
 
230
251
  const res = await handleChannelInbound(req, noopProcessMessage);
231
- const body = await res.json() as Record<string, unknown>;
252
+ const body = (await res.json()) as Record<string, unknown>;
232
253
 
233
254
  // Callback payloads without a matching pending approval are treated as
234
255
  // stale and ignored.
235
256
  expect(body.accepted).toBe(true);
236
- expect(body.approval).toBe('stale_ignored');
257
+ expect(body.approval).toBe("stale_ignored");
237
258
  });
238
259
  });
239
260
 
@@ -241,73 +262,91 @@ describe('stale callback handling without matching pending approval', () => {
241
262
  // 2. Callback data triggers decision handling
242
263
  // ═══════════════════════════════════════════════════════════════════════════
243
264
 
244
- describe('inbound callback metadata triggers decision handling', () => {
265
+ describe("inbound callback metadata triggers decision handling", () => {
245
266
  beforeEach(() => {
246
267
  createBinding({
247
- assistantId: 'self',
248
- channel: 'telegram',
249
- guardianExternalUserId: 'telegram-user-default',
250
- guardianDeliveryChatId: 'chat-123',
251
- guardianPrincipalId: 'telegram-user-default',
268
+ assistantId: "self",
269
+ channel: "telegram",
270
+ guardianExternalUserId: "telegram-user-default",
271
+ guardianDeliveryChatId: "chat-123",
272
+ guardianPrincipalId: "telegram-user-default",
252
273
  });
253
274
  });
254
275
 
255
276
  test('callback data "apr:<requestId>:approve_once" is parsed and applied', async () => {
256
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
277
+ const deliverSpy = spyOn(
278
+ gatewayClient,
279
+ "deliverChannelReply",
280
+ ).mockResolvedValue(undefined);
257
281
 
258
282
  // Establish the conversation to get a conversationId mapping
259
- const initReq = makeInboundRequest({ content: 'init' });
283
+ const initReq = makeInboundRequest({ content: "init" });
260
284
  await handleChannelInbound(initReq, noopProcessMessage);
261
285
 
262
286
  const db = getDb();
263
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
287
+ const events = db.$client
288
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
289
+ .all() as Array<{ conversation_id: string }>;
264
290
  const conversationId = events[0]?.conversation_id;
265
291
  expect(conversationId).toBeTruthy();
266
292
  ensureConversation(conversationId!);
267
293
 
268
294
  // Register a pending interaction for this conversation
269
- const sessionMock = registerPendingInteraction('req-cb-1', conversationId!, 'shell');
295
+ const sessionMock = registerPendingInteraction(
296
+ "req-cb-1",
297
+ conversationId!,
298
+ "shell",
299
+ );
270
300
 
271
301
  // Send a callback data message
272
302
  const req = makeInboundRequest({
273
- content: '',
274
- callbackData: 'apr:req-cb-1:approve_once',
303
+ content: "",
304
+ callbackData: "apr:req-cb-1:approve_once",
275
305
  });
276
306
 
277
307
  const res = await handleChannelInbound(req, noopProcessMessage);
278
- const body = await res.json() as Record<string, unknown>;
308
+ const body = (await res.json()) as Record<string, unknown>;
279
309
 
280
310
  expect(body.accepted).toBe(true);
281
- expect(body.approval).toBe('decision_applied');
282
- expect(sessionMock).toHaveBeenCalledWith('req-cb-1', 'allow');
311
+ expect(body.approval).toBe("decision_applied");
312
+ expect(sessionMock).toHaveBeenCalledWith("req-cb-1", "allow");
283
313
 
284
314
  deliverSpy.mockRestore();
285
315
  });
286
316
 
287
317
  test('callback data "apr:<requestId>:reject" applies a rejection', async () => {
288
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
318
+ const deliverSpy = spyOn(
319
+ gatewayClient,
320
+ "deliverChannelReply",
321
+ ).mockResolvedValue(undefined);
289
322
 
290
- const initReq = makeInboundRequest({ content: 'init' });
323
+ const initReq = makeInboundRequest({ content: "init" });
291
324
  await handleChannelInbound(initReq, noopProcessMessage);
292
325
 
293
326
  const db = getDb();
294
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
327
+ const events = db.$client
328
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
329
+ .all() as Array<{ conversation_id: string }>;
295
330
  const conversationId = events[0]?.conversation_id;
296
331
  ensureConversation(conversationId!);
297
332
 
298
- const sessionMock = registerPendingInteraction('req-cb-2', conversationId!, 'shell');
333
+ const sessionMock = registerPendingInteraction(
334
+ "req-cb-2",
335
+ conversationId!,
336
+ "shell",
337
+ );
299
338
 
300
339
  const req = makeInboundRequest({
301
- content: '',
302
- callbackData: 'apr:req-cb-2:reject',
340
+ content: "",
341
+ callbackData: "apr:req-cb-2:reject",
303
342
  });
304
343
 
305
344
  const res = await handleChannelInbound(req, noopProcessMessage);
306
- const body = await res.json() as Record<string, unknown>;
345
+ const body = (await res.json()) as Record<string, unknown>;
307
346
 
308
347
  expect(body.accepted).toBe(true);
309
- expect(body.approval).toBe('decision_applied');
310
- expect(sessionMock).toHaveBeenCalledWith('req-cb-2', 'deny');
348
+ expect(body.approval).toBe("decision_applied");
349
+ expect(sessionMock).toHaveBeenCalledWith("req-cb-2", "deny");
311
350
 
312
351
  deliverSpy.mockRestore();
313
352
  });
@@ -317,61 +356,79 @@ describe('inbound callback metadata triggers decision handling', () => {
317
356
  // 3. Plain text triggers decision handling
318
357
  // ═══════════════════════════════════════════════════════════════════════════
319
358
 
320
- describe('inbound text matching approval phrases triggers decision handling', () => {
359
+ describe("inbound text matching approval phrases triggers decision handling", () => {
321
360
  beforeEach(() => {
322
361
  createBinding({
323
- assistantId: 'self',
324
- channel: 'telegram',
325
- guardianExternalUserId: 'telegram-user-default',
326
- guardianDeliveryChatId: 'chat-123',
327
- guardianPrincipalId: 'telegram-user-default',
362
+ assistantId: "self",
363
+ channel: "telegram",
364
+ guardianExternalUserId: "telegram-user-default",
365
+ guardianDeliveryChatId: "chat-123",
366
+ guardianPrincipalId: "telegram-user-default",
328
367
  });
329
368
  });
330
369
 
331
370
  test('text "approve" triggers approve_once decision', async () => {
332
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
371
+ const deliverSpy = spyOn(
372
+ gatewayClient,
373
+ "deliverChannelReply",
374
+ ).mockResolvedValue(undefined);
333
375
 
334
- const initReq = makeInboundRequest({ content: 'init' });
376
+ const initReq = makeInboundRequest({ content: "init" });
335
377
  await handleChannelInbound(initReq, noopProcessMessage);
336
378
 
337
379
  const db = getDb();
338
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
380
+ const events = db.$client
381
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
382
+ .all() as Array<{ conversation_id: string }>;
339
383
  const conversationId = events[0]?.conversation_id;
340
384
  ensureConversation(conversationId!);
341
385
 
342
- const sessionMock = registerPendingInteraction('req-txt-1', conversationId!, 'shell');
386
+ const sessionMock = registerPendingInteraction(
387
+ "req-txt-1",
388
+ conversationId!,
389
+ "shell",
390
+ );
343
391
 
344
- const req = makeInboundRequest({ content: 'approve' });
392
+ const req = makeInboundRequest({ content: "approve" });
345
393
  const res = await handleChannelInbound(req, noopProcessMessage);
346
- const body = await res.json() as Record<string, unknown>;
394
+ const body = (await res.json()) as Record<string, unknown>;
347
395
 
348
396
  expect(body.accepted).toBe(true);
349
- expect(body.approval).toBe('decision_applied');
350
- expect(sessionMock).toHaveBeenCalledWith('req-txt-1', 'allow');
397
+ expect(body.approval).toBe("decision_applied");
398
+ expect(sessionMock).toHaveBeenCalledWith("req-txt-1", "allow");
351
399
 
352
400
  deliverSpy.mockRestore();
353
401
  });
354
402
 
355
403
  test('text "always" triggers approve_always decision', async () => {
356
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
404
+ const deliverSpy = spyOn(
405
+ gatewayClient,
406
+ "deliverChannelReply",
407
+ ).mockResolvedValue(undefined);
357
408
 
358
- const initReq = makeInboundRequest({ content: 'init' });
409
+ const initReq = makeInboundRequest({ content: "init" });
359
410
  await handleChannelInbound(initReq, noopProcessMessage);
360
411
 
361
412
  const db = getDb();
362
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
413
+ const events = db.$client
414
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
415
+ .all() as Array<{ conversation_id: string }>;
363
416
  const conversationId = events[0]?.conversation_id;
364
417
  ensureConversation(conversationId!);
365
418
 
366
- const sessionMock = registerPendingInteraction('req-txt-2', conversationId!, 'shell');
419
+ const sessionMock = registerPendingInteraction(
420
+ "req-txt-2",
421
+ conversationId!,
422
+ "shell",
423
+ );
367
424
 
368
- const req = makeInboundRequest({ content: 'always' });
425
+ const req = makeInboundRequest({ content: "always" });
369
426
  const res = await handleChannelInbound(req, noopProcessMessage);
370
- const body = await res.json() as Record<string, unknown>;
427
+ const body = (await res.json()) as Record<string, unknown>;
371
428
 
372
429
  expect(body.accepted).toBe(true);
373
- expect(body.approval).toBe('decision_applied');
374
- expect(sessionMock).toHaveBeenCalledWith('req-txt-2', 'allow');
430
+ expect(body.approval).toBe("decision_applied");
431
+ expect(sessionMock).toHaveBeenCalledWith("req-txt-2", "allow");
375
432
 
376
433
  deliverSpy.mockRestore();
377
434
  });
@@ -381,47 +438,54 @@ describe('inbound text matching approval phrases triggers decision handling', ()
381
438
  // 4. Non-decision messages during pending approval (no conversational engine)
382
439
  // ═══════════════════════════════════════════════════════════════════════════
383
440
 
384
- describe('non-decision messages during pending approval (legacy fallback)', () => {
441
+ describe("non-decision messages during pending approval (legacy fallback)", () => {
385
442
  beforeEach(() => {
386
443
  createBinding({
387
- assistantId: 'self',
388
- channel: 'telegram',
389
- guardianExternalUserId: 'telegram-user-default',
390
- guardianDeliveryChatId: 'chat-123',
391
- guardianPrincipalId: 'telegram-user-default',
444
+ assistantId: "self",
445
+ channel: "telegram",
446
+ guardianExternalUserId: "telegram-user-default",
447
+ guardianDeliveryChatId: "chat-123",
448
+ guardianPrincipalId: "telegram-user-default",
392
449
  });
393
450
  });
394
451
 
395
- test('sends a status reply when message is not a decision and no conversational engine', async () => {
396
- const replySpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
452
+ test("sends a status reply when message is not a decision and no conversational engine", async () => {
453
+ const replySpy = spyOn(
454
+ gatewayClient,
455
+ "deliverChannelReply",
456
+ ).mockResolvedValue(undefined);
397
457
 
398
- const initReq = makeInboundRequest({ content: 'init' });
458
+ const initReq = makeInboundRequest({ content: "init" });
399
459
  await handleChannelInbound(initReq, noopProcessMessage);
400
460
 
401
461
  const db = getDb();
402
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
462
+ const events = db.$client
463
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
464
+ .all() as Array<{ conversation_id: string }>;
403
465
  const conversationId = events[0]?.conversation_id;
404
466
  ensureConversation(conversationId!);
405
467
 
406
- registerPendingInteraction('req-nd-1', conversationId!, 'shell');
468
+ registerPendingInteraction("req-nd-1", conversationId!, "shell");
407
469
 
408
470
  // Send a message that is NOT a decision
409
- const req = makeInboundRequest({ content: 'what is the weather?' });
471
+ const req = makeInboundRequest({ content: "what is the weather?" });
410
472
  const res = await handleChannelInbound(req, noopProcessMessage);
411
- const body = await res.json() as Record<string, unknown>;
473
+ const body = (await res.json()) as Record<string, unknown>;
412
474
 
413
475
  expect(body.accepted).toBe(true);
414
- expect(body.approval).toBe('assistant_turn');
476
+ expect(body.approval).toBe("assistant_turn");
415
477
 
416
478
  // A status reply should have been delivered via deliverChannelReply
417
479
  expect(replySpy).toHaveBeenCalled();
418
480
  const statusCall = replySpy.mock.calls.find(
419
- (call) => typeof call[1] === 'object' && (call[1] as { chatId?: string }).chatId === 'chat-123',
481
+ (call) =>
482
+ typeof call[1] === "object" &&
483
+ (call[1] as { chatId?: string }).chatId === "chat-123",
420
484
  );
421
485
  expect(statusCall).toBeDefined();
422
486
  const statusPayload = statusCall![1] as { text?: string };
423
487
  // The status text mentions a pending approval
424
- expect(statusPayload.text).toContain('pending approval request');
488
+ expect(statusPayload.text).toContain("pending approval request");
425
489
 
426
490
  replySpy.mockRestore();
427
491
  });
@@ -431,20 +495,20 @@ describe('non-decision messages during pending approval (legacy fallback)', () =
431
495
  // 5. Messages without pending approval proceed normally
432
496
  // ═══════════════════════════════════════════════════════════════════════════
433
497
 
434
- describe('messages without pending approval proceed normally', () => {
435
- test('proceeds to normal processing when no pending approval exists', async () => {
436
- const req = makeInboundRequest({ content: 'hello world' });
498
+ describe("messages without pending approval proceed normally", () => {
499
+ test("proceeds to normal processing when no pending approval exists", async () => {
500
+ const req = makeInboundRequest({ content: "hello world" });
437
501
  const res = await handleChannelInbound(req, noopProcessMessage);
438
- const body = await res.json() as Record<string, unknown>;
502
+ const body = (await res.json()) as Record<string, unknown>;
439
503
 
440
504
  expect(body.accepted).toBe(true);
441
505
  expect(body.approval).toBeUndefined();
442
506
  });
443
507
 
444
508
  test('text "approve" is processed normally when no pending approval exists', async () => {
445
- const req = makeInboundRequest({ content: 'approve' });
509
+ const req = makeInboundRequest({ content: "approve" });
446
510
  const res = await handleChannelInbound(req, noopProcessMessage);
447
- const body = await res.json() as Record<string, unknown>;
511
+ const body = (await res.json()) as Record<string, unknown>;
448
512
 
449
513
  expect(body.accepted).toBe(true);
450
514
  // Should NOT be treated as an approval decision since there's no pending approval
@@ -456,90 +520,109 @@ describe('messages without pending approval proceed normally', () => {
456
520
  // 6. Empty content with callbackData bypasses validation
457
521
  // ═══════════════════════════════════════════════════════════════════════════
458
522
 
459
- describe('empty content with callbackData bypasses validation', () => {
523
+ describe("empty content with callbackData bypasses validation", () => {
460
524
  beforeEach(() => {
461
525
  createBinding({
462
- assistantId: 'self',
463
- channel: 'telegram',
464
- guardianExternalUserId: 'telegram-user-default',
465
- guardianDeliveryChatId: 'chat-123',
466
- guardianPrincipalId: 'telegram-user-default',
526
+ assistantId: "self",
527
+ channel: "telegram",
528
+ guardianExternalUserId: "telegram-user-default",
529
+ guardianDeliveryChatId: "chat-123",
530
+ guardianPrincipalId: "telegram-user-default",
467
531
  });
468
532
  });
469
533
 
470
- test('rejects empty content without callbackData', async () => {
471
- const req = makeInboundRequest({ content: '' });
534
+ test("rejects empty content without callbackData", async () => {
535
+ const req = makeInboundRequest({ content: "" });
472
536
  const res = await handleChannelInbound(req, noopProcessMessage);
473
537
  expect(res.status).toBe(400);
474
- const body = await res.json() as Record<string, unknown>;
475
- expect((body.error as Record<string, unknown>).message).toBe('content or attachmentIds is required');
538
+ const body = (await res.json()) as Record<string, unknown>;
539
+ expect((body.error as Record<string, unknown>).message).toBe(
540
+ "content or attachmentIds is required",
541
+ );
476
542
  });
477
543
 
478
- test('allows empty content when callbackData is present', async () => {
544
+ test("allows empty content when callbackData is present", async () => {
479
545
  // Establish the conversation first
480
- const initReq = makeInboundRequest({ content: 'init' });
546
+ const initReq = makeInboundRequest({ content: "init" });
481
547
  await handleChannelInbound(initReq, noopProcessMessage);
482
548
 
483
549
  const db = getDb();
484
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
550
+ const events = db.$client
551
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
552
+ .all() as Array<{ conversation_id: string }>;
485
553
  const conversationId = events[0]?.conversation_id;
486
554
  ensureConversation(conversationId!);
487
555
 
488
- const sessionMock = registerPendingInteraction('req-empty-1', conversationId!, 'shell');
556
+ const sessionMock = registerPendingInteraction(
557
+ "req-empty-1",
558
+ conversationId!,
559
+ "shell",
560
+ );
489
561
 
490
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
562
+ const deliverSpy = spyOn(
563
+ gatewayClient,
564
+ "deliverChannelReply",
565
+ ).mockResolvedValue(undefined);
491
566
 
492
567
  const req = makeInboundRequest({
493
- content: '',
494
- callbackData: 'apr:req-empty-1:approve_once',
568
+ content: "",
569
+ callbackData: "apr:req-empty-1:approve_once",
495
570
  });
496
571
 
497
572
  const res = await handleChannelInbound(req, noopProcessMessage);
498
573
  expect(res.status).toBe(200);
499
- const body = await res.json() as Record<string, unknown>;
574
+ const body = (await res.json()) as Record<string, unknown>;
500
575
  expect(body.accepted).toBe(true);
501
- expect(body.approval).toBe('decision_applied');
502
- expect(sessionMock).toHaveBeenCalledWith('req-empty-1', 'allow');
576
+ expect(body.approval).toBe("decision_applied");
577
+ expect(sessionMock).toHaveBeenCalledWith("req-empty-1", "allow");
503
578
 
504
579
  deliverSpy.mockRestore();
505
580
  });
506
581
 
507
- test('allows undefined content when callbackData is present', async () => {
582
+ test("allows undefined content when callbackData is present", async () => {
508
583
  // Establish the conversation first
509
- const initReq = makeInboundRequest({ content: 'init' });
584
+ const initReq = makeInboundRequest({ content: "init" });
510
585
  await handleChannelInbound(initReq, noopProcessMessage);
511
586
 
512
587
  const db = getDb();
513
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
588
+ const events = db.$client
589
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
590
+ .all() as Array<{ conversation_id: string }>;
514
591
  const conversationId = events[0]?.conversation_id;
515
592
  ensureConversation(conversationId!);
516
593
 
517
- const _sessionMock = registerPendingInteraction('req-empty-2', conversationId!, 'shell');
594
+ const _sessionMock = registerPendingInteraction(
595
+ "req-empty-2",
596
+ conversationId!,
597
+ "shell",
598
+ );
518
599
 
519
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
600
+ const deliverSpy = spyOn(
601
+ gatewayClient,
602
+ "deliverChannelReply",
603
+ ).mockResolvedValue(undefined);
520
604
 
521
605
  // Send with no content field at all, just callbackData
522
606
  const reqBody = {
523
- sourceChannel: 'telegram',
524
- interface: 'telegram',
525
- conversationExternalId: 'chat-123',
607
+ sourceChannel: "telegram",
608
+ interface: "telegram",
609
+ conversationExternalId: "chat-123",
526
610
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
527
- callbackData: 'apr:req-empty-2:approve_once',
528
- replyCallbackUrl: 'https://gateway.test/deliver',
529
- actorExternalId: 'telegram-user-default',
611
+ callbackData: "apr:req-empty-2:approve_once",
612
+ replyCallbackUrl: "https://gateway.test/deliver",
613
+ actorExternalId: "telegram-user-default",
530
614
  };
531
- const req = new Request('http://localhost/channels/inbound', {
532
- method: 'POST',
615
+ const req = new Request("http://localhost/channels/inbound", {
616
+ method: "POST",
533
617
  headers: {
534
- 'Content-Type': 'application/json',
535
- 'X-Gateway-Origin': TEST_BEARER_TOKEN,
618
+ "Content-Type": "application/json",
536
619
  },
537
620
  body: JSON.stringify(reqBody),
538
621
  });
539
622
 
540
623
  const res = await handleChannelInbound(req, noopProcessMessage);
541
624
  expect(res.status).toBe(200);
542
- const resBody = await res.json() as Record<string, unknown>;
625
+ const resBody = (await res.json()) as Record<string, unknown>;
543
626
  expect(resBody.accepted).toBe(true);
544
627
 
545
628
  deliverSpy.mockRestore();
@@ -550,98 +633,125 @@ describe('empty content with callbackData bypasses validation', () => {
550
633
  // 7. Callback requestId validation — stale button press
551
634
  // ═══════════════════════════════════════════════════════════════════════════
552
635
 
553
- describe('callback requestId validation', () => {
636
+ describe("callback requestId validation", () => {
554
637
  beforeEach(() => {
555
638
  createBinding({
556
- assistantId: 'self',
557
- channel: 'telegram',
558
- guardianExternalUserId: 'telegram-user-default',
559
- guardianDeliveryChatId: 'chat-123',
560
- guardianPrincipalId: 'telegram-user-default',
639
+ assistantId: "self",
640
+ channel: "telegram",
641
+ guardianExternalUserId: "telegram-user-default",
642
+ guardianDeliveryChatId: "chat-123",
643
+ guardianPrincipalId: "telegram-user-default",
561
644
  });
562
645
  });
563
646
 
564
- test('ignores stale callback when requestId does not match any pending interaction', async () => {
565
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
647
+ test("ignores stale callback when requestId does not match any pending interaction", async () => {
648
+ const deliverSpy = spyOn(
649
+ gatewayClient,
650
+ "deliverChannelReply",
651
+ ).mockResolvedValue(undefined);
566
652
 
567
- const initReq = makeInboundRequest({ content: 'init' });
653
+ const initReq = makeInboundRequest({ content: "init" });
568
654
  await handleChannelInbound(initReq, noopProcessMessage);
569
655
 
570
656
  const db = getDb();
571
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
657
+ const events = db.$client
658
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
659
+ .all() as Array<{ conversation_id: string }>;
572
660
  const conversationId = events[0]?.conversation_id;
573
661
  ensureConversation(conversationId!);
574
662
 
575
663
  // Register a pending interaction
576
- const sessionMock = registerPendingInteraction('req-valid', conversationId!, 'shell');
664
+ const sessionMock = registerPendingInteraction(
665
+ "req-valid",
666
+ conversationId!,
667
+ "shell",
668
+ );
577
669
 
578
670
  // Send callback with a DIFFERENT requestId (stale button)
579
671
  const req = makeInboundRequest({
580
- content: '',
581
- callbackData: 'apr:stale-request-id:approve_once',
672
+ content: "",
673
+ callbackData: "apr:stale-request-id:approve_once",
582
674
  });
583
675
 
584
676
  const res = await handleChannelInbound(req, noopProcessMessage);
585
- const body = await res.json() as Record<string, unknown>;
677
+ const body = (await res.json()) as Record<string, unknown>;
586
678
 
587
679
  expect(body.accepted).toBe(true);
588
- expect(body.approval).toBe('stale_ignored');
680
+ expect(body.approval).toBe("stale_ignored");
589
681
  // session should NOT have been called because the requestId didn't match
590
682
  expect(sessionMock).not.toHaveBeenCalled();
591
683
 
592
684
  deliverSpy.mockRestore();
593
685
  });
594
686
 
595
- test('applies callback when requestId matches pending interaction', async () => {
596
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
687
+ test("applies callback when requestId matches pending interaction", async () => {
688
+ const deliverSpy = spyOn(
689
+ gatewayClient,
690
+ "deliverChannelReply",
691
+ ).mockResolvedValue(undefined);
597
692
 
598
- const initReq = makeInboundRequest({ content: 'init' });
693
+ const initReq = makeInboundRequest({ content: "init" });
599
694
  await handleChannelInbound(initReq, noopProcessMessage);
600
695
 
601
696
  const db = getDb();
602
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
697
+ const events = db.$client
698
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
699
+ .all() as Array<{ conversation_id: string }>;
603
700
  const conversationId = events[0]?.conversation_id;
604
701
  ensureConversation(conversationId!);
605
702
 
606
- const sessionMock = registerPendingInteraction('req-match', conversationId!, 'shell');
703
+ const sessionMock = registerPendingInteraction(
704
+ "req-match",
705
+ conversationId!,
706
+ "shell",
707
+ );
607
708
 
608
709
  // Send callback with the CORRECT requestId
609
710
  const req = makeInboundRequest({
610
- content: '',
611
- callbackData: 'apr:req-match:approve_once',
711
+ content: "",
712
+ callbackData: "apr:req-match:approve_once",
612
713
  });
613
714
 
614
715
  const res = await handleChannelInbound(req, noopProcessMessage);
615
- const body = await res.json() as Record<string, unknown>;
716
+ const body = (await res.json()) as Record<string, unknown>;
616
717
 
617
718
  expect(body.accepted).toBe(true);
618
- expect(body.approval).toBe('decision_applied');
619
- expect(sessionMock).toHaveBeenCalledWith('req-match', 'allow');
719
+ expect(body.approval).toBe("decision_applied");
720
+ expect(sessionMock).toHaveBeenCalledWith("req-match", "allow");
620
721
 
621
722
  deliverSpy.mockRestore();
622
723
  });
623
724
 
624
- test('plain-text decisions bypass requestId validation (no requestId in result)', async () => {
625
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
725
+ test("plain-text decisions bypass requestId validation (no requestId in result)", async () => {
726
+ const deliverSpy = spyOn(
727
+ gatewayClient,
728
+ "deliverChannelReply",
729
+ ).mockResolvedValue(undefined);
626
730
 
627
- const initReq = makeInboundRequest({ content: 'init' });
731
+ const initReq = makeInboundRequest({ content: "init" });
628
732
  await handleChannelInbound(initReq, noopProcessMessage);
629
733
 
630
734
  const db = getDb();
631
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
735
+ const events = db.$client
736
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
737
+ .all() as Array<{ conversation_id: string }>;
632
738
  const conversationId = events[0]?.conversation_id;
633
739
  ensureConversation(conversationId!);
634
740
 
635
- const sessionMock = registerPendingInteraction('req-plaintext', conversationId!, 'shell');
741
+ const sessionMock = registerPendingInteraction(
742
+ "req-plaintext",
743
+ conversationId!,
744
+ "shell",
745
+ );
636
746
 
637
747
  // Send plain text "yes" — no requestId in the parsed result
638
- const req = makeInboundRequest({ content: 'yes' });
748
+ const req = makeInboundRequest({ content: "yes" });
639
749
  const res = await handleChannelInbound(req, noopProcessMessage);
640
- const body = await res.json() as Record<string, unknown>;
750
+ const body = (await res.json()) as Record<string, unknown>;
641
751
 
642
752
  expect(body.accepted).toBe(true);
643
- expect(body.approval).toBe('decision_applied');
644
- expect(sessionMock).toHaveBeenCalledWith('req-plaintext', 'allow');
753
+ expect(body.approval).toBe("decision_applied");
754
+ expect(sessionMock).toHaveBeenCalledWith("req-plaintext", "allow");
645
755
 
646
756
  deliverSpy.mockRestore();
647
757
  });
@@ -651,43 +761,48 @@ describe('callback requestId validation', () => {
651
761
  // 10. No immediate reply after approval decision
652
762
  // ═══════════════════════════════════════════════════════════════════════════
653
763
 
654
- describe('no immediate reply after approval decision', () => {
764
+ describe("no immediate reply after approval decision", () => {
655
765
  beforeEach(() => {
656
766
  createBinding({
657
- assistantId: 'self',
658
- channel: 'telegram',
659
- guardianExternalUserId: 'telegram-user-default',
660
- guardianDeliveryChatId: 'chat-123',
661
- guardianPrincipalId: 'telegram-user-default',
767
+ assistantId: "self",
768
+ channel: "telegram",
769
+ guardianExternalUserId: "telegram-user-default",
770
+ guardianDeliveryChatId: "chat-123",
771
+ guardianPrincipalId: "telegram-user-default",
662
772
  });
663
773
  });
664
774
 
665
- test('deliverChannelReply is NOT called from interception after decision is applied', async () => {
666
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
775
+ test("deliverChannelReply is NOT called from interception after decision is applied", async () => {
776
+ const deliverSpy = spyOn(
777
+ gatewayClient,
778
+ "deliverChannelReply",
779
+ ).mockResolvedValue(undefined);
667
780
 
668
- const initReq = makeInboundRequest({ content: 'init' });
781
+ const initReq = makeInboundRequest({ content: "init" });
669
782
  await handleChannelInbound(initReq, noopProcessMessage);
670
783
 
671
784
  const db = getDb();
672
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
785
+ const events = db.$client
786
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
787
+ .all() as Array<{ conversation_id: string }>;
673
788
  const conversationId = events[0]?.conversation_id;
674
789
  ensureConversation(conversationId!);
675
790
 
676
- registerPendingInteraction('req-noreply-1', conversationId!, 'shell');
791
+ registerPendingInteraction("req-noreply-1", conversationId!, "shell");
677
792
 
678
793
  // Clear the spy to only track calls from the decision path
679
794
  deliverSpy.mockClear();
680
795
 
681
796
  // Send a callback decision
682
797
  const req = makeInboundRequest({
683
- content: '',
684
- callbackData: 'apr:req-noreply-1:approve_once',
798
+ content: "",
799
+ callbackData: "apr:req-noreply-1:approve_once",
685
800
  });
686
801
 
687
802
  const res = await handleChannelInbound(req, noopProcessMessage);
688
- const body = await res.json() as Record<string, unknown>;
803
+ const body = (await res.json()) as Record<string, unknown>;
689
804
 
690
- expect(body.approval).toBe('decision_applied');
805
+ expect(body.approval).toBe("decision_applied");
691
806
 
692
807
  // The interception handler should NOT have called deliverChannelReply.
693
808
  // The reply should only come from the session's onEvent callback.
@@ -696,27 +811,32 @@ describe('no immediate reply after approval decision', () => {
696
811
  deliverSpy.mockRestore();
697
812
  });
698
813
 
699
- test('plain-text decision also does not trigger immediate reply', async () => {
700
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
814
+ test("plain-text decision also does not trigger immediate reply", async () => {
815
+ const deliverSpy = spyOn(
816
+ gatewayClient,
817
+ "deliverChannelReply",
818
+ ).mockResolvedValue(undefined);
701
819
 
702
- const initReq = makeInboundRequest({ content: 'init' });
820
+ const initReq = makeInboundRequest({ content: "init" });
703
821
  await handleChannelInbound(initReq, noopProcessMessage);
704
822
 
705
823
  const db = getDb();
706
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
824
+ const events = db.$client
825
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
826
+ .all() as Array<{ conversation_id: string }>;
707
827
  const conversationId = events[0]?.conversation_id;
708
828
  ensureConversation(conversationId!);
709
829
 
710
- registerPendingInteraction('req-noreply-2', conversationId!, 'shell');
830
+ registerPendingInteraction("req-noreply-2", conversationId!, "shell");
711
831
 
712
832
  deliverSpy.mockClear();
713
833
 
714
834
  // Send a plain-text approval
715
- const req = makeInboundRequest({ content: 'approve' });
835
+ const req = makeInboundRequest({ content: "approve" });
716
836
  const res = await handleChannelInbound(req, noopProcessMessage);
717
- const body = await res.json() as Record<string, unknown>;
837
+ const body = (await res.json()) as Record<string, unknown>;
718
838
 
719
- expect(body.approval).toBe('decision_applied');
839
+ expect(body.approval).toBe("decision_applied");
720
840
  expect(deliverSpy).not.toHaveBeenCalled();
721
841
 
722
842
  deliverSpy.mockRestore();
@@ -727,42 +847,42 @@ describe('no immediate reply after approval decision', () => {
727
847
  // 11. Stale callback with no pending approval returns stale_ignored
728
848
  // ═══════════════════════════════════════════════════════════════════════════
729
849
 
730
- describe('stale callback handling', () => {
731
- test('callback with no pending approval returns stale_ignored', async () => {
850
+ describe("stale callback handling", () => {
851
+ test("callback with no pending approval returns stale_ignored", async () => {
732
852
  // No pending interactions — send a stale callback
733
853
  const req = makeInboundRequest({
734
- content: '',
735
- callbackData: 'apr:stale-req:approve_once',
854
+ content: "",
855
+ callbackData: "apr:stale-req:approve_once",
736
856
  });
737
857
 
738
858
  const res = await handleChannelInbound(req, noopProcessMessage);
739
- const body = await res.json() as Record<string, unknown>;
859
+ const body = (await res.json()) as Record<string, unknown>;
740
860
 
741
861
  expect(body.accepted).toBe(true);
742
- expect(body.approval).toBe('stale_ignored');
862
+ expect(body.approval).toBe("stale_ignored");
743
863
  });
744
864
 
745
- test('callback with non-empty content but no pending approval returns stale_ignored', async () => {
865
+ test("callback with non-empty content but no pending approval returns stale_ignored", async () => {
746
866
  // Simulate what normalize.ts does: callbackData present AND content is
747
867
  // set to the callback data value (non-empty).
748
868
  const req = makeInboundRequest({
749
- content: 'apr:stale-req:approve_once',
750
- callbackData: 'apr:stale-req:approve_once',
869
+ content: "apr:stale-req:approve_once",
870
+ callbackData: "apr:stale-req:approve_once",
751
871
  });
752
872
 
753
873
  const res = await handleChannelInbound(req, noopProcessMessage);
754
- const body = await res.json() as Record<string, unknown>;
874
+ const body = (await res.json()) as Record<string, unknown>;
755
875
 
756
876
  expect(body.accepted).toBe(true);
757
- expect(body.approval).toBe('stale_ignored');
877
+ expect(body.approval).toBe("stale_ignored");
758
878
  });
759
879
 
760
- test('non-callback message without pending approval proceeds to normal processing', async () => {
880
+ test("non-callback message without pending approval proceeds to normal processing", async () => {
761
881
  // Regular text message (no callbackData) should proceed normally
762
- const req = makeInboundRequest({ content: 'hello world' });
882
+ const req = makeInboundRequest({ content: "hello world" });
763
883
 
764
884
  const res = await handleChannelInbound(req, noopProcessMessage);
765
- const body = await res.json() as Record<string, unknown>;
885
+ const body = (await res.json()) as Record<string, unknown>;
766
886
 
767
887
  expect(body.accepted).toBe(true);
768
888
  // No approval field — normal processing
@@ -774,118 +894,150 @@ describe('stale callback handling', () => {
774
894
  // 15. SMS channel approval decisions
775
895
  // ═══════════════════════════════════════════════════════════════════════════
776
896
 
777
- describe('SMS channel approval decisions', () => {
897
+ describe("SMS channel approval decisions", () => {
778
898
  beforeEach(() => {
779
899
  createBinding({
780
- assistantId: 'self',
781
- channel: 'sms',
782
- guardianExternalUserId: 'sms-user-default',
783
- guardianDeliveryChatId: 'sms-chat-123',
784
- guardianPrincipalId: 'sms-user-default',
900
+ assistantId: "self",
901
+ channel: "sms",
902
+ guardianExternalUserId: "sms-user-default",
903
+ guardianDeliveryChatId: "sms-chat-123",
904
+ guardianPrincipalId: "sms-user-default",
785
905
  });
786
906
  });
787
907
 
788
- function makeSmsInboundRequest(overrides: Record<string, unknown> = {}): Request {
908
+ function makeSmsInboundRequest(
909
+ overrides: Record<string, unknown> = {},
910
+ ): Request {
789
911
  const body = {
790
- sourceChannel: 'sms',
791
- interface: 'sms',
792
- conversationExternalId: 'sms-chat-123',
793
- actorExternalId: 'sms-user-default',
912
+ sourceChannel: "sms",
913
+ interface: "sms",
914
+ conversationExternalId: "sms-chat-123",
915
+ actorExternalId: "sms-user-default",
794
916
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
795
- content: 'hello',
796
- replyCallbackUrl: 'https://gateway.test/deliver',
917
+ content: "hello",
918
+ replyCallbackUrl: "https://gateway.test/deliver",
797
919
  ...overrides,
798
920
  };
799
- return new Request('http://localhost/channels/inbound', {
800
- method: 'POST',
921
+ return new Request("http://localhost/channels/inbound", {
922
+ method: "POST",
801
923
  headers: {
802
- 'Content-Type': 'application/json',
803
- 'X-Gateway-Origin': TEST_BEARER_TOKEN,
924
+ "Content-Type": "application/json",
804
925
  },
805
926
  body: JSON.stringify(body),
806
927
  });
807
928
  }
808
929
 
809
930
  test('plain-text "yes" via SMS triggers approve_once decision', async () => {
810
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
931
+ const deliverSpy = spyOn(
932
+ gatewayClient,
933
+ "deliverChannelReply",
934
+ ).mockResolvedValue(undefined);
811
935
 
812
936
  // Establish the conversation via SMS
813
- const initReq = makeSmsInboundRequest({ content: 'init' });
937
+ const initReq = makeSmsInboundRequest({ content: "init" });
814
938
  await handleChannelInbound(initReq, noopProcessMessage);
815
939
 
816
940
  const db = getDb();
817
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
941
+ const events = db.$client
942
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
943
+ .all() as Array<{ conversation_id: string }>;
818
944
  const conversationId = events[events.length - 1]?.conversation_id;
819
945
  ensureConversation(conversationId!);
820
946
 
821
- const sessionMock = registerPendingInteraction('req-sms-1', conversationId!, 'shell');
947
+ const sessionMock = registerPendingInteraction(
948
+ "req-sms-1",
949
+ conversationId!,
950
+ "shell",
951
+ );
822
952
 
823
- const req = makeSmsInboundRequest({ content: 'yes' });
953
+ const req = makeSmsInboundRequest({ content: "yes" });
824
954
  const res = await handleChannelInbound(req, noopProcessMessage);
825
- const body = await res.json() as Record<string, unknown>;
955
+ const body = (await res.json()) as Record<string, unknown>;
826
956
 
827
957
  expect(body.accepted).toBe(true);
828
- expect(body.approval).toBe('decision_applied');
829
- expect(sessionMock).toHaveBeenCalledWith('req-sms-1', 'allow');
958
+ expect(body.approval).toBe("decision_applied");
959
+ expect(sessionMock).toHaveBeenCalledWith("req-sms-1", "allow");
830
960
 
831
961
  deliverSpy.mockRestore();
832
962
  });
833
963
 
834
964
  test('plain-text "no" via SMS triggers reject decision', async () => {
835
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
965
+ const deliverSpy = spyOn(
966
+ gatewayClient,
967
+ "deliverChannelReply",
968
+ ).mockResolvedValue(undefined);
836
969
 
837
- const initReq = makeSmsInboundRequest({ content: 'init' });
970
+ const initReq = makeSmsInboundRequest({ content: "init" });
838
971
  await handleChannelInbound(initReq, noopProcessMessage);
839
972
 
840
973
  const db = getDb();
841
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
974
+ const events = db.$client
975
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
976
+ .all() as Array<{ conversation_id: string }>;
842
977
  const conversationId = events[events.length - 1]?.conversation_id;
843
978
  ensureConversation(conversationId!);
844
979
 
845
- const sessionMock = registerPendingInteraction('req-sms-2', conversationId!, 'shell');
980
+ const sessionMock = registerPendingInteraction(
981
+ "req-sms-2",
982
+ conversationId!,
983
+ "shell",
984
+ );
846
985
 
847
- const req = makeSmsInboundRequest({ content: 'no' });
986
+ const req = makeSmsInboundRequest({ content: "no" });
848
987
  const res = await handleChannelInbound(req, noopProcessMessage);
849
- const body = await res.json() as Record<string, unknown>;
988
+ const body = (await res.json()) as Record<string, unknown>;
850
989
 
851
990
  expect(body.accepted).toBe(true);
852
- expect(body.approval).toBe('decision_applied');
853
- expect(sessionMock).toHaveBeenCalledWith('req-sms-2', 'deny');
991
+ expect(body.approval).toBe("decision_applied");
992
+ expect(sessionMock).toHaveBeenCalledWith("req-sms-2", "deny");
854
993
 
855
994
  deliverSpy.mockRestore();
856
995
  });
857
996
 
858
- test('non-decision SMS message during pending approval sends status reply', async () => {
859
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
860
- const approvalSpy = spyOn(gatewayClient, 'deliverApprovalPrompt').mockResolvedValue(undefined);
997
+ test("non-decision SMS message during pending approval sends status reply", async () => {
998
+ const deliverSpy = spyOn(
999
+ gatewayClient,
1000
+ "deliverChannelReply",
1001
+ ).mockResolvedValue(undefined);
1002
+ const approvalSpy = spyOn(
1003
+ gatewayClient,
1004
+ "deliverApprovalPrompt",
1005
+ ).mockResolvedValue(undefined);
861
1006
 
862
- const initReq = makeSmsInboundRequest({ content: 'init' });
1007
+ const initReq = makeSmsInboundRequest({ content: "init" });
863
1008
  await handleChannelInbound(initReq, noopProcessMessage);
864
1009
 
865
1010
  const db = getDb();
866
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
1011
+ const events = db.$client
1012
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
1013
+ .all() as Array<{ conversation_id: string }>;
867
1014
  const conversationId = events[events.length - 1]?.conversation_id;
868
1015
  ensureConversation(conversationId!);
869
1016
 
870
- registerPendingInteraction('req-sms-3', conversationId!, 'shell');
1017
+ registerPendingInteraction("req-sms-3", conversationId!, "shell");
871
1018
 
872
- const req = makeSmsInboundRequest({ content: 'what is happening?' });
1019
+ const req = makeSmsInboundRequest({ content: "what is happening?" });
873
1020
  const res = await handleChannelInbound(req, noopProcessMessage);
874
- const body = await res.json() as Record<string, unknown>;
1021
+ const body = (await res.json()) as Record<string, unknown>;
875
1022
 
876
1023
  expect(body.accepted).toBe(true);
877
- expect(body.approval).toBe('assistant_turn');
1024
+ expect(body.approval).toBe("assistant_turn");
878
1025
 
879
1026
  // SMS non-decision: status reply delivered via plain text
880
1027
  expect(deliverSpy).toHaveBeenCalled();
881
1028
  expect(approvalSpy).not.toHaveBeenCalled();
882
1029
  const statusCall = deliverSpy.mock.calls.find(
883
- (call) => typeof call[1] === 'object' && (call[1] as { chatId?: string }).chatId === 'sms-chat-123',
1030
+ (call) =>
1031
+ typeof call[1] === "object" &&
1032
+ (call[1] as { chatId?: string }).chatId === "sms-chat-123",
884
1033
  );
885
1034
  expect(statusCall).toBeDefined();
886
- const statusPayload = statusCall![1] as { text?: string; approval?: unknown };
887
- const deliveredText = statusPayload.text ?? '';
888
- expect(deliveredText).toContain('pending approval request');
1035
+ const statusPayload = statusCall![1] as {
1036
+ text?: string;
1037
+ approval?: unknown;
1038
+ };
1039
+ const deliveredText = statusPayload.text ?? "";
1040
+ expect(deliveredText).toContain("pending approval request");
889
1041
  expect(statusPayload.approval).toBeUndefined();
890
1042
 
891
1043
  deliverSpy.mockRestore();
@@ -897,97 +1049,104 @@ describe('SMS channel approval decisions', () => {
897
1049
  // 16. SMS guardian verify intercept
898
1050
  // ═══════════════════════════════════════════════════════════════════════════
899
1051
 
900
- describe('SMS guardian verify intercept', () => {
901
- test('verification code reply works with sourceChannel sms', async () => {
902
- const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
903
- const { secret } = createVerificationChallenge('self', 'sms');
1052
+ describe("SMS guardian verify intercept", () => {
1053
+ test("verification code reply works with sourceChannel sms", async () => {
1054
+ const { createVerificationChallenge } =
1055
+ await import("../runtime/channel-guardian-service.js");
1056
+ const { secret } = createVerificationChallenge("self", "sms");
904
1057
 
905
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1058
+ const deliverSpy = spyOn(
1059
+ gatewayClient,
1060
+ "deliverChannelReply",
1061
+ ).mockResolvedValue(undefined);
906
1062
 
907
- const req = new Request('http://localhost/channels/inbound', {
908
- method: 'POST',
1063
+ const req = new Request("http://localhost/channels/inbound", {
1064
+ method: "POST",
909
1065
  headers: {
910
- 'Content-Type': 'application/json',
911
- 'X-Gateway-Origin': TEST_BEARER_TOKEN,
1066
+ "Content-Type": "application/json",
912
1067
  },
913
1068
  body: JSON.stringify({
914
- sourceChannel: 'sms',
915
- interface: 'sms',
916
- conversationExternalId: 'sms-chat-verify',
1069
+ sourceChannel: "sms",
1070
+ interface: "sms",
1071
+ conversationExternalId: "sms-chat-verify",
917
1072
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
918
1073
  content: secret,
919
- actorExternalId: 'sms-user-42',
920
- replyCallbackUrl: 'https://gateway.test/deliver',
1074
+ actorExternalId: "sms-user-42",
1075
+ replyCallbackUrl: "https://gateway.test/deliver",
921
1076
  }),
922
1077
  });
923
1078
 
924
1079
  const res = await handleChannelInbound(req, noopProcessMessage);
925
- const body = await res.json() as Record<string, unknown>;
1080
+ const body = (await res.json()) as Record<string, unknown>;
926
1081
 
927
1082
  expect(body.accepted).toBe(true);
928
- expect(body.guardianVerification).toBe('verified');
1083
+ expect(body.guardianVerification).toBe("verified");
929
1084
 
930
1085
  expect(deliverSpy).toHaveBeenCalled();
931
1086
  const replyArgs = deliverSpy.mock.calls[0];
932
1087
  const replyPayload = replyArgs[1] as { chatId: string; text: string };
933
- expect(replyPayload.chatId).toBe('sms-chat-verify');
934
- expect(typeof replyPayload.text).toBe('string');
935
- expect(replyPayload.text.toLowerCase()).toContain('guardian');
936
- expect(replyPayload.text.toLowerCase()).toContain('verif');
1088
+ expect(replyPayload.chatId).toBe("sms-chat-verify");
1089
+ expect(typeof replyPayload.text).toBe("string");
1090
+ expect(replyPayload.text.toLowerCase()).toContain("guardian");
1091
+ expect(replyPayload.text.toLowerCase()).toContain("verif");
937
1092
 
938
1093
  deliverSpy.mockRestore();
939
1094
  });
940
1095
 
941
- test('invalid verification code returns failed via SMS', async () => {
942
- const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
1096
+ test("invalid verification code returns failed via SMS", async () => {
1097
+ const { createVerificationChallenge } =
1098
+ await import("../runtime/channel-guardian-service.js");
943
1099
  // Ensure there is a pending challenge so bare-code verification is intercepted.
944
- createVerificationChallenge('self', 'sms');
1100
+ createVerificationChallenge("self", "sms");
945
1101
 
946
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1102
+ const deliverSpy = spyOn(
1103
+ gatewayClient,
1104
+ "deliverChannelReply",
1105
+ ).mockResolvedValue(undefined);
947
1106
 
948
- const req = new Request('http://localhost/channels/inbound', {
949
- method: 'POST',
1107
+ const req = new Request("http://localhost/channels/inbound", {
1108
+ method: "POST",
950
1109
  headers: {
951
- 'Content-Type': 'application/json',
952
- 'X-Gateway-Origin': TEST_BEARER_TOKEN,
1110
+ "Content-Type": "application/json",
953
1111
  },
954
1112
  body: JSON.stringify({
955
- sourceChannel: 'sms',
956
- interface: 'sms',
957
- conversationExternalId: 'sms-chat-verify-fail',
1113
+ sourceChannel: "sms",
1114
+ interface: "sms",
1115
+ conversationExternalId: "sms-chat-verify-fail",
958
1116
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
959
- content: '000000',
960
- actorExternalId: 'sms-user-43',
961
- replyCallbackUrl: 'https://gateway.test/deliver',
1117
+ content: "000000",
1118
+ actorExternalId: "sms-user-43",
1119
+ replyCallbackUrl: "https://gateway.test/deliver",
962
1120
  }),
963
1121
  });
964
1122
 
965
1123
  const res = await handleChannelInbound(req, noopProcessMessage);
966
- const body = await res.json() as Record<string, unknown>;
1124
+ const body = (await res.json()) as Record<string, unknown>;
967
1125
 
968
1126
  expect(body.accepted).toBe(true);
969
- expect(body.guardianVerification).toBe('failed');
1127
+ expect(body.guardianVerification).toBe("failed");
970
1128
 
971
1129
  expect(deliverSpy).toHaveBeenCalled();
972
1130
  const replyArgs = deliverSpy.mock.calls[0];
973
1131
  const replyPayload = replyArgs[1] as { chatId: string; text: string };
974
- expect(typeof replyPayload.text).toBe('string');
975
- expect(replyPayload.text.toLowerCase()).toContain('verif');
976
- expect(replyPayload.text.toLowerCase()).toContain('invalid');
1132
+ expect(typeof replyPayload.text).toBe("string");
1133
+ expect(replyPayload.text.toLowerCase()).toContain("verif");
1134
+ expect(replyPayload.text.toLowerCase()).toContain("invalid");
977
1135
 
978
1136
  deliverSpy.mockRestore();
979
1137
  });
980
1138
 
981
- test('64-char hex verification codes are intercepted when a pending challenge exists', async () => {
982
- const { createHash, randomBytes } = await import('node:crypto');
983
- const { createChallenge } = await import('../memory/channel-guardian-store.js');
1139
+ test("64-char hex verification codes are intercepted when a pending challenge exists", async () => {
1140
+ const { createHash, randomBytes } = await import("node:crypto");
1141
+ const { createChallenge } =
1142
+ await import("../memory/channel-guardian-store.js");
984
1143
 
985
- const secret = randomBytes(32).toString('hex');
986
- const challengeHash = createHash('sha256').update(secret).digest('hex');
1144
+ const secret = randomBytes(32).toString("hex");
1145
+ const challengeHash = createHash("sha256").update(secret).digest("hex");
987
1146
  createChallenge({
988
1147
  id: `challenge-hex-${Date.now()}`,
989
- assistantId: 'self',
990
- channel: 'sms',
1148
+ assistantId: "self",
1149
+ channel: "sms",
991
1150
  challengeHash,
992
1151
  expiresAt: Date.now() + 600_000,
993
1152
  });
@@ -995,31 +1154,30 @@ describe('SMS guardian verify intercept', () => {
995
1154
  let processMessageCalled = false;
996
1155
  const processMessage = async () => {
997
1156
  processMessageCalled = true;
998
- return { messageId: 'msg-hex-not-verify' };
1157
+ return { messageId: "msg-hex-not-verify" };
999
1158
  };
1000
1159
 
1001
- const req = new Request('http://localhost/channels/inbound', {
1002
- method: 'POST',
1160
+ const req = new Request("http://localhost/channels/inbound", {
1161
+ method: "POST",
1003
1162
  headers: {
1004
- 'Content-Type': 'application/json',
1005
- 'X-Gateway-Origin': TEST_BEARER_TOKEN,
1163
+ "Content-Type": "application/json",
1006
1164
  },
1007
1165
  body: JSON.stringify({
1008
- sourceChannel: 'sms',
1009
- interface: 'sms',
1010
- conversationExternalId: 'sms-chat-hex-message',
1166
+ sourceChannel: "sms",
1167
+ interface: "sms",
1168
+ conversationExternalId: "sms-chat-hex-message",
1011
1169
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
1012
1170
  content: secret,
1013
- actorExternalId: 'sms-user-hex',
1014
- replyCallbackUrl: 'https://gateway.test/deliver',
1171
+ actorExternalId: "sms-user-hex",
1172
+ replyCallbackUrl: "https://gateway.test/deliver",
1015
1173
  }),
1016
1174
  });
1017
1175
 
1018
1176
  const res = await handleChannelInbound(req, processMessage);
1019
- const body = await res.json() as Record<string, unknown>;
1177
+ const body = (await res.json()) as Record<string, unknown>;
1020
1178
 
1021
1179
  expect(body.accepted).toBe(true);
1022
- expect(body.guardianVerification).toBe('verified');
1180
+ expect(body.guardianVerification).toBe("verified");
1023
1181
  expect(processMessageCalled).toBe(false);
1024
1182
  });
1025
1183
  });
@@ -1028,68 +1186,79 @@ describe('SMS guardian verify intercept', () => {
1028
1186
  // 21. Guardian decision scoping — callback for older request
1029
1187
  // ═══════════════════════════════════════════════════════════════════════════
1030
1188
 
1031
- describe('guardian decision scoping — multiple pending approvals', () => {
1032
- test('callback for older request resolves to the correct approval request', async () => {
1189
+ describe("guardian decision scoping — multiple pending approvals", () => {
1190
+ test("callback for older request resolves to the correct approval request", async () => {
1033
1191
  createBinding({
1034
- assistantId: 'self',
1035
- channel: 'telegram',
1036
- guardianExternalUserId: 'guardian-scope-user',
1037
- guardianDeliveryChatId: 'guardian-scope-chat',
1038
- guardianPrincipalId: 'guardian-scope-user',
1192
+ assistantId: "self",
1193
+ channel: "telegram",
1194
+ guardianExternalUserId: "guardian-scope-user",
1195
+ guardianDeliveryChatId: "guardian-scope-chat",
1196
+ guardianPrincipalId: "guardian-scope-user",
1039
1197
  });
1040
1198
 
1041
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1199
+ const deliverSpy = spyOn(
1200
+ gatewayClient,
1201
+ "deliverChannelReply",
1202
+ ).mockResolvedValue(undefined);
1042
1203
 
1043
- const olderConvId = 'conv-scope-older';
1044
- const newerConvId = 'conv-scope-newer';
1204
+ const olderConvId = "conv-scope-older";
1205
+ const newerConvId = "conv-scope-newer";
1045
1206
  ensureConversation(olderConvId);
1046
1207
  ensureConversation(newerConvId);
1047
1208
 
1048
1209
  // Register pending interactions and create guardian approval requests
1049
- const olderSession = registerPendingInteraction('req-older', olderConvId, 'shell');
1210
+ const olderSession = registerPendingInteraction(
1211
+ "req-older",
1212
+ olderConvId,
1213
+ "shell",
1214
+ );
1050
1215
  createApprovalRequest({
1051
- runId: 'run-older',
1052
- requestId: 'req-older',
1216
+ runId: "run-older",
1217
+ requestId: "req-older",
1053
1218
  conversationId: olderConvId,
1054
- channel: 'telegram',
1055
- requesterExternalUserId: 'requester-a',
1056
- requesterChatId: 'chat-requester-a',
1057
- guardianExternalUserId: 'guardian-scope-user',
1058
- guardianChatId: 'guardian-scope-chat',
1059
- toolName: 'shell',
1219
+ channel: "telegram",
1220
+ requesterExternalUserId: "requester-a",
1221
+ requesterChatId: "chat-requester-a",
1222
+ guardianExternalUserId: "guardian-scope-user",
1223
+ guardianChatId: "guardian-scope-chat",
1224
+ toolName: "shell",
1060
1225
  expiresAt: Date.now() + 300_000,
1061
1226
  });
1062
1227
 
1063
- const newerSession = registerPendingInteraction('req-newer', newerConvId, 'browser');
1228
+ const newerSession = registerPendingInteraction(
1229
+ "req-newer",
1230
+ newerConvId,
1231
+ "browser",
1232
+ );
1064
1233
  createApprovalRequest({
1065
- runId: 'run-newer',
1066
- requestId: 'req-newer',
1234
+ runId: "run-newer",
1235
+ requestId: "req-newer",
1067
1236
  conversationId: newerConvId,
1068
- channel: 'telegram',
1069
- requesterExternalUserId: 'requester-b',
1070
- requesterChatId: 'chat-requester-b',
1071
- guardianExternalUserId: 'guardian-scope-user',
1072
- guardianChatId: 'guardian-scope-chat',
1073
- toolName: 'browser',
1237
+ channel: "telegram",
1238
+ requesterExternalUserId: "requester-b",
1239
+ requesterChatId: "chat-requester-b",
1240
+ guardianExternalUserId: "guardian-scope-user",
1241
+ guardianChatId: "guardian-scope-chat",
1242
+ toolName: "browser",
1074
1243
  expiresAt: Date.now() + 300_000,
1075
1244
  });
1076
1245
 
1077
1246
  // The guardian clicks the approval button for the OLDER request
1078
1247
  const req = makeInboundRequest({
1079
- content: '',
1080
- conversationExternalId: 'guardian-scope-chat',
1081
- callbackData: 'apr:req-older:approve_once',
1082
- actorExternalId: 'guardian-scope-user',
1248
+ content: "",
1249
+ conversationExternalId: "guardian-scope-chat",
1250
+ callbackData: "apr:req-older:approve_once",
1251
+ actorExternalId: "guardian-scope-user",
1083
1252
  });
1084
1253
 
1085
1254
  const res = await handleChannelInbound(req, noopProcessMessage);
1086
- const body = await res.json() as Record<string, unknown>;
1255
+ const body = (await res.json()) as Record<string, unknown>;
1087
1256
 
1088
1257
  expect(body.accepted).toBe(true);
1089
- expect(body.approval).toBe('guardian_decision_applied');
1258
+ expect(body.approval).toBe("guardian_decision_applied");
1090
1259
 
1091
1260
  // The older request's session should have been called
1092
- expect(olderSession).toHaveBeenCalledWith('req-older', 'allow');
1261
+ expect(olderSession).toHaveBeenCalledWith("req-older", "allow");
1093
1262
 
1094
1263
  // The newer request's session should NOT have been called
1095
1264
  expect(newerSession).not.toHaveBeenCalled();
@@ -1102,71 +1271,82 @@ describe('guardian decision scoping — multiple pending approvals', () => {
1102
1271
  // 22. Ambiguous plain-text decision with multiple pending requests
1103
1272
  // ═══════════════════════════════════════════════════════════════════════════
1104
1273
 
1105
- describe('ambiguous plain-text decision with multiple pending requests', () => {
1106
- test('does not apply plain-text decision to wrong request when multiple pending', async () => {
1274
+ describe("ambiguous plain-text decision with multiple pending requests", () => {
1275
+ test("does not apply plain-text decision to wrong request when multiple pending", async () => {
1107
1276
  createBinding({
1108
- assistantId: 'self',
1109
- channel: 'telegram',
1110
- guardianExternalUserId: 'guardian-ambig-user',
1111
- guardianDeliveryChatId: 'guardian-ambig-chat',
1112
- guardianPrincipalId: 'guardian-ambig-user',
1277
+ assistantId: "self",
1278
+ channel: "telegram",
1279
+ guardianExternalUserId: "guardian-ambig-user",
1280
+ guardianDeliveryChatId: "guardian-ambig-chat",
1281
+ guardianPrincipalId: "guardian-ambig-user",
1113
1282
  });
1114
1283
 
1115
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1284
+ const deliverSpy = spyOn(
1285
+ gatewayClient,
1286
+ "deliverChannelReply",
1287
+ ).mockResolvedValue(undefined);
1116
1288
 
1117
- const convA = 'conv-ambig-a';
1118
- const convB = 'conv-ambig-b';
1289
+ const convA = "conv-ambig-a";
1290
+ const convB = "conv-ambig-b";
1119
1291
  ensureConversation(convA);
1120
1292
  ensureConversation(convB);
1121
1293
 
1122
- const sessionA = registerPendingInteraction('req-ambig-a', convA, 'shell');
1294
+ const sessionA = registerPendingInteraction("req-ambig-a", convA, "shell");
1123
1295
  createApprovalRequest({
1124
- runId: 'run-ambig-a',
1125
- requestId: 'req-ambig-a',
1296
+ runId: "run-ambig-a",
1297
+ requestId: "req-ambig-a",
1126
1298
  conversationId: convA,
1127
- channel: 'telegram',
1128
- requesterExternalUserId: 'requester-x',
1129
- requesterChatId: 'chat-requester-x',
1130
- guardianExternalUserId: 'guardian-ambig-user',
1131
- guardianChatId: 'guardian-ambig-chat',
1132
- toolName: 'shell',
1299
+ channel: "telegram",
1300
+ requesterExternalUserId: "requester-x",
1301
+ requesterChatId: "chat-requester-x",
1302
+ guardianExternalUserId: "guardian-ambig-user",
1303
+ guardianChatId: "guardian-ambig-chat",
1304
+ toolName: "shell",
1133
1305
  expiresAt: Date.now() + 300_000,
1134
1306
  });
1135
1307
 
1136
- const sessionB = registerPendingInteraction('req-ambig-b', convB, 'browser');
1308
+ const sessionB = registerPendingInteraction(
1309
+ "req-ambig-b",
1310
+ convB,
1311
+ "browser",
1312
+ );
1137
1313
  createApprovalRequest({
1138
- runId: 'run-ambig-b',
1139
- requestId: 'req-ambig-b',
1314
+ runId: "run-ambig-b",
1315
+ requestId: "req-ambig-b",
1140
1316
  conversationId: convB,
1141
- channel: 'telegram',
1142
- requesterExternalUserId: 'requester-y',
1143
- requesterChatId: 'chat-requester-y',
1144
- guardianExternalUserId: 'guardian-ambig-user',
1145
- guardianChatId: 'guardian-ambig-chat',
1146
- toolName: 'browser',
1317
+ channel: "telegram",
1318
+ requesterExternalUserId: "requester-y",
1319
+ requesterChatId: "chat-requester-y",
1320
+ guardianExternalUserId: "guardian-ambig-user",
1321
+ guardianChatId: "guardian-ambig-chat",
1322
+ toolName: "browser",
1147
1323
  expiresAt: Date.now() + 300_000,
1148
1324
  });
1149
1325
 
1150
1326
  // Conversational engine that returns keep_pending for disambiguation
1151
1327
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
1152
- disposition: 'keep_pending' as const,
1153
- replyText: 'You have 2 pending requests. Which one?',
1328
+ disposition: "keep_pending" as const,
1329
+ replyText: "You have 2 pending requests. Which one?",
1154
1330
  }));
1155
1331
 
1156
1332
  // Guardian sends plain-text "yes" — ambiguous because two approvals are pending
1157
1333
  const req = makeInboundRequest({
1158
- content: 'yes',
1159
- conversationExternalId: 'guardian-ambig-chat',
1160
- actorExternalId: 'guardian-ambig-user',
1334
+ content: "yes",
1335
+ conversationExternalId: "guardian-ambig-chat",
1336
+ actorExternalId: "guardian-ambig-user",
1161
1337
  });
1162
1338
 
1163
1339
  const res = await handleChannelInbound(
1164
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
1340
+ req,
1341
+ noopProcessMessage,
1342
+ "self",
1343
+ undefined,
1344
+ mockConversationGenerator,
1165
1345
  );
1166
- const body = await res.json() as Record<string, unknown>;
1346
+ const body = (await res.json()) as Record<string, unknown>;
1167
1347
 
1168
1348
  expect(body.accepted).toBe(true);
1169
- expect(body.approval).toBe('assistant_turn');
1349
+ expect(body.approval).toBe("assistant_turn");
1170
1350
 
1171
1351
  // Neither session should have been called — disambiguation was required
1172
1352
  expect(sessionA).not.toHaveBeenCalled();
@@ -1174,8 +1354,11 @@ describe('ambiguous plain-text decision with multiple pending requests', () => {
1174
1354
 
1175
1355
  // The conversational engine should have been called with both pending approvals
1176
1356
  expect(mockConversationGenerator).toHaveBeenCalledTimes(1);
1177
- const engineCtx = mockConversationGenerator.mock.calls[0][0] as Record<string, unknown>;
1178
- expect((engineCtx.pendingApprovals as Array<unknown>)).toHaveLength(2);
1357
+ const engineCtx = mockConversationGenerator.mock.calls[0][0] as Record<
1358
+ string,
1359
+ unknown
1360
+ >;
1361
+ expect(engineCtx.pendingApprovals as Array<unknown>).toHaveLength(2);
1179
1362
 
1180
1363
  deliverSpy.mockRestore();
1181
1364
  });
@@ -1185,84 +1368,96 @@ describe('ambiguous plain-text decision with multiple pending requests', () => {
1185
1368
  // 23. Expired guardian approval auto-denies and transitions to terminal status
1186
1369
  // ═══════════════════════════════════════════════════════════════════════════
1187
1370
 
1188
- describe('expired guardian approval auto-denies via sweep', () => {
1189
- test('sweepExpiredGuardianApprovals auto-denies and notifies both parties', async () => {
1190
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1371
+ describe("expired guardian approval auto-denies via sweep", () => {
1372
+ test("sweepExpiredGuardianApprovals auto-denies and notifies both parties", async () => {
1373
+ const deliverSpy = spyOn(
1374
+ gatewayClient,
1375
+ "deliverChannelReply",
1376
+ ).mockResolvedValue(undefined);
1191
1377
 
1192
- const convId = 'conv-expiry-sweep';
1378
+ const convId = "conv-expiry-sweep";
1193
1379
  ensureConversation(convId);
1194
1380
 
1195
1381
  // Register a pending interaction so the sweep can resolve the session
1196
- const sessionMock = registerPendingInteraction('req-exp-1', convId, 'shell');
1382
+ const sessionMock = registerPendingInteraction(
1383
+ "req-exp-1",
1384
+ convId,
1385
+ "shell",
1386
+ );
1197
1387
 
1198
1388
  createApprovalRequest({
1199
- runId: 'run-exp-1',
1200
- requestId: 'req-exp-1',
1389
+ runId: "run-exp-1",
1390
+ requestId: "req-exp-1",
1201
1391
  conversationId: convId,
1202
- channel: 'telegram',
1203
- requesterExternalUserId: 'requester-exp',
1204
- requesterChatId: 'chat-requester-exp',
1205
- guardianExternalUserId: 'guardian-exp-user',
1206
- guardianChatId: 'guardian-exp-chat',
1207
- toolName: 'shell',
1392
+ channel: "telegram",
1393
+ requesterExternalUserId: "requester-exp",
1394
+ requesterChatId: "chat-requester-exp",
1395
+ guardianExternalUserId: "guardian-exp-user",
1396
+ guardianChatId: "guardian-exp-chat",
1397
+ toolName: "shell",
1208
1398
  expiresAt: Date.now() - 1000, // already expired
1209
1399
  });
1210
1400
 
1211
1401
  // Run the sweep
1212
- sweepExpiredGuardianApprovals('https://gateway.test', () => 'token');
1402
+ sweepExpiredGuardianApprovals("https://gateway.test", () => "token");
1213
1403
 
1214
1404
  // Wait for async notifications
1215
1405
  await new Promise((resolve) => setTimeout(resolve, 200));
1216
1406
 
1217
1407
  // The session should have been denied
1218
- expect(sessionMock).toHaveBeenCalledWith('req-exp-1', 'deny');
1408
+ expect(sessionMock).toHaveBeenCalledWith("req-exp-1", "deny");
1219
1409
 
1220
1410
  // Both requester and guardian should have been notified
1221
1411
  const requesterNotify = deliverSpy.mock.calls.filter(
1222
- (call) => typeof call[1] === 'object' &&
1223
- (call[1] as { chatId?: string }).chatId === 'chat-requester-exp' &&
1224
- (call[1] as { text?: string }).text?.includes('expired'),
1412
+ (call) =>
1413
+ typeof call[1] === "object" &&
1414
+ (call[1] as { chatId?: string }).chatId === "chat-requester-exp" &&
1415
+ (call[1] as { text?: string }).text?.includes("expired"),
1225
1416
  );
1226
1417
  expect(requesterNotify.length).toBeGreaterThanOrEqual(1);
1227
1418
 
1228
1419
  const guardianNotify = deliverSpy.mock.calls.filter(
1229
- (call) => typeof call[1] === 'object' &&
1230
- (call[1] as { chatId?: string }).chatId === 'guardian-exp-chat' &&
1231
- (call[1] as { text?: string }).text?.includes('expired'),
1420
+ (call) =>
1421
+ typeof call[1] === "object" &&
1422
+ (call[1] as { chatId?: string }).chatId === "guardian-exp-chat" &&
1423
+ (call[1] as { text?: string }).text?.includes("expired"),
1232
1424
  );
1233
1425
  expect(guardianNotify.length).toBeGreaterThanOrEqual(1);
1234
1426
 
1235
1427
  // Verify the delivery URL is constructed per-channel
1236
1428
  const allDeliverCalls = deliverSpy.mock.calls;
1237
1429
  for (const call of allDeliverCalls) {
1238
- expect(call[0]).toBe('https://gateway.test/deliver/telegram');
1430
+ expect(call[0]).toBe("https://gateway.test/deliver/telegram");
1239
1431
  }
1240
1432
 
1241
1433
  deliverSpy.mockRestore();
1242
1434
  });
1243
1435
 
1244
- test('non-expired approvals are not affected by the sweep', async () => {
1245
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1436
+ test("non-expired approvals are not affected by the sweep", async () => {
1437
+ const deliverSpy = spyOn(
1438
+ gatewayClient,
1439
+ "deliverChannelReply",
1440
+ ).mockResolvedValue(undefined);
1246
1441
 
1247
- const convId = 'conv-not-expired';
1442
+ const convId = "conv-not-expired";
1248
1443
  ensureConversation(convId);
1249
1444
 
1250
- const sessionMock = registerPendingInteraction('req-ne-1', convId, 'shell');
1445
+ const sessionMock = registerPendingInteraction("req-ne-1", convId, "shell");
1251
1446
 
1252
1447
  createApprovalRequest({
1253
- runId: 'run-ne-1',
1254
- requestId: 'req-ne-1',
1448
+ runId: "run-ne-1",
1449
+ requestId: "req-ne-1",
1255
1450
  conversationId: convId,
1256
- channel: 'telegram',
1257
- requesterExternalUserId: 'requester-ne',
1258
- requesterChatId: 'chat-requester-ne',
1259
- guardianExternalUserId: 'guardian-ne-user',
1260
- guardianChatId: 'guardian-ne-chat',
1261
- toolName: 'shell',
1451
+ channel: "telegram",
1452
+ requesterExternalUserId: "requester-ne",
1453
+ requesterChatId: "chat-requester-ne",
1454
+ guardianExternalUserId: "guardian-ne-user",
1455
+ guardianChatId: "guardian-ne-chat",
1456
+ toolName: "shell",
1262
1457
  expiresAt: Date.now() + 300_000, // still valid
1263
1458
  });
1264
1459
 
1265
- sweepExpiredGuardianApprovals('https://gateway.test', () => 'token');
1460
+ sweepExpiredGuardianApprovals("https://gateway.test", () => "token");
1266
1461
 
1267
1462
  await new Promise((resolve) => setTimeout(resolve, 200));
1268
1463
 
@@ -1277,26 +1472,26 @@ describe('expired guardian approval auto-denies via sweep', () => {
1277
1472
  // 24. Deliver-once idempotency guard
1278
1473
  // ═══════════════════════════════════════════════════════════════════════════
1279
1474
 
1280
- describe('deliver-once idempotency guard', () => {
1281
- test('claimRunDelivery returns true on first call, false on subsequent calls', () => {
1282
- const runId = 'run-idem-unit';
1475
+ describe("deliver-once idempotency guard", () => {
1476
+ test("claimRunDelivery returns true on first call, false on subsequent calls", () => {
1477
+ const runId = "run-idem-unit";
1283
1478
  expect(channelDeliveryStore.claimRunDelivery(runId)).toBe(true);
1284
1479
  expect(channelDeliveryStore.claimRunDelivery(runId)).toBe(false);
1285
1480
  expect(channelDeliveryStore.claimRunDelivery(runId)).toBe(false);
1286
1481
  channelDeliveryStore.resetRunDeliveryClaim(runId);
1287
1482
  });
1288
1483
 
1289
- test('different run IDs are independent', () => {
1290
- expect(channelDeliveryStore.claimRunDelivery('run-a')).toBe(true);
1291
- expect(channelDeliveryStore.claimRunDelivery('run-b')).toBe(true);
1292
- expect(channelDeliveryStore.claimRunDelivery('run-a')).toBe(false);
1293
- expect(channelDeliveryStore.claimRunDelivery('run-b')).toBe(false);
1294
- channelDeliveryStore.resetRunDeliveryClaim('run-a');
1295
- channelDeliveryStore.resetRunDeliveryClaim('run-b');
1484
+ test("different run IDs are independent", () => {
1485
+ expect(channelDeliveryStore.claimRunDelivery("run-a")).toBe(true);
1486
+ expect(channelDeliveryStore.claimRunDelivery("run-b")).toBe(true);
1487
+ expect(channelDeliveryStore.claimRunDelivery("run-a")).toBe(false);
1488
+ expect(channelDeliveryStore.claimRunDelivery("run-b")).toBe(false);
1489
+ channelDeliveryStore.resetRunDeliveryClaim("run-a");
1490
+ channelDeliveryStore.resetRunDeliveryClaim("run-b");
1296
1491
  });
1297
1492
 
1298
- test('resetRunDeliveryClaim allows re-claim', () => {
1299
- const runId = 'run-idem-reset';
1493
+ test("resetRunDeliveryClaim allows re-claim", () => {
1494
+ const runId = "run-idem-reset";
1300
1495
  expect(channelDeliveryStore.claimRunDelivery(runId)).toBe(true);
1301
1496
  channelDeliveryStore.resetRunDeliveryClaim(runId);
1302
1497
  expect(channelDeliveryStore.claimRunDelivery(runId)).toBe(true);
@@ -1308,105 +1503,137 @@ describe('deliver-once idempotency guard', () => {
1308
1503
  // 26. Assistant-scoped guardian verification via handleChannelInbound
1309
1504
  // ═══════════════════════════════════════════════════════════════════════════
1310
1505
 
1311
- describe('assistant-scoped guardian verification via handleChannelInbound', () => {
1312
- test('verification code uses the threaded assistantId (default: self)', async () => {
1313
- const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
1314
- const { secret } = createVerificationChallenge('self', 'telegram');
1506
+ describe("assistant-scoped guardian verification via handleChannelInbound", () => {
1507
+ test("verification code uses the threaded assistantId (default: self)", async () => {
1508
+ const { createVerificationChallenge } =
1509
+ await import("../runtime/channel-guardian-service.js");
1510
+ const { secret } = createVerificationChallenge("self", "telegram");
1315
1511
 
1316
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1512
+ const deliverSpy = spyOn(
1513
+ gatewayClient,
1514
+ "deliverChannelReply",
1515
+ ).mockResolvedValue(undefined);
1317
1516
 
1318
1517
  const req = makeInboundRequest({
1319
1518
  content: secret,
1320
- actorExternalId: 'user-default-asst',
1519
+ actorExternalId: "user-default-asst",
1321
1520
  });
1322
1521
 
1323
1522
  const res = await handleChannelInbound(req, noopProcessMessage);
1324
- const body = await res.json() as Record<string, unknown>;
1523
+ const body = (await res.json()) as Record<string, unknown>;
1325
1524
 
1326
1525
  expect(body.accepted).toBe(true);
1327
- expect(body.guardianVerification).toBe('verified');
1526
+ expect(body.guardianVerification).toBe("verified");
1328
1527
 
1329
1528
  deliverSpy.mockRestore();
1330
1529
  });
1331
1530
 
1332
- test('verification code with explicit assistantId resolves against canonical scope', async () => {
1333
- const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
1334
- const { getGuardianBinding } = await import('../runtime/channel-guardian-service.js');
1531
+ test("verification code with explicit assistantId resolves against canonical scope", async () => {
1532
+ const { createVerificationChallenge } =
1533
+ await import("../runtime/channel-guardian-service.js");
1534
+ const { getGuardianBinding } =
1535
+ await import("../runtime/channel-guardian-service.js");
1335
1536
 
1336
1537
  // All assistant IDs canonicalize to 'self' in the single-tenant daemon
1337
- const { secret } = createVerificationChallenge('self', 'telegram');
1538
+ const { secret } = createVerificationChallenge("self", "telegram");
1338
1539
 
1339
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1540
+ const deliverSpy = spyOn(
1541
+ gatewayClient,
1542
+ "deliverChannelReply",
1543
+ ).mockResolvedValue(undefined);
1340
1544
 
1341
1545
  const req = makeInboundRequest({
1342
1546
  content: secret,
1343
- actorExternalId: 'user-for-asst-x',
1547
+ actorExternalId: "user-for-asst-x",
1344
1548
  });
1345
1549
 
1346
- const res = await handleChannelInbound(req, noopProcessMessage, 'asst-route-X');
1347
- const body = await res.json() as Record<string, unknown>;
1550
+ const res = await handleChannelInbound(
1551
+ req,
1552
+ noopProcessMessage,
1553
+ "asst-route-X",
1554
+ );
1555
+ const body = (await res.json()) as Record<string, unknown>;
1348
1556
 
1349
1557
  expect(body.accepted).toBe(true);
1350
- expect(body.guardianVerification).toBe('verified');
1558
+ expect(body.guardianVerification).toBe("verified");
1351
1559
 
1352
- const bindingX = getGuardianBinding('self', 'telegram');
1560
+ const bindingX = getGuardianBinding("self", "telegram");
1353
1561
  expect(bindingX).not.toBeNull();
1354
- expect(bindingX!.guardianExternalUserId).toBe('user-for-asst-x');
1562
+ expect(bindingX!.guardianExternalUserId).toBe("user-for-asst-x");
1355
1563
 
1356
1564
  deliverSpy.mockRestore();
1357
1565
  });
1358
1566
 
1359
- test('all assistant IDs share canonical scope for verification', async () => {
1360
- const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
1567
+ test("all assistant IDs share canonical scope for verification", async () => {
1568
+ const { createVerificationChallenge } =
1569
+ await import("../runtime/channel-guardian-service.js");
1361
1570
 
1362
1571
  // Both IDs canonicalize to 'self', so the challenge is found
1363
- const { secret } = createVerificationChallenge('self', 'telegram');
1572
+ const { secret } = createVerificationChallenge("self", "telegram");
1364
1573
 
1365
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1574
+ const deliverSpy = spyOn(
1575
+ gatewayClient,
1576
+ "deliverChannelReply",
1577
+ ).mockResolvedValue(undefined);
1366
1578
 
1367
1579
  const req = makeInboundRequest({
1368
1580
  content: secret,
1369
- actorExternalId: 'user-cross-test',
1581
+ actorExternalId: "user-cross-test",
1370
1582
  });
1371
1583
 
1372
- const res = await handleChannelInbound(req, noopProcessMessage, 'asst-B-cross');
1373
- const body = await res.json() as Record<string, unknown>;
1584
+ const res = await handleChannelInbound(
1585
+ req,
1586
+ noopProcessMessage,
1587
+ "asst-B-cross",
1588
+ );
1589
+ const body = (await res.json()) as Record<string, unknown>;
1374
1590
 
1375
1591
  expect(body.accepted).toBe(true);
1376
- expect(body.guardianVerification).toBe('verified');
1592
+ expect(body.guardianVerification).toBe("verified");
1377
1593
 
1378
1594
  deliverSpy.mockRestore();
1379
1595
  });
1380
1596
 
1381
- test('inbound with explicit assistantId does not mutate existing external bindings', async () => {
1597
+ test("inbound with explicit assistantId does not mutate existing external bindings", async () => {
1382
1598
  const db = getDb();
1383
1599
  const now = Date.now();
1384
- ensureConversation('conv-existing-binding');
1385
- db.insert(externalConversationBindings).values({
1386
- conversationId: 'conv-existing-binding',
1387
- sourceChannel: 'telegram',
1388
- externalChatId: 'chat-existing-999',
1389
- externalUserId: 'existing-user',
1390
- createdAt: now,
1391
- updatedAt: now,
1392
- lastInboundAt: now,
1393
- }).run();
1600
+ ensureConversation("conv-existing-binding");
1601
+ db.insert(externalConversationBindings)
1602
+ .values({
1603
+ conversationId: "conv-existing-binding",
1604
+ sourceChannel: "telegram",
1605
+ externalChatId: "chat-existing-999",
1606
+ externalUserId: "existing-user",
1607
+ createdAt: now,
1608
+ updatedAt: now,
1609
+ lastInboundAt: now,
1610
+ })
1611
+ .run();
1394
1612
 
1395
1613
  const req = makeInboundRequest({
1396
- content: 'hello from non-self assistant',
1397
- actorExternalId: 'incoming-user',
1614
+ content: "hello from non-self assistant",
1615
+ actorExternalId: "incoming-user",
1398
1616
  });
1399
1617
 
1400
- const res = await handleChannelInbound(req, noopProcessMessage, 'asst-non-self');
1618
+ const res = await handleChannelInbound(
1619
+ req,
1620
+ noopProcessMessage,
1621
+ "asst-non-self",
1622
+ );
1401
1623
  expect(res.status).toBe(200);
1402
1624
 
1403
1625
  const binding = db
1404
1626
  .select()
1405
1627
  .from(externalConversationBindings)
1406
- .where(eq(externalConversationBindings.conversationId, 'conv-existing-binding'))
1628
+ .where(
1629
+ eq(
1630
+ externalConversationBindings.conversationId,
1631
+ "conv-existing-binding",
1632
+ ),
1633
+ )
1407
1634
  .get();
1408
1635
  expect(binding).not.toBeNull();
1409
- expect(binding!.externalUserId).toBe('existing-user');
1636
+ expect(binding!.externalUserId).toBe("existing-user");
1410
1637
  });
1411
1638
  });
1412
1639
 
@@ -1418,50 +1645,66 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1418
1645
  // Conversational approval engine — standard path
1419
1646
  // ═══════════════════════════════════════════════════════════════════════════
1420
1647
 
1421
- describe('conversational approval engine — standard path', () => {
1648
+ describe("conversational approval engine — standard path", () => {
1422
1649
  beforeEach(() => {
1423
1650
  createBinding({
1424
- assistantId: 'self',
1425
- channel: 'telegram',
1426
- guardianExternalUserId: 'telegram-user-default',
1427
- guardianDeliveryChatId: 'chat-123',
1428
- guardianPrincipalId: 'telegram-user-default',
1651
+ assistantId: "self",
1652
+ channel: "telegram",
1653
+ guardianExternalUserId: "telegram-user-default",
1654
+ guardianDeliveryChatId: "chat-123",
1655
+ guardianPrincipalId: "telegram-user-default",
1429
1656
  });
1430
1657
  });
1431
1658
 
1432
- test('non-decision follow-up: engine returns keep_pending, reply sent', async () => {
1433
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1659
+ test("non-decision follow-up: engine returns keep_pending, reply sent", async () => {
1660
+ const deliverSpy = spyOn(
1661
+ gatewayClient,
1662
+ "deliverChannelReply",
1663
+ ).mockResolvedValue(undefined);
1434
1664
 
1435
- const initReq = makeInboundRequest({ content: 'init' });
1665
+ const initReq = makeInboundRequest({ content: "init" });
1436
1666
  await handleChannelInbound(initReq, noopProcessMessage);
1437
1667
 
1438
1668
  const db = getDb();
1439
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
1669
+ const events = db.$client
1670
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
1671
+ .all() as Array<{ conversation_id: string }>;
1440
1672
  const conversationId = events[0]?.conversation_id;
1441
1673
  ensureConversation(conversationId!);
1442
1674
 
1443
- const sessionMock = registerPendingInteraction('req-conv-1', conversationId!, 'shell');
1675
+ const sessionMock = registerPendingInteraction(
1676
+ "req-conv-1",
1677
+ conversationId!,
1678
+ "shell",
1679
+ );
1444
1680
 
1445
1681
  deliverSpy.mockClear();
1446
1682
 
1447
1683
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
1448
- disposition: 'keep_pending' as const,
1449
- replyText: 'There is a pending shell command. Would you like to approve or deny it?',
1684
+ disposition: "keep_pending" as const,
1685
+ replyText:
1686
+ "There is a pending shell command. Would you like to approve or deny it?",
1450
1687
  }));
1451
1688
 
1452
- const req = makeInboundRequest({ content: 'what does this command do?' });
1689
+ const req = makeInboundRequest({ content: "what does this command do?" });
1453
1690
  const res = await handleChannelInbound(
1454
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
1691
+ req,
1692
+ noopProcessMessage,
1693
+ "self",
1694
+ undefined,
1695
+ mockConversationGenerator,
1455
1696
  );
1456
- const body = await res.json() as Record<string, unknown>;
1697
+ const body = (await res.json()) as Record<string, unknown>;
1457
1698
 
1458
1699
  expect(body.accepted).toBe(true);
1459
- expect(body.approval).toBe('assistant_turn');
1700
+ expect(body.approval).toBe("assistant_turn");
1460
1701
 
1461
1702
  // The engine reply should have been delivered
1462
1703
  expect(deliverSpy).toHaveBeenCalled();
1463
1704
  const replyCall = deliverSpy.mock.calls.find(
1464
- (call) => typeof call[1] === 'object' && (call[1] as { text?: string }).text?.includes('pending shell command'),
1705
+ (call) =>
1706
+ typeof call[1] === "object" &&
1707
+ (call[1] as { text?: string }).text?.includes("pending shell command"),
1465
1708
  );
1466
1709
  expect(replyCall).toBeDefined();
1467
1710
 
@@ -1471,110 +1714,149 @@ describe('conversational approval engine — standard path', () => {
1471
1714
  deliverSpy.mockRestore();
1472
1715
  });
1473
1716
 
1474
- test('natural-language approval: engine returns approve_once, decision applied', async () => {
1475
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1717
+ test("natural-language approval: engine returns approve_once, decision applied", async () => {
1718
+ const deliverSpy = spyOn(
1719
+ gatewayClient,
1720
+ "deliverChannelReply",
1721
+ ).mockResolvedValue(undefined);
1476
1722
 
1477
- const initReq = makeInboundRequest({ content: 'init' });
1723
+ const initReq = makeInboundRequest({ content: "init" });
1478
1724
  await handleChannelInbound(initReq, noopProcessMessage);
1479
1725
 
1480
1726
  const db = getDb();
1481
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
1727
+ const events = db.$client
1728
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
1729
+ .all() as Array<{ conversation_id: string }>;
1482
1730
  const conversationId = events[0]?.conversation_id;
1483
1731
  ensureConversation(conversationId!);
1484
1732
 
1485
- const sessionMock = registerPendingInteraction('req-conv-2', conversationId!, 'shell');
1733
+ const sessionMock = registerPendingInteraction(
1734
+ "req-conv-2",
1735
+ conversationId!,
1736
+ "shell",
1737
+ );
1486
1738
 
1487
1739
  deliverSpy.mockClear();
1488
1740
 
1489
1741
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
1490
- disposition: 'approve_once' as const,
1491
- replyText: 'Got it, approving the shell command.',
1742
+ disposition: "approve_once" as const,
1743
+ replyText: "Got it, approving the shell command.",
1492
1744
  }));
1493
1745
 
1494
- const req = makeInboundRequest({ content: 'yeah go ahead and run it' });
1746
+ const req = makeInboundRequest({ content: "yeah go ahead and run it" });
1495
1747
  const res = await handleChannelInbound(
1496
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
1748
+ req,
1749
+ noopProcessMessage,
1750
+ "self",
1751
+ undefined,
1752
+ mockConversationGenerator,
1497
1753
  );
1498
- const body = await res.json() as Record<string, unknown>;
1754
+ const body = (await res.json()) as Record<string, unknown>;
1499
1755
 
1500
1756
  expect(body.accepted).toBe(true);
1501
- expect(body.approval).toBe('decision_applied');
1757
+ expect(body.approval).toBe("decision_applied");
1502
1758
 
1503
1759
  // The session should have received an allow decision
1504
- expect(sessionMock).toHaveBeenCalledWith('req-conv-2', 'allow');
1760
+ expect(sessionMock).toHaveBeenCalledWith("req-conv-2", "allow");
1505
1761
 
1506
1762
  deliverSpy.mockRestore();
1507
1763
  });
1508
1764
 
1509
1765
  test('"nevermind" style message: engine returns reject, rejection applied', async () => {
1510
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1766
+ const deliverSpy = spyOn(
1767
+ gatewayClient,
1768
+ "deliverChannelReply",
1769
+ ).mockResolvedValue(undefined);
1511
1770
 
1512
- const initReq = makeInboundRequest({ content: 'init' });
1771
+ const initReq = makeInboundRequest({ content: "init" });
1513
1772
  await handleChannelInbound(initReq, noopProcessMessage);
1514
1773
 
1515
1774
  const db = getDb();
1516
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
1775
+ const events = db.$client
1776
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
1777
+ .all() as Array<{ conversation_id: string }>;
1517
1778
  const conversationId = events[0]?.conversation_id;
1518
1779
  ensureConversation(conversationId!);
1519
1780
 
1520
- const sessionMock = registerPendingInteraction('req-conv-3', conversationId!, 'shell');
1781
+ const sessionMock = registerPendingInteraction(
1782
+ "req-conv-3",
1783
+ conversationId!,
1784
+ "shell",
1785
+ );
1521
1786
 
1522
1787
  deliverSpy.mockClear();
1523
1788
 
1524
1789
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
1525
- disposition: 'reject' as const,
1526
- replyText: 'No problem, I\'ve cancelled the shell command.',
1790
+ disposition: "reject" as const,
1791
+ replyText: "No problem, I've cancelled the shell command.",
1527
1792
  }));
1528
1793
 
1529
- const req = makeInboundRequest({ content: 'nevermind, don\'t run that' });
1794
+ const req = makeInboundRequest({ content: "nevermind, don't run that" });
1530
1795
  const res = await handleChannelInbound(
1531
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
1796
+ req,
1797
+ noopProcessMessage,
1798
+ "self",
1799
+ undefined,
1800
+ mockConversationGenerator,
1532
1801
  );
1533
- const body = await res.json() as Record<string, unknown>;
1802
+ const body = (await res.json()) as Record<string, unknown>;
1534
1803
 
1535
1804
  expect(body.accepted).toBe(true);
1536
- expect(body.approval).toBe('decision_applied');
1805
+ expect(body.approval).toBe("decision_applied");
1537
1806
 
1538
- expect(sessionMock).toHaveBeenCalledWith('req-conv-3', 'deny');
1807
+ expect(sessionMock).toHaveBeenCalledWith("req-conv-3", "deny");
1539
1808
 
1540
1809
  deliverSpy.mockRestore();
1541
1810
  });
1542
1811
 
1543
- test('callback button still takes priority even with conversational engine present', async () => {
1544
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1812
+ test("callback button still takes priority even with conversational engine present", async () => {
1813
+ const deliverSpy = spyOn(
1814
+ gatewayClient,
1815
+ "deliverChannelReply",
1816
+ ).mockResolvedValue(undefined);
1545
1817
 
1546
- const initReq = makeInboundRequest({ content: 'init' });
1818
+ const initReq = makeInboundRequest({ content: "init" });
1547
1819
  await handleChannelInbound(initReq, noopProcessMessage);
1548
1820
 
1549
1821
  const db = getDb();
1550
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
1822
+ const events = db.$client
1823
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
1824
+ .all() as Array<{ conversation_id: string }>;
1551
1825
  const conversationId = events[0]?.conversation_id;
1552
1826
  ensureConversation(conversationId!);
1553
1827
 
1554
- const sessionMock = registerPendingInteraction('req-conv-4', conversationId!, 'shell');
1828
+ const sessionMock = registerPendingInteraction(
1829
+ "req-conv-4",
1830
+ conversationId!,
1831
+ "shell",
1832
+ );
1555
1833
 
1556
1834
  // Mock conversational engine — should NOT be called for callback buttons
1557
1835
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
1558
- disposition: 'keep_pending' as const,
1559
- replyText: 'This should not be called',
1836
+ disposition: "keep_pending" as const,
1837
+ replyText: "This should not be called",
1560
1838
  }));
1561
1839
 
1562
1840
  const req = makeInboundRequest({
1563
- content: '',
1564
- callbackData: 'apr:req-conv-4:approve_once',
1841
+ content: "",
1842
+ callbackData: "apr:req-conv-4:approve_once",
1565
1843
  });
1566
1844
 
1567
1845
  const res = await handleChannelInbound(
1568
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
1846
+ req,
1847
+ noopProcessMessage,
1848
+ "self",
1849
+ undefined,
1850
+ mockConversationGenerator,
1569
1851
  );
1570
- const body = await res.json() as Record<string, unknown>;
1852
+ const body = (await res.json()) as Record<string, unknown>;
1571
1853
 
1572
1854
  expect(body.accepted).toBe(true);
1573
- expect(body.approval).toBe('decision_applied');
1855
+ expect(body.approval).toBe("decision_applied");
1574
1856
 
1575
1857
  // The callback button should have been used directly, not the engine
1576
1858
  expect(mockConversationGenerator).not.toHaveBeenCalled();
1577
- expect(sessionMock).toHaveBeenCalledWith('req-conv-4', 'allow');
1859
+ expect(sessionMock).toHaveBeenCalledWith("req-conv-4", "allow");
1578
1860
 
1579
1861
  deliverSpy.mockRestore();
1580
1862
  });
@@ -1584,246 +1866,298 @@ describe('conversational approval engine — standard path', () => {
1584
1866
  // Guardian conversational approval engine tests
1585
1867
  // ═══════════════════════════════════════════════════════════════════════════
1586
1868
 
1587
- describe('guardian conversational approval via conversation engine', () => {
1588
- test('guardian follow-up clarification: engine returns keep_pending', async () => {
1869
+ describe("guardian conversational approval via conversation engine", () => {
1870
+ test("guardian follow-up clarification: engine returns keep_pending", async () => {
1589
1871
  createBinding({
1590
- assistantId: 'self',
1591
- channel: 'telegram',
1592
- guardianExternalUserId: 'guardian-conv-user',
1593
- guardianDeliveryChatId: 'guardian-conv-chat',
1594
- guardianPrincipalId: 'guardian-conv-user',
1872
+ assistantId: "self",
1873
+ channel: "telegram",
1874
+ guardianExternalUserId: "guardian-conv-user",
1875
+ guardianDeliveryChatId: "guardian-conv-chat",
1876
+ guardianPrincipalId: "guardian-conv-user",
1595
1877
  });
1596
1878
 
1597
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1879
+ const deliverSpy = spyOn(
1880
+ gatewayClient,
1881
+ "deliverChannelReply",
1882
+ ).mockResolvedValue(undefined);
1598
1883
 
1599
- const convId = 'conv-guardian-clarify';
1884
+ const convId = "conv-guardian-clarify";
1600
1885
  ensureConversation(convId);
1601
1886
 
1602
- const sessionMock = registerPendingInteraction('req-gclarify-1', convId, 'shell');
1887
+ const sessionMock = registerPendingInteraction(
1888
+ "req-gclarify-1",
1889
+ convId,
1890
+ "shell",
1891
+ );
1603
1892
  createApprovalRequest({
1604
- runId: 'run-gclarify-1',
1605
- requestId: 'req-gclarify-1',
1893
+ runId: "run-gclarify-1",
1894
+ requestId: "req-gclarify-1",
1606
1895
  conversationId: convId,
1607
- channel: 'telegram',
1608
- requesterExternalUserId: 'requester-clarify',
1609
- requesterChatId: 'chat-requester-clarify',
1610
- guardianExternalUserId: 'guardian-conv-user',
1611
- guardianChatId: 'guardian-conv-chat',
1612
- toolName: 'shell',
1896
+ channel: "telegram",
1897
+ requesterExternalUserId: "requester-clarify",
1898
+ requesterChatId: "chat-requester-clarify",
1899
+ guardianExternalUserId: "guardian-conv-user",
1900
+ guardianChatId: "guardian-conv-chat",
1901
+ toolName: "shell",
1613
1902
  expiresAt: Date.now() + 300_000,
1614
1903
  });
1615
1904
 
1616
1905
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
1617
- disposition: 'keep_pending' as const,
1618
- replyText: 'Could you clarify which action you want me to approve?',
1906
+ disposition: "keep_pending" as const,
1907
+ replyText: "Could you clarify which action you want me to approve?",
1619
1908
  }));
1620
1909
 
1621
1910
  const req = makeInboundRequest({
1622
- content: 'hmm what does this do?',
1623
- conversationExternalId: 'guardian-conv-chat',
1624
- actorExternalId: 'guardian-conv-user',
1911
+ content: "hmm what does this do?",
1912
+ conversationExternalId: "guardian-conv-chat",
1913
+ actorExternalId: "guardian-conv-user",
1625
1914
  });
1626
1915
 
1627
1916
  const res = await handleChannelInbound(
1628
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
1917
+ req,
1918
+ noopProcessMessage,
1919
+ "self",
1920
+ undefined,
1921
+ mockConversationGenerator,
1629
1922
  );
1630
- const body = await res.json() as Record<string, unknown>;
1923
+ const body = (await res.json()) as Record<string, unknown>;
1631
1924
 
1632
1925
  expect(body.accepted).toBe(true);
1633
- expect(body.approval).toBe('assistant_turn');
1926
+ expect(body.approval).toBe("assistant_turn");
1634
1927
 
1635
1928
  // The engine should have been called with role: 'guardian'
1636
1929
  expect(mockConversationGenerator).toHaveBeenCalledTimes(1);
1637
- const callCtx = mockConversationGenerator.mock.calls[0][0] as Record<string, unknown>;
1638
- expect(callCtx.role).toBe('guardian');
1639
- expect(callCtx.allowedActions).toEqual(['approve_once', 'reject']);
1640
- expect(callCtx.userMessage).toBe('hmm what does this do?');
1930
+ const callCtx = mockConversationGenerator.mock.calls[0][0] as Record<
1931
+ string,
1932
+ unknown
1933
+ >;
1934
+ expect(callCtx.role).toBe("guardian");
1935
+ expect(callCtx.allowedActions).toEqual(["approve_once", "reject"]);
1936
+ expect(callCtx.userMessage).toBe("hmm what does this do?");
1641
1937
 
1642
1938
  // The session should NOT have received a decision
1643
1939
  expect(sessionMock).not.toHaveBeenCalled();
1644
1940
 
1645
1941
  // The approval should still be pending
1646
- const pending = getAllPendingApprovalsByGuardianChat('telegram', 'guardian-conv-chat', 'self');
1942
+ const pending = getAllPendingApprovalsByGuardianChat(
1943
+ "telegram",
1944
+ "guardian-conv-chat",
1945
+ "self",
1946
+ );
1647
1947
  expect(pending).toHaveLength(1);
1648
1948
 
1649
1949
  deliverSpy.mockRestore();
1650
1950
  });
1651
1951
 
1652
- test('guardian natural-language approval: engine returns approve_once', async () => {
1952
+ test("guardian natural-language approval: engine returns approve_once", async () => {
1653
1953
  createBinding({
1654
- assistantId: 'self',
1655
- channel: 'telegram',
1656
- guardianExternalUserId: 'guardian-nlp-user',
1657
- guardianDeliveryChatId: 'guardian-nlp-chat',
1658
- guardianPrincipalId: 'guardian-nlp-user',
1954
+ assistantId: "self",
1955
+ channel: "telegram",
1956
+ guardianExternalUserId: "guardian-nlp-user",
1957
+ guardianDeliveryChatId: "guardian-nlp-chat",
1958
+ guardianPrincipalId: "guardian-nlp-user",
1659
1959
  });
1660
1960
 
1661
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1961
+ const deliverSpy = spyOn(
1962
+ gatewayClient,
1963
+ "deliverChannelReply",
1964
+ ).mockResolvedValue(undefined);
1662
1965
 
1663
- const convId = 'conv-guardian-nlp';
1966
+ const convId = "conv-guardian-nlp";
1664
1967
  ensureConversation(convId);
1665
1968
 
1666
- const sessionMock = registerPendingInteraction('req-gnlp-1', convId, 'shell');
1969
+ const sessionMock = registerPendingInteraction(
1970
+ "req-gnlp-1",
1971
+ convId,
1972
+ "shell",
1973
+ );
1667
1974
  createApprovalRequest({
1668
- runId: 'run-gnlp-1',
1669
- requestId: 'req-gnlp-1',
1975
+ runId: "run-gnlp-1",
1976
+ requestId: "req-gnlp-1",
1670
1977
  conversationId: convId,
1671
- channel: 'telegram',
1672
- requesterExternalUserId: 'requester-nlp',
1673
- requesterChatId: 'chat-requester-nlp',
1674
- guardianExternalUserId: 'guardian-nlp-user',
1675
- guardianChatId: 'guardian-nlp-chat',
1676
- toolName: 'shell',
1978
+ channel: "telegram",
1979
+ requesterExternalUserId: "requester-nlp",
1980
+ requesterChatId: "chat-requester-nlp",
1981
+ guardianExternalUserId: "guardian-nlp-user",
1982
+ guardianChatId: "guardian-nlp-chat",
1983
+ toolName: "shell",
1677
1984
  expiresAt: Date.now() + 300_000,
1678
1985
  });
1679
1986
 
1680
1987
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
1681
- disposition: 'approve_once' as const,
1682
- replyText: 'Approved! The shell command will proceed.',
1988
+ disposition: "approve_once" as const,
1989
+ replyText: "Approved! The shell command will proceed.",
1683
1990
  }));
1684
1991
 
1685
1992
  const req = makeInboundRequest({
1686
- content: 'yes go ahead and run it',
1687
- conversationExternalId: 'guardian-nlp-chat',
1688
- actorExternalId: 'guardian-nlp-user',
1993
+ content: "yes go ahead and run it",
1994
+ conversationExternalId: "guardian-nlp-chat",
1995
+ actorExternalId: "guardian-nlp-user",
1689
1996
  });
1690
1997
 
1691
1998
  const res = await handleChannelInbound(
1692
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
1999
+ req,
2000
+ noopProcessMessage,
2001
+ "self",
2002
+ undefined,
2003
+ mockConversationGenerator,
1693
2004
  );
1694
- const body = await res.json() as Record<string, unknown>;
2005
+ const body = (await res.json()) as Record<string, unknown>;
1695
2006
 
1696
2007
  expect(body.accepted).toBe(true);
1697
- expect(body.approval).toBe('guardian_decision_applied');
2008
+ expect(body.approval).toBe("guardian_decision_applied");
1698
2009
 
1699
2010
  // The session should have received an 'allow' decision
1700
- expect(sessionMock).toHaveBeenCalledWith('req-gnlp-1', 'allow');
2011
+ expect(sessionMock).toHaveBeenCalledWith("req-gnlp-1", "allow");
1701
2012
 
1702
2013
  // The approval record should have been updated (no longer pending)
1703
- const pending = getAllPendingApprovalsByGuardianChat('telegram', 'guardian-nlp-chat', 'self');
2014
+ const pending = getAllPendingApprovalsByGuardianChat(
2015
+ "telegram",
2016
+ "guardian-nlp-chat",
2017
+ "self",
2018
+ );
1704
2019
  expect(pending).toHaveLength(0);
1705
2020
 
1706
2021
  // The engine context excluded approve_always for guardians
1707
- const callCtx = mockConversationGenerator.mock.calls[0][0] as Record<string, unknown>;
1708
- expect(callCtx.allowedActions).toEqual(['approve_once', 'reject']);
1709
- expect((callCtx.allowedActions as string[])).not.toContain('approve_always');
2022
+ const callCtx = mockConversationGenerator.mock.calls[0][0] as Record<
2023
+ string,
2024
+ unknown
2025
+ >;
2026
+ expect(callCtx.allowedActions).toEqual(["approve_once", "reject"]);
2027
+ expect(callCtx.allowedActions as string[]).not.toContain("approve_always");
1710
2028
 
1711
2029
  deliverSpy.mockRestore();
1712
2030
  });
1713
2031
 
1714
- test('guardian callback button approve_always is downgraded to approve_once', async () => {
2032
+ test("guardian callback button approve_always is downgraded to approve_once", async () => {
1715
2033
  createBinding({
1716
- assistantId: 'self',
1717
- channel: 'telegram',
1718
- guardianExternalUserId: 'guardian-dg-user',
1719
- guardianDeliveryChatId: 'guardian-dg-chat',
1720
- guardianPrincipalId: 'guardian-dg-user',
2034
+ assistantId: "self",
2035
+ channel: "telegram",
2036
+ guardianExternalUserId: "guardian-dg-user",
2037
+ guardianDeliveryChatId: "guardian-dg-chat",
2038
+ guardianPrincipalId: "guardian-dg-user",
1721
2039
  });
1722
2040
 
1723
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2041
+ const deliverSpy = spyOn(
2042
+ gatewayClient,
2043
+ "deliverChannelReply",
2044
+ ).mockResolvedValue(undefined);
1724
2045
 
1725
- const convId = 'conv-guardian-downgrade';
2046
+ const convId = "conv-guardian-downgrade";
1726
2047
  ensureConversation(convId);
1727
2048
 
1728
- const sessionMock = registerPendingInteraction('req-gdg-1', convId, 'shell');
2049
+ const sessionMock = registerPendingInteraction(
2050
+ "req-gdg-1",
2051
+ convId,
2052
+ "shell",
2053
+ );
1729
2054
  createApprovalRequest({
1730
- runId: 'run-gdg-1',
1731
- requestId: 'req-gdg-1',
2055
+ runId: "run-gdg-1",
2056
+ requestId: "req-gdg-1",
1732
2057
  conversationId: convId,
1733
- channel: 'telegram',
1734
- requesterExternalUserId: 'requester-dg',
1735
- requesterChatId: 'chat-requester-dg',
1736
- guardianExternalUserId: 'guardian-dg-user',
1737
- guardianChatId: 'guardian-dg-chat',
1738
- toolName: 'shell',
2058
+ channel: "telegram",
2059
+ requesterExternalUserId: "requester-dg",
2060
+ requesterChatId: "chat-requester-dg",
2061
+ guardianExternalUserId: "guardian-dg-user",
2062
+ guardianChatId: "guardian-dg-chat",
2063
+ toolName: "shell",
1739
2064
  expiresAt: Date.now() + 300_000,
1740
2065
  });
1741
2066
 
1742
2067
  // Guardian clicks approve_always via callback button
1743
2068
  const req = makeInboundRequest({
1744
- content: '',
1745
- conversationExternalId: 'guardian-dg-chat',
1746
- callbackData: 'apr:req-gdg-1:approve_always',
1747
- actorExternalId: 'guardian-dg-user',
2069
+ content: "",
2070
+ conversationExternalId: "guardian-dg-chat",
2071
+ callbackData: "apr:req-gdg-1:approve_always",
2072
+ actorExternalId: "guardian-dg-user",
1748
2073
  });
1749
2074
 
1750
- const res = await handleChannelInbound(
1751
- req, noopProcessMessage, 'self',
1752
- );
1753
- const body = await res.json() as Record<string, unknown>;
2075
+ const res = await handleChannelInbound(req, noopProcessMessage, "self");
2076
+ const body = (await res.json()) as Record<string, unknown>;
1754
2077
 
1755
2078
  expect(body.accepted).toBe(true);
1756
- expect(body.approval).toBe('guardian_decision_applied');
2079
+ expect(body.approval).toBe("guardian_decision_applied");
1757
2080
 
1758
2081
  // approve_always should have been downgraded to approve_once ('allow')
1759
- expect(sessionMock).toHaveBeenCalledWith('req-gdg-1', 'allow');
2082
+ expect(sessionMock).toHaveBeenCalledWith("req-gdg-1", "allow");
1760
2083
 
1761
2084
  deliverSpy.mockRestore();
1762
2085
  });
1763
2086
 
1764
- test('multi-pending guardian disambiguation: engine requests clarification', async () => {
2087
+ test("multi-pending guardian disambiguation: engine requests clarification", async () => {
1765
2088
  createBinding({
1766
- assistantId: 'self',
1767
- channel: 'telegram',
1768
- guardianExternalUserId: 'guardian-multi-user',
1769
- guardianDeliveryChatId: 'guardian-multi-chat',
1770
- guardianPrincipalId: 'guardian-multi-user',
2089
+ assistantId: "self",
2090
+ channel: "telegram",
2091
+ guardianExternalUserId: "guardian-multi-user",
2092
+ guardianDeliveryChatId: "guardian-multi-chat",
2093
+ guardianPrincipalId: "guardian-multi-user",
1771
2094
  });
1772
2095
 
1773
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2096
+ const deliverSpy = spyOn(
2097
+ gatewayClient,
2098
+ "deliverChannelReply",
2099
+ ).mockResolvedValue(undefined);
1774
2100
 
1775
- const convA = 'conv-multi-a';
1776
- const convB = 'conv-multi-b';
2101
+ const convA = "conv-multi-a";
2102
+ const convB = "conv-multi-b";
1777
2103
  ensureConversation(convA);
1778
2104
  ensureConversation(convB);
1779
2105
 
1780
- const sessionA = registerPendingInteraction('req-multi-a', convA, 'shell');
2106
+ const sessionA = registerPendingInteraction("req-multi-a", convA, "shell");
1781
2107
  createApprovalRequest({
1782
- runId: 'run-multi-a',
1783
- requestId: 'req-multi-a',
2108
+ runId: "run-multi-a",
2109
+ requestId: "req-multi-a",
1784
2110
  conversationId: convA,
1785
- channel: 'telegram',
1786
- requesterExternalUserId: 'requester-multi-a',
1787
- requesterChatId: 'chat-requester-multi-a',
1788
- guardianExternalUserId: 'guardian-multi-user',
1789
- guardianChatId: 'guardian-multi-chat',
1790
- toolName: 'shell',
2111
+ channel: "telegram",
2112
+ requesterExternalUserId: "requester-multi-a",
2113
+ requesterChatId: "chat-requester-multi-a",
2114
+ guardianExternalUserId: "guardian-multi-user",
2115
+ guardianChatId: "guardian-multi-chat",
2116
+ toolName: "shell",
1791
2117
  expiresAt: Date.now() + 300_000,
1792
2118
  });
1793
2119
 
1794
- const sessionB = registerPendingInteraction('req-multi-b', convB, 'file_edit');
2120
+ const sessionB = registerPendingInteraction(
2121
+ "req-multi-b",
2122
+ convB,
2123
+ "file_edit",
2124
+ );
1795
2125
  createApprovalRequest({
1796
- runId: 'run-multi-b',
1797
- requestId: 'req-multi-b',
2126
+ runId: "run-multi-b",
2127
+ requestId: "req-multi-b",
1798
2128
  conversationId: convB,
1799
- channel: 'telegram',
1800
- requesterExternalUserId: 'requester-multi-b',
1801
- requesterChatId: 'chat-requester-multi-b',
1802
- guardianExternalUserId: 'guardian-multi-user',
1803
- guardianChatId: 'guardian-multi-chat',
1804
- toolName: 'file_edit',
2129
+ channel: "telegram",
2130
+ requesterExternalUserId: "requester-multi-b",
2131
+ requesterChatId: "chat-requester-multi-b",
2132
+ guardianExternalUserId: "guardian-multi-user",
2133
+ guardianChatId: "guardian-multi-chat",
2134
+ toolName: "file_edit",
1805
2135
  expiresAt: Date.now() + 300_000,
1806
2136
  });
1807
2137
 
1808
2138
  // Engine returns keep_pending for disambiguation
1809
2139
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
1810
- disposition: 'keep_pending' as const,
1811
- replyText: 'You have 2 pending requests: shell and file_edit. Which one?',
2140
+ disposition: "keep_pending" as const,
2141
+ replyText: "You have 2 pending requests: shell and file_edit. Which one?",
1812
2142
  }));
1813
2143
 
1814
2144
  const req = makeInboundRequest({
1815
- content: 'approve it',
1816
- conversationExternalId: 'guardian-multi-chat',
1817
- actorExternalId: 'guardian-multi-user',
2145
+ content: "approve it",
2146
+ conversationExternalId: "guardian-multi-chat",
2147
+ actorExternalId: "guardian-multi-user",
1818
2148
  });
1819
2149
 
1820
2150
  const res = await handleChannelInbound(
1821
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
2151
+ req,
2152
+ noopProcessMessage,
2153
+ "self",
2154
+ undefined,
2155
+ mockConversationGenerator,
1822
2156
  );
1823
- const body = await res.json() as Record<string, unknown>;
2157
+ const body = (await res.json()) as Record<string, unknown>;
1824
2158
 
1825
2159
  expect(body.accepted).toBe(true);
1826
- expect(body.approval).toBe('assistant_turn');
2160
+ expect(body.approval).toBe("assistant_turn");
1827
2161
 
1828
2162
  // Neither session should have been called
1829
2163
  expect(sessionA).not.toHaveBeenCalled();
@@ -1831,13 +2165,16 @@ describe('guardian conversational approval via conversation engine', () => {
1831
2165
 
1832
2166
  // The engine should have received both pending approvals
1833
2167
  expect(mockConversationGenerator).toHaveBeenCalledTimes(1);
1834
- const engineCtx = mockConversationGenerator.mock.calls[0][0] as Record<string, unknown>;
1835
- expect((engineCtx.pendingApprovals as Array<unknown>)).toHaveLength(2);
1836
- expect(engineCtx.role).toBe('guardian');
2168
+ const engineCtx = mockConversationGenerator.mock.calls[0][0] as Record<
2169
+ string,
2170
+ unknown
2171
+ >;
2172
+ expect(engineCtx.pendingApprovals as Array<unknown>).toHaveLength(2);
2173
+ expect(engineCtx.role).toBe("guardian");
1837
2174
 
1838
2175
  // Disambiguation reply delivered to guardian
1839
- const disambigCall = deliverSpy.mock.calls.find(
1840
- (call) => (call[1] as { text?: string }).text?.includes('2 pending requests'),
2176
+ const disambigCall = deliverSpy.mock.calls.find((call) =>
2177
+ (call[1] as { text?: string }).text?.includes("2 pending requests"),
1841
2178
  );
1842
2179
  expect(disambigCall).toBeTruthy();
1843
2180
 
@@ -1849,47 +2186,60 @@ describe('guardian conversational approval via conversation engine', () => {
1849
2186
  // keep_pending must remain conversational (no deterministic fallback)
1850
2187
  // ═══════════════════════════════════════════════════════════════════════════
1851
2188
 
1852
- describe('keep_pending remains conversational — standard path', () => {
2189
+ describe("keep_pending remains conversational — standard path", () => {
1853
2190
  beforeEach(() => {
1854
2191
  createBinding({
1855
- assistantId: 'self',
1856
- channel: 'telegram',
1857
- guardianExternalUserId: 'telegram-user-default',
1858
- guardianDeliveryChatId: 'chat-123',
1859
- guardianPrincipalId: 'telegram-user-default',
2192
+ assistantId: "self",
2193
+ channel: "telegram",
2194
+ guardianExternalUserId: "telegram-user-default",
2195
+ guardianDeliveryChatId: "chat-123",
2196
+ guardianPrincipalId: "telegram-user-default",
1860
2197
  });
1861
2198
  });
1862
2199
 
1863
2200
  test('explicit "approve" with keep_pending returns assistant_turn and does not auto-decide', async () => {
1864
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2201
+ const deliverSpy = spyOn(
2202
+ gatewayClient,
2203
+ "deliverChannelReply",
2204
+ ).mockResolvedValue(undefined);
1865
2205
 
1866
- const initReq = makeInboundRequest({ content: 'init' });
2206
+ const initReq = makeInboundRequest({ content: "init" });
1867
2207
  await handleChannelInbound(initReq, noopProcessMessage);
1868
2208
 
1869
2209
  const db = getDb();
1870
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
2210
+ const events = db.$client
2211
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
2212
+ .all() as Array<{ conversation_id: string }>;
1871
2213
  const conversationId = events[0]?.conversation_id;
1872
2214
  ensureConversation(conversationId!);
1873
2215
 
1874
- const sessionMock = registerPendingInteraction('req-kp-1', conversationId!, 'shell');
2216
+ const sessionMock = registerPendingInteraction(
2217
+ "req-kp-1",
2218
+ conversationId!,
2219
+ "shell",
2220
+ );
1875
2221
 
1876
2222
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
1877
- disposition: 'keep_pending' as const,
1878
- replyText: 'Before deciding, can you confirm the intent?',
2223
+ disposition: "keep_pending" as const,
2224
+ replyText: "Before deciding, can you confirm the intent?",
1879
2225
  }));
1880
2226
 
1881
- const req = makeInboundRequest({ content: 'approve' });
2227
+ const req = makeInboundRequest({ content: "approve" });
1882
2228
  const res = await handleChannelInbound(
1883
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
2229
+ req,
2230
+ noopProcessMessage,
2231
+ "self",
2232
+ undefined,
2233
+ mockConversationGenerator,
1884
2234
  );
1885
- const body = await res.json() as Record<string, unknown>;
2235
+ const body = (await res.json()) as Record<string, unknown>;
1886
2236
 
1887
2237
  expect(body.accepted).toBe(true);
1888
- expect(body.approval).toBe('assistant_turn');
2238
+ expect(body.approval).toBe("assistant_turn");
1889
2239
  expect(sessionMock).not.toHaveBeenCalled();
1890
2240
 
1891
- const followupReply = deliverSpy.mock.calls.find(
1892
- (call) => (call[1] as { text?: string }).text?.includes('confirm the intent'),
2241
+ const followupReply = deliverSpy.mock.calls.find((call) =>
2242
+ (call[1] as { text?: string }).text?.includes("confirm the intent"),
1893
2243
  );
1894
2244
  expect(followupReply).toBeDefined();
1895
2245
 
@@ -1897,57 +2247,70 @@ describe('keep_pending remains conversational — standard path', () => {
1897
2247
  });
1898
2248
  });
1899
2249
 
1900
- describe('keep_pending remains conversational — guardian path', () => {
2250
+ describe("keep_pending remains conversational — guardian path", () => {
1901
2251
  test('guardian explicit "yes" with keep_pending returns assistant_turn without applying a decision', async () => {
1902
2252
  createBinding({
1903
- assistantId: 'self',
1904
- channel: 'telegram',
1905
- guardianExternalUserId: 'guardian-user-fb',
1906
- guardianDeliveryChatId: 'guardian-chat-fb',
1907
- guardianPrincipalId: 'guardian-user-fb',
2253
+ assistantId: "self",
2254
+ channel: "telegram",
2255
+ guardianExternalUserId: "guardian-user-fb",
2256
+ guardianDeliveryChatId: "guardian-chat-fb",
2257
+ guardianPrincipalId: "guardian-user-fb",
1908
2258
  });
1909
2259
 
1910
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2260
+ const deliverSpy = spyOn(
2261
+ gatewayClient,
2262
+ "deliverChannelReply",
2263
+ ).mockResolvedValue(undefined);
1911
2264
 
1912
- const convId = 'conv-gfb-1';
2265
+ const convId = "conv-gfb-1";
1913
2266
  ensureConversation(convId);
1914
2267
 
1915
- const sessionMock = registerPendingInteraction('req-gfb-1', convId, 'shell');
2268
+ const sessionMock = registerPendingInteraction(
2269
+ "req-gfb-1",
2270
+ convId,
2271
+ "shell",
2272
+ );
1916
2273
  createApprovalRequest({
1917
- runId: 'run-gfb-1',
1918
- requestId: 'req-gfb-1',
2274
+ runId: "run-gfb-1",
2275
+ requestId: "req-gfb-1",
1919
2276
  conversationId: convId,
1920
- assistantId: 'self',
1921
- channel: 'telegram',
1922
- requesterExternalUserId: 'requester-user-fb',
1923
- requesterChatId: 'requester-chat-fb',
1924
- guardianExternalUserId: 'guardian-user-fb',
1925
- guardianChatId: 'guardian-chat-fb',
1926
- toolName: 'shell',
2277
+ assistantId: "self",
2278
+ channel: "telegram",
2279
+ requesterExternalUserId: "requester-user-fb",
2280
+ requesterChatId: "requester-chat-fb",
2281
+ guardianExternalUserId: "guardian-user-fb",
2282
+ guardianChatId: "guardian-chat-fb",
2283
+ toolName: "shell",
1927
2284
  expiresAt: Date.now() + 300_000,
1928
2285
  });
1929
2286
 
1930
2287
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
1931
- disposition: 'keep_pending' as const,
1932
- replyText: 'Which run are you approving?',
2288
+ disposition: "keep_pending" as const,
2289
+ replyText: "Which run are you approving?",
1933
2290
  }));
1934
2291
 
1935
2292
  const guardianReq = makeInboundRequest({
1936
- content: 'yes',
1937
- conversationExternalId: 'guardian-chat-fb',
1938
- actorExternalId: 'guardian-user-fb',
2293
+ content: "yes",
2294
+ conversationExternalId: "guardian-chat-fb",
2295
+ actorExternalId: "guardian-user-fb",
1939
2296
  });
1940
2297
  const res = await handleChannelInbound(
1941
- guardianReq, noopProcessMessage, 'self', undefined, mockConversationGenerator,
2298
+ guardianReq,
2299
+ noopProcessMessage,
2300
+ "self",
2301
+ undefined,
2302
+ mockConversationGenerator,
1942
2303
  );
1943
- const body = await res.json() as Record<string, unknown>;
2304
+ const body = (await res.json()) as Record<string, unknown>;
1944
2305
 
1945
2306
  expect(body.accepted).toBe(true);
1946
- expect(body.approval).toBe('assistant_turn');
2307
+ expect(body.approval).toBe("assistant_turn");
1947
2308
  expect(sessionMock).not.toHaveBeenCalled();
1948
2309
 
1949
- const followupReply = deliverSpy.mock.calls.find(
1950
- (call) => (call[1] as { text?: string }).text?.includes('Which run are you approving'),
2310
+ const followupReply = deliverSpy.mock.calls.find((call) =>
2311
+ (call[1] as { text?: string }).text?.includes(
2312
+ "Which run are you approving",
2313
+ ),
1951
2314
  );
1952
2315
  expect(followupReply).toBeDefined();
1953
2316
 
@@ -1959,79 +2322,94 @@ describe('keep_pending remains conversational — guardian path', () => {
1959
2322
  // Requester cancel of guardian-gated pending request
1960
2323
  // ═══════════════════════════════════════════════════════════════════════════
1961
2324
 
1962
- describe('requester cancel of guardian-gated pending request', () => {
2325
+ describe("requester cancel of guardian-gated pending request", () => {
1963
2326
  beforeEach(() => {
1964
2327
  createBinding({
1965
- assistantId: 'self',
1966
- channel: 'telegram',
1967
- guardianExternalUserId: 'guardian-cancel',
1968
- guardianDeliveryChatId: 'guardian-cancel-chat',
1969
- guardianPrincipalId: 'guardian-cancel',
2328
+ assistantId: "self",
2329
+ channel: "telegram",
2330
+ guardianExternalUserId: "guardian-cancel",
2331
+ guardianDeliveryChatId: "guardian-cancel-chat",
2332
+ guardianPrincipalId: "guardian-cancel",
1970
2333
  });
1971
2334
  });
1972
2335
 
1973
2336
  test('requester explicit "deny" can cancel when the conversation engine returns reject', async () => {
1974
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2337
+ const deliverSpy = spyOn(
2338
+ gatewayClient,
2339
+ "deliverChannelReply",
2340
+ ).mockResolvedValue(undefined);
1975
2341
 
1976
2342
  // Create requester conversation
1977
2343
  const initReq = makeInboundRequest({
1978
- content: 'init',
1979
- conversationExternalId: 'requester-cancel-chat',
1980
- actorExternalId: 'requester-cancel-user',
2344
+ content: "init",
2345
+ conversationExternalId: "requester-cancel-chat",
2346
+ actorExternalId: "requester-cancel-user",
1981
2347
  });
1982
2348
  await handleChannelInbound(initReq, noopProcessMessage);
1983
2349
 
1984
2350
  const db = getDb();
1985
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
2351
+ const events = db.$client
2352
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
2353
+ .all() as Array<{ conversation_id: string }>;
1986
2354
  const conversationId = events[0]?.conversation_id;
1987
2355
  ensureConversation(conversationId!);
1988
2356
 
1989
- const sessionMock = registerPendingInteraction('req-cancel-1', conversationId!, 'shell');
2357
+ const sessionMock = registerPendingInteraction(
2358
+ "req-cancel-1",
2359
+ conversationId!,
2360
+ "shell",
2361
+ );
1990
2362
 
1991
2363
  createApprovalRequest({
1992
- runId: 'run-cancel-1',
1993
- requestId: 'req-cancel-1',
2364
+ runId: "run-cancel-1",
2365
+ requestId: "req-cancel-1",
1994
2366
  conversationId: conversationId!,
1995
- assistantId: 'self',
1996
- channel: 'telegram',
1997
- requesterExternalUserId: 'requester-cancel-user',
1998
- requesterChatId: 'requester-cancel-chat',
1999
- guardianExternalUserId: 'guardian-cancel',
2000
- guardianChatId: 'guardian-cancel-chat',
2001
- toolName: 'shell',
2367
+ assistantId: "self",
2368
+ channel: "telegram",
2369
+ requesterExternalUserId: "requester-cancel-user",
2370
+ requesterChatId: "requester-cancel-chat",
2371
+ guardianExternalUserId: "guardian-cancel",
2372
+ guardianChatId: "guardian-cancel-chat",
2373
+ toolName: "shell",
2002
2374
  expiresAt: Date.now() + 300_000,
2003
2375
  });
2004
2376
 
2005
2377
  deliverSpy.mockClear();
2006
2378
 
2007
2379
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
2008
- disposition: 'reject' as const,
2009
- replyText: 'Cancelling this request now.',
2380
+ disposition: "reject" as const,
2381
+ replyText: "Cancelling this request now.",
2010
2382
  }));
2011
2383
 
2012
2384
  const req = makeInboundRequest({
2013
- content: 'deny',
2014
- conversationExternalId: 'requester-cancel-chat',
2015
- actorExternalId: 'requester-cancel-user',
2385
+ content: "deny",
2386
+ conversationExternalId: "requester-cancel-chat",
2387
+ actorExternalId: "requester-cancel-user",
2016
2388
  });
2017
2389
  const res = await handleChannelInbound(
2018
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
2390
+ req,
2391
+ noopProcessMessage,
2392
+ "self",
2393
+ undefined,
2394
+ mockConversationGenerator,
2019
2395
  );
2020
- const body = await res.json() as Record<string, unknown>;
2396
+ const body = (await res.json()) as Record<string, unknown>;
2021
2397
 
2022
2398
  expect(body.accepted).toBe(true);
2023
- expect(body.approval).toBe('decision_applied');
2024
- expect(sessionMock).toHaveBeenCalledWith('req-cancel-1', 'deny');
2399
+ expect(body.approval).toBe("decision_applied");
2400
+ expect(sessionMock).toHaveBeenCalledWith("req-cancel-1", "deny");
2025
2401
 
2026
2402
  // Requester should have been notified
2027
2403
  const requesterReply = deliverSpy.mock.calls.find(
2028
- (call) => (call[1] as { chatId?: string }).chatId === 'requester-cancel-chat',
2404
+ (call) =>
2405
+ (call[1] as { chatId?: string }).chatId === "requester-cancel-chat",
2029
2406
  );
2030
2407
  expect(requesterReply).toBeDefined();
2031
2408
 
2032
2409
  // Guardian should have been notified of the cancellation
2033
2410
  const guardianNotice = deliverSpy.mock.calls.find(
2034
- (call) => (call[1] as { chatId?: string }).chatId === 'guardian-cancel-chat',
2411
+ (call) =>
2412
+ (call[1] as { chatId?: string }).chatId === "guardian-cancel-chat",
2035
2413
  );
2036
2414
  expect(guardianNotice).toBeDefined();
2037
2415
 
@@ -2039,119 +2417,148 @@ describe('requester cancel of guardian-gated pending request', () => {
2039
2417
  });
2040
2418
 
2041
2419
  test('requester "nevermind" via conversational engine cancels guardian-gated request', async () => {
2042
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2420
+ const deliverSpy = spyOn(
2421
+ gatewayClient,
2422
+ "deliverChannelReply",
2423
+ ).mockResolvedValue(undefined);
2043
2424
 
2044
2425
  const initReq = makeInboundRequest({
2045
- content: 'init',
2046
- conversationExternalId: 'requester-cancel-chat',
2047
- actorExternalId: 'requester-cancel-user',
2426
+ content: "init",
2427
+ conversationExternalId: "requester-cancel-chat",
2428
+ actorExternalId: "requester-cancel-user",
2048
2429
  });
2049
2430
  await handleChannelInbound(initReq, noopProcessMessage);
2050
2431
 
2051
2432
  const db = getDb();
2052
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
2433
+ const events = db.$client
2434
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
2435
+ .all() as Array<{ conversation_id: string }>;
2053
2436
  const conversationId = events[0]?.conversation_id;
2054
2437
  ensureConversation(conversationId!);
2055
2438
 
2056
- const sessionMock = registerPendingInteraction('req-cancel-2', conversationId!, 'shell');
2439
+ const sessionMock = registerPendingInteraction(
2440
+ "req-cancel-2",
2441
+ conversationId!,
2442
+ "shell",
2443
+ );
2057
2444
 
2058
2445
  createApprovalRequest({
2059
- runId: 'run-cancel-2',
2060
- requestId: 'req-cancel-2',
2446
+ runId: "run-cancel-2",
2447
+ requestId: "req-cancel-2",
2061
2448
  conversationId: conversationId!,
2062
- assistantId: 'self',
2063
- channel: 'telegram',
2064
- requesterExternalUserId: 'requester-cancel-user',
2065
- requesterChatId: 'requester-cancel-chat',
2066
- guardianExternalUserId: 'guardian-cancel',
2067
- guardianChatId: 'guardian-cancel-chat',
2068
- toolName: 'shell',
2449
+ assistantId: "self",
2450
+ channel: "telegram",
2451
+ requesterExternalUserId: "requester-cancel-user",
2452
+ requesterChatId: "requester-cancel-chat",
2453
+ guardianExternalUserId: "guardian-cancel",
2454
+ guardianChatId: "guardian-cancel-chat",
2455
+ toolName: "shell",
2069
2456
  expiresAt: Date.now() + 300_000,
2070
2457
  });
2071
2458
 
2072
2459
  deliverSpy.mockClear();
2073
2460
 
2074
2461
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
2075
- disposition: 'reject' as const,
2076
- replyText: 'OK, I have cancelled the pending request.',
2462
+ disposition: "reject" as const,
2463
+ replyText: "OK, I have cancelled the pending request.",
2077
2464
  }));
2078
2465
 
2079
2466
  const req = makeInboundRequest({
2080
- content: 'actually never mind, cancel it',
2081
- conversationExternalId: 'requester-cancel-chat',
2082
- actorExternalId: 'requester-cancel-user',
2467
+ content: "actually never mind, cancel it",
2468
+ conversationExternalId: "requester-cancel-chat",
2469
+ actorExternalId: "requester-cancel-user",
2083
2470
  });
2084
2471
  const res = await handleChannelInbound(
2085
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
2472
+ req,
2473
+ noopProcessMessage,
2474
+ "self",
2475
+ undefined,
2476
+ mockConversationGenerator,
2086
2477
  );
2087
- const body = await res.json() as Record<string, unknown>;
2478
+ const body = (await res.json()) as Record<string, unknown>;
2088
2479
 
2089
2480
  expect(body.accepted).toBe(true);
2090
- expect(body.approval).toBe('decision_applied');
2091
- expect(sessionMock).toHaveBeenCalledWith('req-cancel-2', 'deny');
2481
+ expect(body.approval).toBe("decision_applied");
2482
+ expect(sessionMock).toHaveBeenCalledWith("req-cancel-2", "deny");
2092
2483
 
2093
2484
  // Engine should have been called with reject-only allowed actions
2094
2485
  expect(mockConversationGenerator).toHaveBeenCalledTimes(1);
2095
- const engineCtx = mockConversationGenerator.mock.calls[0][0] as Record<string, unknown>;
2096
- expect(engineCtx.allowedActions).toEqual(['reject']);
2486
+ const engineCtx = mockConversationGenerator.mock.calls[0][0] as Record<
2487
+ string,
2488
+ unknown
2489
+ >;
2490
+ expect(engineCtx.allowedActions).toEqual(["reject"]);
2097
2491
 
2098
2492
  deliverSpy.mockRestore();
2099
2493
  });
2100
2494
 
2101
- test('requester non-cancel message with keep_pending returns conversational reply', async () => {
2102
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2495
+ test("requester non-cancel message with keep_pending returns conversational reply", async () => {
2496
+ const deliverSpy = spyOn(
2497
+ gatewayClient,
2498
+ "deliverChannelReply",
2499
+ ).mockResolvedValue(undefined);
2103
2500
 
2104
2501
  const initReq = makeInboundRequest({
2105
- content: 'init',
2106
- conversationExternalId: 'requester-cancel-chat',
2107
- actorExternalId: 'requester-cancel-user',
2502
+ content: "init",
2503
+ conversationExternalId: "requester-cancel-chat",
2504
+ actorExternalId: "requester-cancel-user",
2108
2505
  });
2109
2506
  await handleChannelInbound(initReq, noopProcessMessage);
2110
2507
 
2111
2508
  const db = getDb();
2112
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
2509
+ const events = db.$client
2510
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
2511
+ .all() as Array<{ conversation_id: string }>;
2113
2512
  const conversationId = events[0]?.conversation_id;
2114
2513
  ensureConversation(conversationId!);
2115
2514
 
2116
- const sessionMock = registerPendingInteraction('req-cancel-3', conversationId!, 'shell');
2515
+ const sessionMock = registerPendingInteraction(
2516
+ "req-cancel-3",
2517
+ conversationId!,
2518
+ "shell",
2519
+ );
2117
2520
 
2118
2521
  createApprovalRequest({
2119
- runId: 'run-cancel-3',
2120
- requestId: 'req-cancel-3',
2522
+ runId: "run-cancel-3",
2523
+ requestId: "req-cancel-3",
2121
2524
  conversationId: conversationId!,
2122
- assistantId: 'self',
2123
- channel: 'telegram',
2124
- requesterExternalUserId: 'requester-cancel-user',
2125
- requesterChatId: 'requester-cancel-chat',
2126
- guardianExternalUserId: 'guardian-cancel',
2127
- guardianChatId: 'guardian-cancel-chat',
2128
- toolName: 'shell',
2525
+ assistantId: "self",
2526
+ channel: "telegram",
2527
+ requesterExternalUserId: "requester-cancel-user",
2528
+ requesterChatId: "requester-cancel-chat",
2529
+ guardianExternalUserId: "guardian-cancel",
2530
+ guardianChatId: "guardian-cancel-chat",
2531
+ toolName: "shell",
2129
2532
  expiresAt: Date.now() + 300_000,
2130
2533
  });
2131
2534
 
2132
2535
  deliverSpy.mockClear();
2133
2536
 
2134
2537
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
2135
- disposition: 'keep_pending' as const,
2136
- replyText: 'Still waiting.',
2538
+ disposition: "keep_pending" as const,
2539
+ replyText: "Still waiting.",
2137
2540
  }));
2138
2541
 
2139
2542
  const req = makeInboundRequest({
2140
- content: 'what is happening?',
2141
- conversationExternalId: 'requester-cancel-chat',
2142
- actorExternalId: 'requester-cancel-user',
2543
+ content: "what is happening?",
2544
+ conversationExternalId: "requester-cancel-chat",
2545
+ actorExternalId: "requester-cancel-user",
2143
2546
  });
2144
2547
  const res = await handleChannelInbound(
2145
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
2548
+ req,
2549
+ noopProcessMessage,
2550
+ "self",
2551
+ undefined,
2552
+ mockConversationGenerator,
2146
2553
  );
2147
- const body = await res.json() as Record<string, unknown>;
2554
+ const body = (await res.json()) as Record<string, unknown>;
2148
2555
 
2149
2556
  expect(body.accepted).toBe(true);
2150
- expect(body.approval).toBe('assistant_turn');
2557
+ expect(body.approval).toBe("assistant_turn");
2151
2558
  expect(sessionMock).not.toHaveBeenCalled();
2152
2559
 
2153
- const pendingReply = deliverSpy.mock.calls.find(
2154
- (call) => (call[1] as { text?: string }).text?.includes('Still waiting.'),
2560
+ const pendingReply = deliverSpy.mock.calls.find((call) =>
2561
+ (call[1] as { text?: string }).text?.includes("Still waiting."),
2155
2562
  );
2156
2563
  expect(pendingReply).toBeDefined();
2157
2564
 
@@ -2159,33 +2566,38 @@ describe('requester cancel of guardian-gated pending request', () => {
2159
2566
  });
2160
2567
 
2161
2568
  test('requester "approve" is blocked — self-approval not allowed even during cancel check', async () => {
2162
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2569
+ const deliverSpy = spyOn(
2570
+ gatewayClient,
2571
+ "deliverChannelReply",
2572
+ ).mockResolvedValue(undefined);
2163
2573
 
2164
2574
  const initReq = makeInboundRequest({
2165
- content: 'init',
2166
- conversationExternalId: 'requester-cancel-chat',
2167
- actorExternalId: 'requester-cancel-user',
2575
+ content: "init",
2576
+ conversationExternalId: "requester-cancel-chat",
2577
+ actorExternalId: "requester-cancel-user",
2168
2578
  });
2169
2579
  await handleChannelInbound(initReq, noopProcessMessage);
2170
2580
 
2171
2581
  const db = getDb();
2172
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
2582
+ const events = db.$client
2583
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
2584
+ .all() as Array<{ conversation_id: string }>;
2173
2585
  const conversationId = events[0]?.conversation_id;
2174
2586
  ensureConversation(conversationId!);
2175
2587
 
2176
- registerPendingInteraction('req-cancel-4', conversationId!, 'shell');
2588
+ registerPendingInteraction("req-cancel-4", conversationId!, "shell");
2177
2589
 
2178
2590
  createApprovalRequest({
2179
- runId: 'run-cancel-4',
2180
- requestId: 'req-cancel-4',
2591
+ runId: "run-cancel-4",
2592
+ requestId: "req-cancel-4",
2181
2593
  conversationId: conversationId!,
2182
- assistantId: 'self',
2183
- channel: 'telegram',
2184
- requesterExternalUserId: 'requester-cancel-user',
2185
- requesterChatId: 'requester-cancel-chat',
2186
- guardianExternalUserId: 'guardian-cancel',
2187
- guardianChatId: 'guardian-cancel-chat',
2188
- toolName: 'shell',
2594
+ assistantId: "self",
2595
+ channel: "telegram",
2596
+ requesterExternalUserId: "requester-cancel-user",
2597
+ requesterChatId: "requester-cancel-chat",
2598
+ guardianExternalUserId: "guardian-cancel",
2599
+ guardianChatId: "guardian-cancel-chat",
2600
+ toolName: "shell",
2189
2601
  expiresAt: Date.now() + 300_000,
2190
2602
  });
2191
2603
 
@@ -2193,16 +2605,16 @@ describe('requester cancel of guardian-gated pending request', () => {
2193
2605
 
2194
2606
  // Requester tries to self-approve while guardian approval is pending.
2195
2607
  const req = makeInboundRequest({
2196
- content: 'approve',
2197
- conversationExternalId: 'requester-cancel-chat',
2198
- actorExternalId: 'requester-cancel-user',
2608
+ content: "approve",
2609
+ conversationExternalId: "requester-cancel-chat",
2610
+ actorExternalId: "requester-cancel-user",
2199
2611
  });
2200
2612
  const res = await handleChannelInbound(req, noopProcessMessage);
2201
- const body = await res.json() as Record<string, unknown>;
2613
+ const body = (await res.json()) as Record<string, unknown>;
2202
2614
 
2203
2615
  expect(body.accepted).toBe(true);
2204
2616
  // Should get the guardian-pending notice, NOT decision_applied
2205
- expect(body.approval).toBe('assistant_turn');
2617
+ expect(body.approval).toBe("assistant_turn");
2206
2618
 
2207
2619
  deliverSpy.mockRestore();
2208
2620
  });
@@ -2212,60 +2624,69 @@ describe('requester cancel of guardian-gated pending request', () => {
2212
2624
  // Engine decision race condition — standard path
2213
2625
  // ═══════════════════════════════════════════════════════════════════════════
2214
2626
 
2215
- describe('engine decision race condition — standard path', () => {
2627
+ describe("engine decision race condition — standard path", () => {
2216
2628
  beforeEach(() => {
2217
2629
  createBinding({
2218
- assistantId: 'self',
2219
- channel: 'telegram',
2220
- guardianExternalUserId: 'telegram-user-default',
2221
- guardianDeliveryChatId: 'chat-123',
2222
- guardianPrincipalId: 'telegram-user-default',
2630
+ assistantId: "self",
2631
+ channel: "telegram",
2632
+ guardianExternalUserId: "telegram-user-default",
2633
+ guardianDeliveryChatId: "chat-123",
2634
+ guardianPrincipalId: "telegram-user-default",
2223
2635
  });
2224
2636
  });
2225
2637
 
2226
- test('returns stale_ignored when engine approves but interaction was already resolved', async () => {
2227
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2638
+ test("returns stale_ignored when engine approves but interaction was already resolved", async () => {
2639
+ const deliverSpy = spyOn(
2640
+ gatewayClient,
2641
+ "deliverChannelReply",
2642
+ ).mockResolvedValue(undefined);
2228
2643
 
2229
- const initReq = makeInboundRequest({ content: 'init' });
2644
+ const initReq = makeInboundRequest({ content: "init" });
2230
2645
  await handleChannelInbound(initReq, noopProcessMessage);
2231
2646
 
2232
2647
  const db = getDb();
2233
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
2648
+ const events = db.$client
2649
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
2650
+ .all() as Array<{ conversation_id: string }>;
2234
2651
  const conversationId = events[0]?.conversation_id;
2235
2652
  ensureConversation(conversationId!);
2236
2653
 
2237
- registerPendingInteraction('req-race-1', conversationId!, 'shell');
2654
+ registerPendingInteraction("req-race-1", conversationId!, "shell");
2238
2655
 
2239
2656
  deliverSpy.mockClear();
2240
2657
 
2241
2658
  // Engine returns approve_once, but resolves the pending interaction
2242
2659
  // before handleChannelDecision is called (simulating race condition)
2243
2660
  const mockConversationGenerator = mock(async (_ctx: unknown) => {
2244
- pendingInteractions.resolve('req-race-1');
2661
+ pendingInteractions.resolve("req-race-1");
2245
2662
  return {
2246
- disposition: 'approve_once' as const,
2247
- replyText: 'Approved! Running the command now.',
2663
+ disposition: "approve_once" as const,
2664
+ replyText: "Approved! Running the command now.",
2248
2665
  };
2249
2666
  });
2250
2667
 
2251
- const req = makeInboundRequest({ content: 'go ahead' });
2668
+ const req = makeInboundRequest({ content: "go ahead" });
2252
2669
  const res = await handleChannelInbound(
2253
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
2670
+ req,
2671
+ noopProcessMessage,
2672
+ "self",
2673
+ undefined,
2674
+ mockConversationGenerator,
2254
2675
  );
2255
- const body = await res.json() as Record<string, unknown>;
2676
+ const body = (await res.json()) as Record<string, unknown>;
2256
2677
 
2257
2678
  expect(body.accepted).toBe(true);
2258
- expect(body.approval).toBe('stale_ignored');
2679
+ expect(body.approval).toBe("stale_ignored");
2259
2680
 
2260
2681
  // The engine's optimistic "Approved!" reply should NOT have been delivered
2261
- const approvedReply = deliverSpy.mock.calls.find(
2262
- (call) => (call[1] as { text?: string }).text?.includes('Approved!'),
2682
+ const approvedReply = deliverSpy.mock.calls.find((call) =>
2683
+ (call[1] as { text?: string }).text?.includes("Approved!"),
2263
2684
  );
2264
2685
  expect(approvedReply).toBeUndefined();
2265
2686
 
2266
2687
  // A stale notice should have been delivered instead
2267
- const staleReply = deliverSpy.mock.calls.find(
2268
- (call) => (call[1] as { text?: string }).text?.includes('already been resolved'),
2688
+ const staleReply = deliverSpy.mock.calls.find((call) =>
2689
+ (call[1] as { text?: string }).text?.includes("already been resolved"),
2269
2690
  );
2270
2691
  expect(staleReply).toBeDefined();
2271
2692
 
@@ -2273,33 +2694,36 @@ describe('engine decision race condition — standard path', () => {
2273
2694
  });
2274
2695
  });
2275
2696
 
2276
- describe('engine decision race condition — guardian path', () => {
2277
- test('returns stale_ignored when guardian engine approves but interaction was already resolved', async () => {
2697
+ describe("engine decision race condition — guardian path", () => {
2698
+ test("returns stale_ignored when guardian engine approves but interaction was already resolved", async () => {
2278
2699
  createBinding({
2279
- assistantId: 'self',
2280
- channel: 'telegram',
2281
- guardianExternalUserId: 'guardian-race-user',
2282
- guardianDeliveryChatId: 'guardian-race-chat',
2283
- guardianPrincipalId: 'guardian-race-user',
2700
+ assistantId: "self",
2701
+ channel: "telegram",
2702
+ guardianExternalUserId: "guardian-race-user",
2703
+ guardianDeliveryChatId: "guardian-race-chat",
2704
+ guardianPrincipalId: "guardian-race-user",
2284
2705
  });
2285
2706
 
2286
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2707
+ const deliverSpy = spyOn(
2708
+ gatewayClient,
2709
+ "deliverChannelReply",
2710
+ ).mockResolvedValue(undefined);
2287
2711
 
2288
- const convId = 'conv-guardian-race';
2712
+ const convId = "conv-guardian-race";
2289
2713
  ensureConversation(convId);
2290
2714
 
2291
- registerPendingInteraction('req-grc-1', convId, 'shell');
2715
+ registerPendingInteraction("req-grc-1", convId, "shell");
2292
2716
  createApprovalRequest({
2293
- runId: 'run-grc-1',
2294
- requestId: 'req-grc-1',
2717
+ runId: "run-grc-1",
2718
+ requestId: "req-grc-1",
2295
2719
  conversationId: convId,
2296
- assistantId: 'self',
2297
- channel: 'telegram',
2298
- requesterExternalUserId: 'requester-race-user',
2299
- requesterChatId: 'requester-race-chat',
2300
- guardianExternalUserId: 'guardian-race-user',
2301
- guardianChatId: 'guardian-race-chat',
2302
- toolName: 'shell',
2720
+ assistantId: "self",
2721
+ channel: "telegram",
2722
+ requesterExternalUserId: "requester-race-user",
2723
+ requesterChatId: "requester-race-chat",
2724
+ guardianExternalUserId: "guardian-race-user",
2725
+ guardianChatId: "guardian-race-chat",
2726
+ toolName: "shell",
2303
2727
  expiresAt: Date.now() + 300_000,
2304
2728
  });
2305
2729
 
@@ -2308,35 +2732,39 @@ describe('engine decision race condition — guardian path', () => {
2308
2732
  // Guardian engine returns approve_once, but resolves the pending interaction
2309
2733
  // to simulate a concurrent resolution (expiry sweep or requester cancel)
2310
2734
  const mockConversationGenerator = mock(async (_ctx: unknown) => {
2311
- pendingInteractions.resolve('req-grc-1');
2735
+ pendingInteractions.resolve("req-grc-1");
2312
2736
  return {
2313
- disposition: 'approve_once' as const,
2314
- replyText: 'Approved the request.',
2737
+ disposition: "approve_once" as const,
2738
+ replyText: "Approved the request.",
2315
2739
  };
2316
2740
  });
2317
2741
 
2318
2742
  const guardianReq = makeInboundRequest({
2319
- content: 'approve it',
2320
- conversationExternalId: 'guardian-race-chat',
2321
- actorExternalId: 'guardian-race-user',
2743
+ content: "approve it",
2744
+ conversationExternalId: "guardian-race-chat",
2745
+ actorExternalId: "guardian-race-user",
2322
2746
  });
2323
2747
  const res = await handleChannelInbound(
2324
- guardianReq, noopProcessMessage, 'self', undefined, mockConversationGenerator,
2748
+ guardianReq,
2749
+ noopProcessMessage,
2750
+ "self",
2751
+ undefined,
2752
+ mockConversationGenerator,
2325
2753
  );
2326
- const body = await res.json() as Record<string, unknown>;
2754
+ const body = (await res.json()) as Record<string, unknown>;
2327
2755
 
2328
2756
  expect(body.accepted).toBe(true);
2329
- expect(body.approval).toBe('stale_ignored');
2757
+ expect(body.approval).toBe("stale_ignored");
2330
2758
 
2331
2759
  // The engine's "Approved the request." should NOT be delivered
2332
- const optimisticReply = deliverSpy.mock.calls.find(
2333
- (call) => (call[1] as { text?: string }).text?.includes('Approved the request'),
2760
+ const optimisticReply = deliverSpy.mock.calls.find((call) =>
2761
+ (call[1] as { text?: string }).text?.includes("Approved the request"),
2334
2762
  );
2335
2763
  expect(optimisticReply).toBeUndefined();
2336
2764
 
2337
2765
  // A stale notice should have been delivered instead
2338
- const staleReply = deliverSpy.mock.calls.find(
2339
- (call) => (call[1] as { text?: string }).text?.includes('already been resolved'),
2766
+ const staleReply = deliverSpy.mock.calls.find((call) =>
2767
+ (call[1] as { text?: string }).text?.includes("already been resolved"),
2340
2768
  );
2341
2769
  expect(staleReply).toBeDefined();
2342
2770
 
@@ -2348,88 +2776,114 @@ describe('engine decision race condition — guardian path', () => {
2348
2776
  // Non-decision status reply for different channels
2349
2777
  // ═══════════════════════════════════════════════════════════════════════════
2350
2778
 
2351
- describe('non-decision status reply for different channels', () => {
2779
+ describe("non-decision status reply for different channels", () => {
2352
2780
  beforeEach(() => {
2353
2781
  createBinding({
2354
- assistantId: 'self',
2355
- channel: 'telegram',
2356
- guardianExternalUserId: 'telegram-user-default',
2357
- guardianDeliveryChatId: 'chat-123',
2358
- guardianPrincipalId: 'telegram-user-default',
2782
+ assistantId: "self",
2783
+ channel: "telegram",
2784
+ guardianExternalUserId: "telegram-user-default",
2785
+ guardianDeliveryChatId: "chat-123",
2786
+ guardianPrincipalId: "telegram-user-default",
2359
2787
  });
2360
2788
  createBinding({
2361
- assistantId: 'self',
2362
- channel: 'sms',
2363
- guardianExternalUserId: 'telegram-user-default',
2364
- guardianDeliveryChatId: 'chat-123',
2365
- guardianPrincipalId: 'telegram-user-default',
2789
+ assistantId: "self",
2790
+ channel: "sms",
2791
+ guardianExternalUserId: "telegram-user-default",
2792
+ guardianDeliveryChatId: "chat-123",
2793
+ guardianPrincipalId: "telegram-user-default",
2366
2794
  });
2367
2795
  });
2368
2796
 
2369
- test('non-decision message on non-rich channel (sms) sends status reply', async () => {
2370
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2797
+ test("non-decision message on non-rich channel (sms) sends status reply", async () => {
2798
+ const deliverSpy = spyOn(
2799
+ gatewayClient,
2800
+ "deliverChannelReply",
2801
+ ).mockResolvedValue(undefined);
2371
2802
 
2372
2803
  // Establish the conversation using sms (non-rich channel)
2373
- const initReq = makeInboundRequest({ content: 'init', sourceChannel: 'sms' });
2804
+ const initReq = makeInboundRequest({
2805
+ content: "init",
2806
+ sourceChannel: "sms",
2807
+ });
2374
2808
  await handleChannelInbound(initReq, noopProcessMessage);
2375
2809
 
2376
2810
  const db = getDb();
2377
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
2811
+ const events = db.$client
2812
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
2813
+ .all() as Array<{ conversation_id: string }>;
2378
2814
  const conversationId = events[0]?.conversation_id;
2379
2815
  ensureConversation(conversationId!);
2380
2816
 
2381
- registerPendingInteraction('req-status-sms', conversationId!, 'shell');
2817
+ registerPendingInteraction("req-status-sms", conversationId!, "shell");
2382
2818
 
2383
2819
  // Send a non-decision message
2384
- const req = makeInboundRequest({ content: 'what is happening?', sourceChannel: 'sms' });
2820
+ const req = makeInboundRequest({
2821
+ content: "what is happening?",
2822
+ sourceChannel: "sms",
2823
+ });
2385
2824
  const res = await handleChannelInbound(req, noopProcessMessage);
2386
- const body = await res.json() as Record<string, unknown>;
2825
+ const body = (await res.json()) as Record<string, unknown>;
2387
2826
 
2388
2827
  expect(body.accepted).toBe(true);
2389
- expect(body.approval).toBe('assistant_turn');
2828
+ expect(body.approval).toBe("assistant_turn");
2390
2829
 
2391
2830
  // Status reply delivered via deliverChannelReply
2392
2831
  expect(deliverSpy).toHaveBeenCalled();
2393
2832
  const statusCall = deliverSpy.mock.calls.find(
2394
- (call) => typeof call[1] === 'object' && (call[1] as { chatId?: string }).chatId === 'chat-123',
2833
+ (call) =>
2834
+ typeof call[1] === "object" &&
2835
+ (call[1] as { chatId?: string }).chatId === "chat-123",
2395
2836
  );
2396
2837
  expect(statusCall).toBeDefined();
2397
2838
  const statusPayload = statusCall![1] as { text?: string };
2398
- expect(statusPayload.text).toContain('pending approval request');
2839
+ expect(statusPayload.text).toContain("pending approval request");
2399
2840
 
2400
2841
  deliverSpy.mockRestore();
2401
2842
  });
2402
2843
 
2403
- test('non-decision message on telegram sends status reply', async () => {
2404
- const replySpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
2844
+ test("non-decision message on telegram sends status reply", async () => {
2845
+ const replySpy = spyOn(
2846
+ gatewayClient,
2847
+ "deliverChannelReply",
2848
+ ).mockResolvedValue(undefined);
2405
2849
 
2406
2850
  // Establish the conversation using telegram (rich channel)
2407
- const initReq = makeInboundRequest({ content: 'init', sourceChannel: 'telegram' });
2851
+ const initReq = makeInboundRequest({
2852
+ content: "init",
2853
+ sourceChannel: "telegram",
2854
+ });
2408
2855
  await handleChannelInbound(initReq, noopProcessMessage);
2409
2856
 
2410
2857
  const db = getDb();
2411
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
2858
+ const events = db.$client
2859
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
2860
+ .all() as Array<{ conversation_id: string }>;
2412
2861
  const conversationId = events[0]?.conversation_id;
2413
2862
  ensureConversation(conversationId!);
2414
2863
 
2415
- registerPendingInteraction('req-status-tg', conversationId!, 'shell');
2864
+ registerPendingInteraction("req-status-tg", conversationId!, "shell");
2416
2865
 
2417
2866
  // Send a non-decision message
2418
- const req = makeInboundRequest({ content: 'what is happening?', sourceChannel: 'telegram' });
2867
+ const req = makeInboundRequest({
2868
+ content: "what is happening?",
2869
+ sourceChannel: "telegram",
2870
+ });
2419
2871
  const res = await handleChannelInbound(req, noopProcessMessage);
2420
- const body = await res.json() as Record<string, unknown>;
2872
+ const body = (await res.json()) as Record<string, unknown>;
2421
2873
 
2422
2874
  expect(body.accepted).toBe(true);
2423
- expect(body.approval).toBe('assistant_turn');
2875
+ expect(body.approval).toBe("assistant_turn");
2424
2876
 
2425
2877
  // Status reply delivered via deliverChannelReply
2426
2878
  expect(replySpy).toHaveBeenCalled();
2427
2879
  const statusCall = replySpy.mock.calls.find(
2428
- (call) => typeof call[1] === 'object' && (call[1] as { chatId?: string }).chatId === 'chat-123',
2880
+ (call) =>
2881
+ typeof call[1] === "object" &&
2882
+ (call[1] as { chatId?: string }).chatId === "chat-123",
2429
2883
  );
2430
2884
  expect(statusCall).toBeDefined();
2431
2885
  const statusPayload = statusCall![1] as { text?: string };
2432
- expect(statusPayload.text).toContain('pending approval request');
2886
+ expect(statusPayload.text).toContain("pending approval request");
2433
2887
 
2434
2888
  replySpy.mockRestore();
2435
2889
  });
@@ -2439,46 +2893,54 @@ describe('non-decision status reply for different channels', () => {
2439
2893
  // Background prompt delivery for channel-triggered tool approvals
2440
2894
  // ═══════════════════════════════════════════════════════════════════════════
2441
2895
 
2442
- describe('background channel processing approval prompts', () => {
2443
- test('marks guardian channel turns interactive and delivers approval prompt when confirmation is pending', async () => {
2896
+ describe("background channel processing approval prompts", () => {
2897
+ test("marks guardian channel turns interactive and delivers approval prompt when confirmation is pending", async () => {
2444
2898
  // Set up a guardian binding so the sender is recognized as a guardian
2445
2899
  createBinding({
2446
- assistantId: 'self',
2447
- channel: 'telegram',
2448
- guardianExternalUserId: 'telegram-user-default',
2449
- guardianDeliveryChatId: 'chat-123',
2450
- guardianPrincipalId: 'telegram-user-default',
2900
+ assistantId: "self",
2901
+ channel: "telegram",
2902
+ guardianExternalUserId: "telegram-user-default",
2903
+ guardianDeliveryChatId: "chat-123",
2904
+ guardianPrincipalId: "telegram-user-default",
2451
2905
  });
2452
2906
 
2453
- const deliverPromptSpy = spyOn(gatewayClient, 'deliverApprovalPrompt').mockResolvedValue(undefined);
2907
+ const deliverPromptSpy = spyOn(
2908
+ gatewayClient,
2909
+ "deliverApprovalPrompt",
2910
+ ).mockResolvedValue(undefined);
2454
2911
  const processCalls: Array<{ options?: Record<string, unknown> }> = [];
2455
2912
 
2456
- const processMessage = mock(async (
2457
- conversationId: string,
2458
- _content: string,
2459
- _attachmentIds?: string[],
2460
- options?: Record<string, unknown>,
2461
- ) => {
2462
- processCalls.push({ options });
2463
-
2464
- registerPendingInteraction('req-bg-1', conversationId, 'host_bash', {
2465
- input: { command: 'ls -la' },
2466
- riskLevel: 'medium',
2467
- });
2468
-
2469
- await new Promise((resolve) => setTimeout(resolve, 350));
2470
- return { messageId: 'msg-bg-1' };
2471
- });
2913
+ const processMessage = mock(
2914
+ async (
2915
+ conversationId: string,
2916
+ _content: string,
2917
+ _attachmentIds?: string[],
2918
+ options?: Record<string, unknown>,
2919
+ ) => {
2920
+ processCalls.push({ options });
2921
+
2922
+ registerPendingInteraction("req-bg-1", conversationId, "host_bash", {
2923
+ input: { command: "ls -la" },
2924
+ riskLevel: "medium",
2925
+ });
2926
+
2927
+ await new Promise((resolve) => setTimeout(resolve, 350));
2928
+ return { messageId: "msg-bg-1" };
2929
+ },
2930
+ );
2472
2931
 
2473
2932
  const req = makeInboundRequest({
2474
- content: 'run ls',
2475
- sourceChannel: 'telegram',
2476
- replyCallbackUrl: 'https://gateway.test/deliver/telegram',
2477
- externalMessageId: 'msg-bg-1',
2933
+ content: "run ls",
2934
+ sourceChannel: "telegram",
2935
+ replyCallbackUrl: "https://gateway.test/deliver/telegram",
2936
+ externalMessageId: "msg-bg-1",
2478
2937
  });
2479
2938
 
2480
- const res = await handleChannelInbound(req, processMessage as unknown as typeof noopProcessMessage);
2481
- const body = await res.json() as Record<string, unknown>;
2939
+ const res = await handleChannelInbound(
2940
+ req,
2941
+ processMessage as unknown as typeof noopProcessMessage,
2942
+ );
2943
+ const body = (await res.json()) as Record<string, unknown>;
2482
2944
  expect(body.accepted).toBe(true);
2483
2945
 
2484
2946
  await new Promise((resolve) => setTimeout(resolve, 700));
@@ -2487,52 +2949,67 @@ describe('background channel processing approval prompts', () => {
2487
2949
  expect(processCalls[0].options?.isInteractive).toBe(true);
2488
2950
 
2489
2951
  expect(deliverPromptSpy).toHaveBeenCalled();
2490
- const approvalMeta = deliverPromptSpy.mock.calls[0]?.[3] as { requestId?: string } | undefined;
2491
- expect(approvalMeta?.requestId).toBe('req-bg-1');
2952
+ const approvalMeta = deliverPromptSpy.mock.calls[0]?.[3] as
2953
+ | { requestId?: string }
2954
+ | undefined;
2955
+ expect(approvalMeta?.requestId).toBe("req-bg-1");
2492
2956
 
2493
2957
  deliverPromptSpy.mockRestore();
2494
2958
  });
2495
2959
 
2496
- test('guardian prompt delivery still works when binding ID formatting differs from sender ID', async () => {
2960
+ test("guardian prompt delivery still works when binding ID formatting differs from sender ID", async () => {
2497
2961
  // Guardian binding includes extra whitespace; trust resolution canonicalizes
2498
2962
  // identity and prompt delivery should still treat this sender as the guardian.
2499
2963
  createBinding({
2500
- assistantId: 'self',
2501
- channel: 'telegram',
2502
- guardianExternalUserId: ' telegram-user-default ',
2503
- guardianDeliveryChatId: 'chat-123',
2504
- guardianPrincipalId: ' telegram-user-default ',
2964
+ assistantId: "self",
2965
+ channel: "telegram",
2966
+ guardianExternalUserId: " telegram-user-default ",
2967
+ guardianDeliveryChatId: "chat-123",
2968
+ guardianPrincipalId: " telegram-user-default ",
2505
2969
  });
2506
2970
 
2507
- const deliverPromptSpy = spyOn(gatewayClient, 'deliverApprovalPrompt').mockResolvedValue(undefined);
2971
+ const deliverPromptSpy = spyOn(
2972
+ gatewayClient,
2973
+ "deliverApprovalPrompt",
2974
+ ).mockResolvedValue(undefined);
2508
2975
  const processCalls: Array<{ options?: Record<string, unknown> }> = [];
2509
2976
 
2510
- const processMessage = mock(async (
2511
- conversationId: string,
2512
- _content: string,
2513
- _attachmentIds?: string[],
2514
- options?: Record<string, unknown>,
2515
- ) => {
2516
- processCalls.push({ options });
2517
-
2518
- registerPendingInteraction('req-bg-format-1', conversationId, 'host_bash', {
2519
- input: { command: 'ls -la' },
2520
- riskLevel: 'medium',
2521
- });
2522
-
2523
- await new Promise((resolve) => setTimeout(resolve, 350));
2524
- return { messageId: 'msg-bg-format-1' };
2525
- });
2977
+ const processMessage = mock(
2978
+ async (
2979
+ conversationId: string,
2980
+ _content: string,
2981
+ _attachmentIds?: string[],
2982
+ options?: Record<string, unknown>,
2983
+ ) => {
2984
+ processCalls.push({ options });
2985
+
2986
+ registerPendingInteraction(
2987
+ "req-bg-format-1",
2988
+ conversationId,
2989
+ "host_bash",
2990
+ {
2991
+ input: { command: "ls -la" },
2992
+ riskLevel: "medium",
2993
+ },
2994
+ );
2995
+
2996
+ await new Promise((resolve) => setTimeout(resolve, 350));
2997
+ return { messageId: "msg-bg-format-1" };
2998
+ },
2999
+ );
2526
3000
 
2527
3001
  const req = makeInboundRequest({
2528
- content: 'run ls',
2529
- sourceChannel: 'telegram',
2530
- replyCallbackUrl: 'https://gateway.test/deliver/telegram',
2531
- externalMessageId: 'msg-bg-format-1',
3002
+ content: "run ls",
3003
+ sourceChannel: "telegram",
3004
+ replyCallbackUrl: "https://gateway.test/deliver/telegram",
3005
+ externalMessageId: "msg-bg-format-1",
2532
3006
  });
2533
3007
 
2534
- const res = await handleChannelInbound(req, processMessage as unknown as typeof noopProcessMessage);
2535
- const body = await res.json() as Record<string, unknown>;
3008
+ const res = await handleChannelInbound(
3009
+ req,
3010
+ processMessage as unknown as typeof noopProcessMessage,
3011
+ );
3012
+ const body = (await res.json()) as Record<string, unknown>;
2536
3013
  expect(body.accepted).toBe(true);
2537
3014
 
2538
3015
  await new Promise((resolve) => setTimeout(resolve, 700));
@@ -2544,40 +3021,45 @@ describe('background channel processing approval prompts', () => {
2544
3021
  deliverPromptSpy.mockRestore();
2545
3022
  });
2546
3023
 
2547
- test('trusted-contact channel turns with resolvable guardian route are interactive', async () => {
3024
+ test("trusted-contact channel turns with resolvable guardian route are interactive", async () => {
2548
3025
  // Set up a guardian binding for a DIFFERENT user so the sender is a
2549
3026
  // trusted contact (not the guardian). The guardian route is resolvable
2550
3027
  // because the binding exists — approval notifications can be delivered.
2551
3028
  createBinding({
2552
- assistantId: 'self',
2553
- channel: 'telegram',
2554
- guardianExternalUserId: 'guardian-user-other',
2555
- guardianDeliveryChatId: 'guardian-chat-other',
2556
- guardianPrincipalId: 'guardian-user-other',
3029
+ assistantId: "self",
3030
+ channel: "telegram",
3031
+ guardianExternalUserId: "guardian-user-other",
3032
+ guardianDeliveryChatId: "guardian-chat-other",
3033
+ guardianPrincipalId: "guardian-user-other",
2557
3034
  });
2558
3035
 
2559
3036
  const processCalls: Array<{ options?: Record<string, unknown> }> = [];
2560
3037
 
2561
- const processMessage = mock(async (
2562
- _conversationId: string,
2563
- _content: string,
2564
- _attachmentIds?: string[],
2565
- options?: Record<string, unknown>,
2566
- ) => {
2567
- processCalls.push({ options });
2568
- await new Promise((resolve) => setTimeout(resolve, 50));
2569
- return { messageId: 'msg-ng-1' };
2570
- });
3038
+ const processMessage = mock(
3039
+ async (
3040
+ _conversationId: string,
3041
+ _content: string,
3042
+ _attachmentIds?: string[],
3043
+ options?: Record<string, unknown>,
3044
+ ) => {
3045
+ processCalls.push({ options });
3046
+ await new Promise((resolve) => setTimeout(resolve, 50));
3047
+ return { messageId: "msg-ng-1" };
3048
+ },
3049
+ );
2571
3050
 
2572
3051
  const req = makeInboundRequest({
2573
- content: 'run something',
2574
- sourceChannel: 'telegram',
2575
- replyCallbackUrl: 'https://gateway.test/deliver/telegram',
2576
- externalMessageId: 'msg-ng-1',
3052
+ content: "run something",
3053
+ sourceChannel: "telegram",
3054
+ replyCallbackUrl: "https://gateway.test/deliver/telegram",
3055
+ externalMessageId: "msg-ng-1",
2577
3056
  });
2578
3057
 
2579
- const res = await handleChannelInbound(req, processMessage as unknown as typeof noopProcessMessage);
2580
- const body = await res.json() as Record<string, unknown>;
3058
+ const res = await handleChannelInbound(
3059
+ req,
3060
+ processMessage as unknown as typeof noopProcessMessage,
3061
+ );
3062
+ const body = (await res.json()) as Record<string, unknown>;
2581
3063
  expect(body.accepted).toBe(true);
2582
3064
 
2583
3065
  await new Promise((resolve) => setTimeout(resolve, 300));
@@ -2588,39 +3070,52 @@ describe('background channel processing approval prompts', () => {
2588
3070
  expect(processCalls[0].options?.isInteractive).toBe(true);
2589
3071
  });
2590
3072
 
2591
- test('unverified channel turns never broadcast approval prompts', async () => {
3073
+ test("unverified channel turns never broadcast approval prompts", async () => {
2592
3074
  // No guardian binding is created, so the sender resolves to unverified_channel.
2593
- const deliverPromptSpy = spyOn(gatewayClient, 'deliverApprovalPrompt').mockResolvedValue(undefined);
3075
+ const deliverPromptSpy = spyOn(
3076
+ gatewayClient,
3077
+ "deliverApprovalPrompt",
3078
+ ).mockResolvedValue(undefined);
2594
3079
  const processCalls: Array<{ options?: Record<string, unknown> }> = [];
2595
3080
 
2596
- const processMessage = mock(async (
2597
- conversationId: string,
2598
- _content: string,
2599
- _attachmentIds?: string[],
2600
- options?: Record<string, unknown>,
2601
- ) => {
2602
- processCalls.push({ options });
2603
-
2604
- // Simulate a pending confirmation becoming visible while background
2605
- // processing is running. Unverified actors must still not receive it.
2606
- registerPendingInteraction('req-bg-unverified-1', conversationId, 'host_bash', {
2607
- input: { command: 'ls -la' },
2608
- riskLevel: 'medium',
2609
- });
2610
-
2611
- await new Promise((resolve) => setTimeout(resolve, 350));
2612
- return { messageId: 'msg-bg-unverified-1' };
2613
- });
3081
+ const processMessage = mock(
3082
+ async (
3083
+ conversationId: string,
3084
+ _content: string,
3085
+ _attachmentIds?: string[],
3086
+ options?: Record<string, unknown>,
3087
+ ) => {
3088
+ processCalls.push({ options });
3089
+
3090
+ // Simulate a pending confirmation becoming visible while background
3091
+ // processing is running. Unverified actors must still not receive it.
3092
+ registerPendingInteraction(
3093
+ "req-bg-unverified-1",
3094
+ conversationId,
3095
+ "host_bash",
3096
+ {
3097
+ input: { command: "ls -la" },
3098
+ riskLevel: "medium",
3099
+ },
3100
+ );
3101
+
3102
+ await new Promise((resolve) => setTimeout(resolve, 350));
3103
+ return { messageId: "msg-bg-unverified-1" };
3104
+ },
3105
+ );
2614
3106
 
2615
3107
  const req = makeInboundRequest({
2616
- content: 'run ls',
2617
- sourceChannel: 'telegram',
2618
- replyCallbackUrl: 'https://gateway.test/deliver/telegram',
2619
- externalMessageId: 'msg-bg-unverified-1',
3108
+ content: "run ls",
3109
+ sourceChannel: "telegram",
3110
+ replyCallbackUrl: "https://gateway.test/deliver/telegram",
3111
+ externalMessageId: "msg-bg-unverified-1",
2620
3112
  });
2621
3113
 
2622
- const res = await handleChannelInbound(req, processMessage as unknown as typeof noopProcessMessage);
2623
- const body = await res.json() as Record<string, unknown>;
3114
+ const res = await handleChannelInbound(
3115
+ req,
3116
+ processMessage as unknown as typeof noopProcessMessage,
3117
+ );
3118
+ const body = (await res.json()) as Record<string, unknown>;
2624
3119
  expect(body.accepted).toBe(true);
2625
3120
 
2626
3121
  await new Promise((resolve) => setTimeout(resolve, 700));
@@ -2637,7 +3132,7 @@ describe('background channel processing approval prompts', () => {
2637
3132
  // NL approval routing via destination-scoped canonical requests
2638
3133
  // ═══════════════════════════════════════════════════════════════════════════
2639
3134
 
2640
- describe('NL approval routing via destination-scoped canonical requests', () => {
3135
+ describe("NL approval routing via destination-scoped canonical requests", () => {
2641
3136
  beforeEach(() => {
2642
3137
  resetTables();
2643
3138
  noopProcessMessage.mockClear();
@@ -2645,16 +3140,16 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2645
3140
 
2646
3141
  test('guardian plain-text "yes" fails closed for tool_approval with no guardianExternalUserId', async () => {
2647
3142
  // Simulate a voice-originated tool approval without guardianExternalUserId
2648
- const guardianChatId = 'guardian-chat-nl-1';
2649
- const guardianUserId = 'guardian-user-nl-1';
3143
+ const guardianChatId = "guardian-chat-nl-1";
3144
+ const guardianUserId = "guardian-user-nl-1";
2650
3145
 
2651
3146
  // Ensure the conversation exists so the resolver finds it
2652
- ensureConversation('conv-voice-nl-1');
3147
+ ensureConversation("conv-voice-nl-1");
2653
3148
 
2654
3149
  // Create guardian binding for Telegram
2655
3150
  createBinding({
2656
- assistantId: 'self',
2657
- channel: 'telegram',
3151
+ assistantId: "self",
3152
+ channel: "telegram",
2658
3153
  guardianExternalUserId: guardianUserId,
2659
3154
  guardianDeliveryChatId: guardianChatId,
2660
3155
 
@@ -2664,55 +3159,55 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2664
3159
  // Create canonical tool_approval request WITHOUT guardianExternalUserId
2665
3160
  // but WITH a conversationId (required by the tool_approval resolver)
2666
3161
  const canonicalReq = createCanonicalGuardianRequest({
2667
- kind: 'tool_approval',
2668
- sourceType: 'voice',
2669
- sourceChannel: 'twilio',
2670
- conversationId: 'conv-voice-nl-1',
2671
- toolName: 'shell',
2672
- guardianPrincipalId: 'test-principal-id',
3162
+ kind: "tool_approval",
3163
+ sourceType: "voice",
3164
+ sourceChannel: "twilio",
3165
+ conversationId: "conv-voice-nl-1",
3166
+ toolName: "shell",
3167
+ guardianPrincipalId: "test-principal-id",
2673
3168
  expiresAt: new Date(Date.now() + 60_000).toISOString(),
2674
3169
  // guardianExternalUserId intentionally omitted
2675
3170
  });
2676
3171
 
2677
3172
  // Register pending interaction so resolver can find it
2678
- registerPendingInteraction(canonicalReq.id, 'conv-voice-nl-1', 'shell');
3173
+ registerPendingInteraction(canonicalReq.id, "conv-voice-nl-1", "shell");
2679
3174
 
2680
3175
  // Create canonical delivery row targeting guardian chat
2681
3176
  createCanonicalGuardianDelivery({
2682
3177
  requestId: canonicalReq.id,
2683
- destinationChannel: 'telegram',
3178
+ destinationChannel: "telegram",
2684
3179
  destinationChatId: guardianChatId,
2685
3180
  });
2686
3181
 
2687
3182
  // Send inbound guardian text reply "yes" from that chat
2688
3183
  const req = makeInboundRequest({
2689
- sourceChannel: 'telegram',
3184
+ sourceChannel: "telegram",
2690
3185
  conversationExternalId: guardianChatId,
2691
3186
  actorExternalId: guardianUserId,
2692
- content: 'yes',
3187
+ content: "yes",
2693
3188
  externalMessageId: `msg-nl-approve-${Date.now()}`,
2694
3189
  });
2695
3190
  const res = await handleChannelInbound(req, noopProcessMessage as any);
2696
- const body = await res.json() as Record<string, unknown>;
3191
+ const body = (await res.json()) as Record<string, unknown>;
2697
3192
 
2698
3193
  expect(body.accepted).toBe(true);
2699
- expect(body.canonicalRouter).toBe('canonical_decision_stale');
3194
+ expect(body.canonicalRouter).toBe("canonical_decision_stale");
2700
3195
 
2701
3196
  // Verify the request remains pending (identity-bound fail-closed).
2702
3197
  const resolved = getCanonicalGuardianRequest(canonicalReq.id);
2703
3198
  expect(resolved).not.toBeNull();
2704
- expect(resolved!.status).toBe('pending');
3199
+ expect(resolved!.status).toBe("pending");
2705
3200
  });
2706
3201
 
2707
- test('inbound from different chat ID does not auto-match delivery-scoped canonical request', async () => {
2708
- const guardianChatId = 'guardian-chat-nl-2';
2709
- const guardianUserId = 'guardian-user-nl-2';
2710
- const differentChatId = 'different-chat-999';
3202
+ test("inbound from different chat ID does not auto-match delivery-scoped canonical request", async () => {
3203
+ const guardianChatId = "guardian-chat-nl-2";
3204
+ const guardianUserId = "guardian-user-nl-2";
3205
+ const differentChatId = "different-chat-999";
2711
3206
 
2712
3207
  // Create guardian binding for the guardian user on the different chat
2713
3208
  createBinding({
2714
- assistantId: 'self',
2715
- channel: 'telegram',
3209
+ assistantId: "self",
3210
+ channel: "telegram",
2716
3211
  guardianExternalUserId: guardianUserId,
2717
3212
  guardianDeliveryChatId: differentChatId,
2718
3213
 
@@ -2721,31 +3216,31 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2721
3216
 
2722
3217
  // Create canonical pending_question WITHOUT guardianExternalUserId
2723
3218
  const canonicalReq = createCanonicalGuardianRequest({
2724
- kind: 'tool_approval',
2725
- sourceType: 'voice',
2726
- sourceChannel: 'twilio',
2727
- toolName: 'shell',
2728
- guardianPrincipalId: 'test-principal-id',
3219
+ kind: "tool_approval",
3220
+ sourceType: "voice",
3221
+ sourceChannel: "twilio",
3222
+ toolName: "shell",
3223
+ guardianPrincipalId: "test-principal-id",
2729
3224
  expiresAt: new Date(Date.now() + 60_000).toISOString(),
2730
3225
  });
2731
3226
 
2732
3227
  // Delivery targets the original guardian chat, NOT the different chat
2733
3228
  createCanonicalGuardianDelivery({
2734
3229
  requestId: canonicalReq.id,
2735
- destinationChannel: 'telegram',
3230
+ destinationChannel: "telegram",
2736
3231
  destinationChatId: guardianChatId,
2737
3232
  });
2738
3233
 
2739
3234
  // Send from differentChatId — delivery-scoped lookup should not match
2740
3235
  const req = makeInboundRequest({
2741
- sourceChannel: 'telegram',
3236
+ sourceChannel: "telegram",
2742
3237
  conversationExternalId: differentChatId,
2743
3238
  actorExternalId: guardianUserId,
2744
- content: 'approve',
3239
+ content: "approve",
2745
3240
  externalMessageId: `msg-nl-mismatch-${Date.now()}`,
2746
3241
  });
2747
3242
  const res = await handleChannelInbound(req, noopProcessMessage as any);
2748
- const body = await res.json() as Record<string, unknown>;
3243
+ const body = (await res.json()) as Record<string, unknown>;
2749
3244
 
2750
3245
  expect(body.accepted).toBe(true);
2751
3246
  // Should NOT have been consumed by canonical router since there are no
@@ -2756,7 +3251,7 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2756
3251
  // Request should remain pending
2757
3252
  const unchanged = getCanonicalGuardianRequest(canonicalReq.id);
2758
3253
  expect(unchanged).not.toBeNull();
2759
- expect(unchanged!.status).toBe('pending');
3254
+ expect(unchanged!.status).toBe("pending");
2760
3255
  });
2761
3256
  });
2762
3257
 
@@ -2764,31 +3259,36 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2764
3259
  // Trusted-contact self-approval guard (pre-row)
2765
3260
  // ═══════════════════════════════════════════════════════════════════════════
2766
3261
 
2767
- describe('trusted-contact self-approval blocked before guardian approval row exists', () => {
3262
+ describe("trusted-contact self-approval blocked before guardian approval row exists", () => {
2768
3263
  beforeEach(() => {
2769
3264
  // Create a guardian binding so the requester resolves as trusted_contact
2770
3265
  createBinding({
2771
- assistantId: 'self',
2772
- channel: 'telegram',
2773
- guardianExternalUserId: 'guardian-tc-selfapproval',
2774
- guardianDeliveryChatId: 'guardian-tc-selfapproval-chat',
2775
- guardianPrincipalId: 'guardian-tc-selfapproval',
3266
+ assistantId: "self",
3267
+ channel: "telegram",
3268
+ guardianExternalUserId: "guardian-tc-selfapproval",
3269
+ guardianDeliveryChatId: "guardian-tc-selfapproval-chat",
3270
+ guardianPrincipalId: "guardian-tc-selfapproval",
2776
3271
  });
2777
3272
  });
2778
3273
 
2779
- test('trusted contact cannot self-approve via conversational engine when no guardian approval row exists', async () => {
2780
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
3274
+ test("trusted contact cannot self-approve via conversational engine when no guardian approval row exists", async () => {
3275
+ const deliverSpy = spyOn(
3276
+ gatewayClient,
3277
+ "deliverChannelReply",
3278
+ ).mockResolvedValue(undefined);
2781
3279
 
2782
3280
  // Create the requester conversation (different user than guardian)
2783
3281
  const initReq = makeInboundRequest({
2784
- content: 'init',
2785
- conversationExternalId: 'tc-selfapproval-chat',
2786
- actorExternalId: 'tc-selfapproval-user',
3282
+ content: "init",
3283
+ conversationExternalId: "tc-selfapproval-chat",
3284
+ actorExternalId: "tc-selfapproval-user",
2787
3285
  });
2788
3286
  await handleChannelInbound(initReq, noopProcessMessage);
2789
3287
 
2790
3288
  const db = getDb();
2791
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
3289
+ const events = db.$client
3290
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
3291
+ .all() as Array<{ conversation_id: string }>;
2792
3292
  const conversationId = events[0]?.conversation_id;
2793
3293
  ensureConversation(conversationId!);
2794
3294
 
@@ -2796,79 +3296,96 @@ describe('trusted-contact self-approval blocked before guardian approval row exi
2796
3296
  // row in channelGuardianApprovalRequests. This simulates the window
2797
3297
  // between the pending confirmation being created (isInteractive=true)
2798
3298
  // and the guardian approval prompt being delivered.
2799
- const sessionMock = registerPendingInteraction('req-tc-selfapproval-1', conversationId!, 'shell');
3299
+ const sessionMock = registerPendingInteraction(
3300
+ "req-tc-selfapproval-1",
3301
+ conversationId!,
3302
+ "shell",
3303
+ );
2800
3304
 
2801
3305
  deliverSpy.mockClear();
2802
3306
 
2803
3307
  // The conversational engine would normally classify "yes" as approve_once,
2804
3308
  // but the guard should intercept before the engine runs.
2805
3309
  const mockConversationGenerator = mock(async (_ctx: unknown) => ({
2806
- disposition: 'approve_once' as const,
2807
- replyText: 'Approved!',
3310
+ disposition: "approve_once" as const,
3311
+ replyText: "Approved!",
2808
3312
  }));
2809
3313
 
2810
3314
  // Trusted contact sends "yes" to try to self-approve
2811
3315
  const req = makeInboundRequest({
2812
- content: 'yes',
2813
- conversationExternalId: 'tc-selfapproval-chat',
2814
- actorExternalId: 'tc-selfapproval-user',
3316
+ content: "yes",
3317
+ conversationExternalId: "tc-selfapproval-chat",
3318
+ actorExternalId: "tc-selfapproval-user",
2815
3319
  });
2816
3320
  const res = await handleChannelInbound(
2817
- req, noopProcessMessage, 'self', undefined, mockConversationGenerator,
3321
+ req,
3322
+ noopProcessMessage,
3323
+ "self",
3324
+ undefined,
3325
+ mockConversationGenerator,
2818
3326
  );
2819
- const body = await res.json() as Record<string, unknown>;
3327
+ const body = (await res.json()) as Record<string, unknown>;
2820
3328
 
2821
3329
  expect(body.accepted).toBe(true);
2822
3330
  // Should be blocked with assistant_turn (pending guardian notice),
2823
3331
  // NOT decision_applied
2824
- expect(body.approval).toBe('assistant_turn');
3332
+ expect(body.approval).toBe("assistant_turn");
2825
3333
  // The session should NOT have been resolved
2826
3334
  expect(sessionMock).not.toHaveBeenCalled();
2827
3335
 
2828
3336
  // The pending interaction should still be registered (not consumed)
2829
- const stillPending = pendingInteractions.get('req-tc-selfapproval-1');
3337
+ const stillPending = pendingInteractions.get("req-tc-selfapproval-1");
2830
3338
  expect(stillPending).toBeDefined();
2831
3339
 
2832
3340
  deliverSpy.mockRestore();
2833
3341
  });
2834
3342
 
2835
- test('trusted contact cannot self-approve via legacy parser when no guardian approval row exists', async () => {
2836
- const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
3343
+ test("trusted contact cannot self-approve via legacy parser when no guardian approval row exists", async () => {
3344
+ const deliverSpy = spyOn(
3345
+ gatewayClient,
3346
+ "deliverChannelReply",
3347
+ ).mockResolvedValue(undefined);
2837
3348
 
2838
3349
  const initReq = makeInboundRequest({
2839
- content: 'init',
2840
- conversationExternalId: 'tc-selfapproval-chat',
2841
- actorExternalId: 'tc-selfapproval-user',
3350
+ content: "init",
3351
+ conversationExternalId: "tc-selfapproval-chat",
3352
+ actorExternalId: "tc-selfapproval-user",
2842
3353
  });
2843
3354
  await handleChannelInbound(initReq, noopProcessMessage);
2844
3355
 
2845
3356
  const db = getDb();
2846
- const events = db.$client.prepare('SELECT conversation_id FROM channel_inbound_events').all() as Array<{ conversation_id: string }>;
3357
+ const events = db.$client
3358
+ .prepare("SELECT conversation_id FROM channel_inbound_events")
3359
+ .all() as Array<{ conversation_id: string }>;
2847
3360
  const conversationId = events[0]?.conversation_id;
2848
3361
  ensureConversation(conversationId!);
2849
3362
 
2850
3363
  // Register pending interaction without guardian approval row
2851
- const sessionMock = registerPendingInteraction('req-tc-selfapproval-2', conversationId!, 'shell');
3364
+ const sessionMock = registerPendingInteraction(
3365
+ "req-tc-selfapproval-2",
3366
+ conversationId!,
3367
+ "shell",
3368
+ );
2852
3369
 
2853
3370
  deliverSpy.mockClear();
2854
3371
 
2855
3372
  // No conversational engine — falls through to legacy parser path.
2856
3373
  // "approve" would normally be parsed as an approval decision.
2857
3374
  const req = makeInboundRequest({
2858
- content: 'approve',
2859
- conversationExternalId: 'tc-selfapproval-chat',
2860
- actorExternalId: 'tc-selfapproval-user',
3375
+ content: "approve",
3376
+ conversationExternalId: "tc-selfapproval-chat",
3377
+ actorExternalId: "tc-selfapproval-user",
2861
3378
  });
2862
3379
  const res = await handleChannelInbound(req, noopProcessMessage);
2863
- const body = await res.json() as Record<string, unknown>;
3380
+ const body = (await res.json()) as Record<string, unknown>;
2864
3381
 
2865
3382
  expect(body.accepted).toBe(true);
2866
3383
  // Should be blocked, not decision_applied
2867
- expect(body.approval).toBe('assistant_turn');
3384
+ expect(body.approval).toBe("assistant_turn");
2868
3385
  expect(sessionMock).not.toHaveBeenCalled();
2869
3386
 
2870
3387
  // Pending interaction should still exist
2871
- const stillPending = pendingInteractions.get('req-tc-selfapproval-2');
3388
+ const stillPending = pendingInteractions.get("req-tc-selfapproval-2");
2872
3389
  expect(stillPending).toBeDefined();
2873
3390
 
2874
3391
  deliverSpy.mockRestore();