@vellumai/assistant 0.4.17 → 0.4.19

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