comisai 1.0.11 → 1.0.12

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 (247) hide show
  1. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +1 -4
  2. package/node_modules/@comis/agent/package.json +1 -1
  3. package/node_modules/@comis/channels/package.json +1 -1
  4. package/node_modules/@comis/cli/package.json +1 -1
  5. package/node_modules/@comis/core/package.json +1 -1
  6. package/node_modules/@comis/daemon/package.json +1 -1
  7. package/node_modules/@comis/gateway/package.json +1 -1
  8. package/node_modules/@comis/infra/package.json +1 -1
  9. package/node_modules/@comis/memory/package.json +1 -1
  10. package/node_modules/@comis/scheduler/package.json +1 -1
  11. package/node_modules/@comis/shared/package.json +1 -1
  12. package/node_modules/@comis/skills/package.json +1 -1
  13. package/package.json +18 -18
  14. package/node_modules/@comis/core/dist/approval/approval-gate.test.d.ts +0 -1
  15. package/node_modules/@comis/core/dist/approval/approval-gate.test.js +0 -1003
  16. package/node_modules/@comis/core/dist/bootstrap.test.d.ts +0 -1
  17. package/node_modules/@comis/core/dist/bootstrap.test.js +0 -150
  18. package/node_modules/@comis/core/dist/config/backup.test.d.ts +0 -1
  19. package/node_modules/@comis/core/dist/config/backup.test.js +0 -160
  20. package/node_modules/@comis/core/dist/config/env-substitution.test.d.ts +0 -1
  21. package/node_modules/@comis/core/dist/config/env-substitution.test.js +0 -259
  22. package/node_modules/@comis/core/dist/config/field-metadata.test.d.ts +0 -1
  23. package/node_modules/@comis/core/dist/config/field-metadata.test.js +0 -72
  24. package/node_modules/@comis/core/dist/config/git-manager.test.d.ts +0 -1
  25. package/node_modules/@comis/core/dist/config/git-manager.test.js +0 -1042
  26. package/node_modules/@comis/core/dist/config/immutable-keys.test.d.ts +0 -1
  27. package/node_modules/@comis/core/dist/config/immutable-keys.test.js +0 -283
  28. package/node_modules/@comis/core/dist/config/include-resolver.test.d.ts +0 -1
  29. package/node_modules/@comis/core/dist/config/include-resolver.test.js +0 -292
  30. package/node_modules/@comis/core/dist/config/layered.test.d.ts +0 -1
  31. package/node_modules/@comis/core/dist/config/layered.test.js +0 -211
  32. package/node_modules/@comis/core/dist/config/loader.test.d.ts +0 -1
  33. package/node_modules/@comis/core/dist/config/loader.test.js +0 -300
  34. package/node_modules/@comis/core/dist/config/migrate.test.d.ts +0 -1
  35. package/node_modules/@comis/core/dist/config/migrate.test.js +0 -244
  36. package/node_modules/@comis/core/dist/config/partial-validator.test.d.ts +0 -1
  37. package/node_modules/@comis/core/dist/config/partial-validator.test.js +0 -98
  38. package/node_modules/@comis/core/dist/config/schema-agent-model.test.d.ts +0 -1
  39. package/node_modules/@comis/core/dist/config/schema-agent-model.test.js +0 -279
  40. package/node_modules/@comis/core/dist/config/schema-agent-session.test.d.ts +0 -1
  41. package/node_modules/@comis/core/dist/config/schema-agent-session.test.js +0 -242
  42. package/node_modules/@comis/core/dist/config/schema-agent.test.d.ts +0 -1
  43. package/node_modules/@comis/core/dist/config/schema-agent.test.js +0 -645
  44. package/node_modules/@comis/core/dist/config/schema-channel.test.d.ts +0 -1
  45. package/node_modules/@comis/core/dist/config/schema-channel.test.js +0 -341
  46. package/node_modules/@comis/core/dist/config/schema-coalescer.test.d.ts +0 -1
  47. package/node_modules/@comis/core/dist/config/schema-coalescer.test.js +0 -51
  48. package/node_modules/@comis/core/dist/config/schema-context-engine.test.d.ts +0 -1
  49. package/node_modules/@comis/core/dist/config/schema-context-engine.test.js +0 -797
  50. package/node_modules/@comis/core/dist/config/schema-context-guard.test.d.ts +0 -1
  51. package/node_modules/@comis/core/dist/config/schema-context-guard.test.js +0 -111
  52. package/node_modules/@comis/core/dist/config/schema-daemon.test.d.ts +0 -1
  53. package/node_modules/@comis/core/dist/config/schema-daemon.test.js +0 -107
  54. package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.d.ts +0 -1
  55. package/node_modules/@comis/core/dist/config/schema-delivery-timing.test.js +0 -51
  56. package/node_modules/@comis/core/dist/config/schema-documentation.test.d.ts +0 -1
  57. package/node_modules/@comis/core/dist/config/schema-documentation.test.js +0 -63
  58. package/node_modules/@comis/core/dist/config/schema-gateway.test.d.ts +0 -1
  59. package/node_modules/@comis/core/dist/config/schema-gateway.test.js +0 -219
  60. package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.d.ts +0 -1
  61. package/node_modules/@comis/core/dist/config/schema-gemini-cache.test.js +0 -41
  62. package/node_modules/@comis/core/dist/config/schema-integrations.test.d.ts +0 -1
  63. package/node_modules/@comis/core/dist/config/schema-integrations.test.js +0 -92
  64. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.d.ts +0 -1
  65. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.test.js +0 -61
  66. package/node_modules/@comis/core/dist/config/schema-misc.test.d.ts +0 -1
  67. package/node_modules/@comis/core/dist/config/schema-misc.test.js +0 -394
  68. package/node_modules/@comis/core/dist/config/schema-observability.test.d.ts +0 -1
  69. package/node_modules/@comis/core/dist/config/schema-observability.test.js +0 -76
  70. package/node_modules/@comis/core/dist/config/schema-providers.test.d.ts +0 -1
  71. package/node_modules/@comis/core/dist/config/schema-providers.test.js +0 -294
  72. package/node_modules/@comis/core/dist/config/schema-queue.test.d.ts +0 -1
  73. package/node_modules/@comis/core/dist/config/schema-queue.test.js +0 -234
  74. package/node_modules/@comis/core/dist/config/schema-response-prefix.test.d.ts +0 -1
  75. package/node_modules/@comis/core/dist/config/schema-response-prefix.test.js +0 -36
  76. package/node_modules/@comis/core/dist/config/schema-security.test.d.ts +0 -1
  77. package/node_modules/@comis/core/dist/config/schema-security.test.js +0 -54
  78. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.d.ts +0 -1
  79. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.test.js +0 -60
  80. package/node_modules/@comis/core/dist/config/schema-serializer.test.d.ts +0 -1
  81. package/node_modules/@comis/core/dist/config/schema-serializer.test.js +0 -73
  82. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.d.ts +0 -1
  83. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.test.js +0 -39
  84. package/node_modules/@comis/core/dist/context/context.test.d.ts +0 -1
  85. package/node_modules/@comis/core/dist/context/context.test.js +0 -276
  86. package/node_modules/@comis/core/dist/domain/agent-response.test.d.ts +0 -1
  87. package/node_modules/@comis/core/dist/domain/agent-response.test.js +0 -159
  88. package/node_modules/@comis/core/dist/domain/credential-mapping.test.d.ts +0 -1
  89. package/node_modules/@comis/core/dist/domain/credential-mapping.test.js +0 -135
  90. package/node_modules/@comis/core/dist/domain/delivery-origin.test.d.ts +0 -1
  91. package/node_modules/@comis/core/dist/domain/delivery-origin.test.js +0 -68
  92. package/node_modules/@comis/core/dist/domain/execution-graph.test.d.ts +0 -1
  93. package/node_modules/@comis/core/dist/domain/execution-graph.test.js +0 -441
  94. package/node_modules/@comis/core/dist/domain/memory-entry.test.d.ts +0 -1
  95. package/node_modules/@comis/core/dist/domain/memory-entry.test.js +0 -193
  96. package/node_modules/@comis/core/dist/domain/normalized-message.test.d.ts +0 -1
  97. package/node_modules/@comis/core/dist/domain/normalized-message.test.js +0 -255
  98. package/node_modules/@comis/core/dist/domain/session-key.test.d.ts +0 -1
  99. package/node_modules/@comis/core/dist/domain/session-key.test.js +0 -291
  100. package/node_modules/@comis/core/dist/domain/subagent-context-config.test.d.ts +0 -1
  101. package/node_modules/@comis/core/dist/domain/subagent-context-config.test.js +0 -131
  102. package/node_modules/@comis/core/dist/domain/subagent-context-types.test.d.ts +0 -1
  103. package/node_modules/@comis/core/dist/domain/subagent-context-types.test.js +0 -182
  104. package/node_modules/@comis/core/dist/event-bus/bus.test.d.ts +0 -1
  105. package/node_modules/@comis/core/dist/event-bus/bus.test.js +0 -152
  106. package/node_modules/@comis/core/dist/event-bus/events-agent.test.d.ts +0 -1
  107. package/node_modules/@comis/core/dist/event-bus/events-agent.test.js +0 -409
  108. package/node_modules/@comis/core/dist/event-bus/events-channel.test.d.ts +0 -1
  109. package/node_modules/@comis/core/dist/event-bus/events-channel.test.js +0 -240
  110. package/node_modules/@comis/core/dist/event-bus/events-infra.test.d.ts +0 -1
  111. package/node_modules/@comis/core/dist/event-bus/events-infra.test.js +0 -416
  112. package/node_modules/@comis/core/dist/event-bus/events-messaging.test.d.ts +0 -1
  113. package/node_modules/@comis/core/dist/event-bus/events-messaging.test.js +0 -433
  114. package/node_modules/@comis/core/dist/event-bus/events.test.d.ts +0 -1
  115. package/node_modules/@comis/core/dist/event-bus/events.test.js +0 -93
  116. package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.d.ts +0 -1
  117. package/node_modules/@comis/core/dist/hooks/hook-runner-global.test.js +0 -29
  118. package/node_modules/@comis/core/dist/hooks/hook-runner.test.d.ts +0 -1
  119. package/node_modules/@comis/core/dist/hooks/hook-runner.test.js +0 -490
  120. package/node_modules/@comis/core/dist/hooks/hook-strategies.test.d.ts +0 -1
  121. package/node_modules/@comis/core/dist/hooks/hook-strategies.test.js +0 -209
  122. package/node_modules/@comis/core/dist/hooks/integration.test.d.ts +0 -1
  123. package/node_modules/@comis/core/dist/hooks/integration.test.js +0 -235
  124. package/node_modules/@comis/core/dist/hooks/plugin-registry.test.d.ts +0 -1
  125. package/node_modules/@comis/core/dist/hooks/plugin-registry.test.js +0 -470
  126. package/node_modules/@comis/core/dist/load-env.test.d.ts +0 -1
  127. package/node_modules/@comis/core/dist/load-env.test.js +0 -21
  128. package/node_modules/@comis/core/dist/security/action-classifier.test.d.ts +0 -1
  129. package/node_modules/@comis/core/dist/security/action-classifier.test.js +0 -298
  130. package/node_modules/@comis/core/dist/security/audit-aggregator.test.d.ts +0 -1
  131. package/node_modules/@comis/core/dist/security/audit-aggregator.test.js +0 -128
  132. package/node_modules/@comis/core/dist/security/audit.test.d.ts +0 -1
  133. package/node_modules/@comis/core/dist/security/audit.test.js +0 -154
  134. package/node_modules/@comis/core/dist/security/canary-token.test.d.ts +0 -1
  135. package/node_modules/@comis/core/dist/security/canary-token.test.js +0 -45
  136. package/node_modules/@comis/core/dist/security/config-redaction.test.d.ts +0 -1
  137. package/node_modules/@comis/core/dist/security/config-redaction.test.js +0 -107
  138. package/node_modules/@comis/core/dist/security/external-content.test.d.ts +0 -1
  139. package/node_modules/@comis/core/dist/security/external-content.test.js +0 -210
  140. package/node_modules/@comis/core/dist/security/injection-patterns.test.d.ts +0 -1
  141. package/node_modules/@comis/core/dist/security/injection-patterns.test.js +0 -213
  142. package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.d.ts +0 -1
  143. package/node_modules/@comis/core/dist/security/injection-rate-limiter.test.js +0 -194
  144. package/node_modules/@comis/core/dist/security/input-guard.test.d.ts +0 -1
  145. package/node_modules/@comis/core/dist/security/input-guard.test.js +0 -215
  146. package/node_modules/@comis/core/dist/security/input-security-guard.test.d.ts +0 -1
  147. package/node_modules/@comis/core/dist/security/input-security-guard.test.js +0 -215
  148. package/node_modules/@comis/core/dist/security/input-validator.test.d.ts +0 -1
  149. package/node_modules/@comis/core/dist/security/input-validator.test.js +0 -133
  150. package/node_modules/@comis/core/dist/security/log-sanitizer.test.d.ts +0 -1
  151. package/node_modules/@comis/core/dist/security/log-sanitizer.test.js +0 -260
  152. package/node_modules/@comis/core/dist/security/memory-write-validator.test.d.ts +0 -1
  153. package/node_modules/@comis/core/dist/security/memory-write-validator.test.js +0 -62
  154. package/node_modules/@comis/core/dist/security/output-guard.test.d.ts +0 -1
  155. package/node_modules/@comis/core/dist/security/output-guard.test.js +0 -397
  156. package/node_modules/@comis/core/dist/security/safe-path.test.d.ts +0 -1
  157. package/node_modules/@comis/core/dist/security/safe-path.test.js +0 -95
  158. package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.d.ts +0 -1
  159. package/node_modules/@comis/core/dist/security/scoped-secret-manager.test.js +0 -231
  160. package/node_modules/@comis/core/dist/security/secret-access.test.d.ts +0 -1
  161. package/node_modules/@comis/core/dist/security/secret-access.test.js +0 -41
  162. package/node_modules/@comis/core/dist/security/secret-crypto.test.d.ts +0 -1
  163. package/node_modules/@comis/core/dist/security/secret-crypto.test.js +0 -113
  164. package/node_modules/@comis/core/dist/security/secret-manager.test.d.ts +0 -1
  165. package/node_modules/@comis/core/dist/security/secret-manager.test.js +0 -394
  166. package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.d.ts +0 -1
  167. package/node_modules/@comis/core/dist/security/secret-ref-resolver.test.js +0 -268
  168. package/node_modules/@comis/core/dist/security/secrets-audit.test.d.ts +0 -10
  169. package/node_modules/@comis/core/dist/security/secrets-audit.test.js +0 -295
  170. package/node_modules/@comis/core/dist/security/ssrf-guard.test.d.ts +0 -1
  171. package/node_modules/@comis/core/dist/security/ssrf-guard.test.js +0 -127
  172. package/node_modules/@comis/core/dist/security/token-generator.test.d.ts +0 -1
  173. package/node_modules/@comis/core/dist/security/token-generator.test.js +0 -35
  174. package/node_modules/@comis/core/dist/tool-metadata.test.d.ts +0 -1
  175. package/node_modules/@comis/core/dist/tool-metadata.test.js +0 -112
  176. package/node_modules/@comis/memory/dist/compaction.test.d.ts +0 -1
  177. package/node_modules/@comis/memory/dist/compaction.test.js +0 -298
  178. package/node_modules/@comis/memory/dist/context-schema.test.d.ts +0 -1
  179. package/node_modules/@comis/memory/dist/context-schema.test.js +0 -246
  180. package/node_modules/@comis/memory/dist/context-store.test.d.ts +0 -1
  181. package/node_modules/@comis/memory/dist/context-store.test.js +0 -708
  182. package/node_modules/@comis/memory/dist/credential-mapping-schema.test.d.ts +0 -7
  183. package/node_modules/@comis/memory/dist/credential-mapping-schema.test.js +0 -73
  184. package/node_modules/@comis/memory/dist/credential-mapping-store.test.d.ts +0 -8
  185. package/node_modules/@comis/memory/dist/credential-mapping-store.test.js +0 -340
  186. package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.d.ts +0 -1
  187. package/node_modules/@comis/memory/dist/delivery-mirror-adapter.test.js +0 -165
  188. package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.d.ts +0 -1
  189. package/node_modules/@comis/memory/dist/delivery-queue-adapter.test.js +0 -263
  190. package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.d.ts +0 -1
  191. package/node_modules/@comis/memory/dist/embedding-batch-indexer.test.js +0 -202
  192. package/node_modules/@comis/memory/dist/embedding-cache-lru.test.d.ts +0 -1
  193. package/node_modules/@comis/memory/dist/embedding-cache-lru.test.js +0 -241
  194. package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.d.ts +0 -8
  195. package/node_modules/@comis/memory/dist/embedding-cache-sqlite.test.js +0 -678
  196. package/node_modules/@comis/memory/dist/embedding-cache.test.d.ts +0 -1
  197. package/node_modules/@comis/memory/dist/embedding-cache.test.js +0 -213
  198. package/node_modules/@comis/memory/dist/embedding-fingerprint.test.d.ts +0 -1
  199. package/node_modules/@comis/memory/dist/embedding-fingerprint.test.js +0 -87
  200. package/node_modules/@comis/memory/dist/embedding-hash.test.d.ts +0 -1
  201. package/node_modules/@comis/memory/dist/embedding-hash.test.js +0 -31
  202. package/node_modules/@comis/memory/dist/embedding-integration.test.d.ts +0 -8
  203. package/node_modules/@comis/memory/dist/embedding-integration.test.js +0 -232
  204. package/node_modules/@comis/memory/dist/embedding-provider-factory.test.d.ts +0 -1
  205. package/node_modules/@comis/memory/dist/embedding-provider-factory.test.js +0 -176
  206. package/node_modules/@comis/memory/dist/embedding-provider-local.test.d.ts +0 -8
  207. package/node_modules/@comis/memory/dist/embedding-provider-local.test.js +0 -76
  208. package/node_modules/@comis/memory/dist/embedding-provider-openai.test.d.ts +0 -8
  209. package/node_modules/@comis/memory/dist/embedding-provider-openai.test.js +0 -100
  210. package/node_modules/@comis/memory/dist/embedding-queue.test.d.ts +0 -1
  211. package/node_modules/@comis/memory/dist/embedding-queue.test.js +0 -236
  212. package/node_modules/@comis/memory/dist/hybrid-search.test.d.ts +0 -1
  213. package/node_modules/@comis/memory/dist/hybrid-search.test.js +0 -476
  214. package/node_modules/@comis/memory/dist/identity-link-store.test.d.ts +0 -1
  215. package/node_modules/@comis/memory/dist/identity-link-store.test.js +0 -88
  216. package/node_modules/@comis/memory/dist/memory-api.test.d.ts +0 -1
  217. package/node_modules/@comis/memory/dist/memory-api.test.js +0 -495
  218. package/node_modules/@comis/memory/dist/memory-concurrency.test.d.ts +0 -20
  219. package/node_modules/@comis/memory/dist/memory-concurrency.test.js +0 -222
  220. package/node_modules/@comis/memory/dist/named-graph-store.test.d.ts +0 -1
  221. package/node_modules/@comis/memory/dist/named-graph-store.test.js +0 -227
  222. package/node_modules/@comis/memory/dist/observability-store.test.d.ts +0 -1
  223. package/node_modules/@comis/memory/dist/observability-store.test.js +0 -704
  224. package/node_modules/@comis/memory/dist/row-mapper.test.d.ts +0 -1
  225. package/node_modules/@comis/memory/dist/row-mapper.test.js +0 -409
  226. package/node_modules/@comis/memory/dist/schema.test.d.ts +0 -1
  227. package/node_modules/@comis/memory/dist/schema.test.js +0 -240
  228. package/node_modules/@comis/memory/dist/secret-store-schema.test.d.ts +0 -7
  229. package/node_modules/@comis/memory/dist/secret-store-schema.test.js +0 -90
  230. package/node_modules/@comis/memory/dist/session-store.test.d.ts +0 -1
  231. package/node_modules/@comis/memory/dist/session-store.test.js +0 -262
  232. package/node_modules/@comis/memory/dist/setup-secrets.test.d.ts +0 -1
  233. package/node_modules/@comis/memory/dist/setup-secrets.test.js +0 -140
  234. package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.d.ts +0 -1
  235. package/node_modules/@comis/memory/dist/sqlite-memory-adapter.test.js +0 -888
  236. package/node_modules/@comis/memory/dist/sqlite-secret-store.test.d.ts +0 -8
  237. package/node_modules/@comis/memory/dist/sqlite-secret-store.test.js +0 -245
  238. package/node_modules/@comis/shared/dist/abort.test.d.ts +0 -1
  239. package/node_modules/@comis/shared/dist/abort.test.js +0 -71
  240. package/node_modules/@comis/shared/dist/result.test.d.ts +0 -1
  241. package/node_modules/@comis/shared/dist/result.test.js +0 -178
  242. package/node_modules/@comis/shared/dist/suppress-error.test.d.ts +0 -1
  243. package/node_modules/@comis/shared/dist/suppress-error.test.js +0 -50
  244. package/node_modules/@comis/shared/dist/timeout.test.d.ts +0 -1
  245. package/node_modules/@comis/shared/dist/timeout.test.js +0 -75
  246. package/node_modules/@comis/shared/dist/ttl-cache.test.d.ts +0 -1
  247. package/node_modules/@comis/shared/dist/ttl-cache.test.js +0 -81
@@ -1,1003 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { createApprovalGate } from "./approval-gate.js";
3
- import { TypedEventBus } from "../event-bus/bus.js";
4
- import { readFileSync } from "node:fs";
5
- import { fileURLToPath } from "node:url";
6
- import { dirname, join } from "node:path";
7
- // ---------------------------------------------------------------------------
8
- // Setup
9
- // ---------------------------------------------------------------------------
10
- let eventBus;
11
- let gate;
12
- const DEFAULT_TIMEOUT_MS = 5000;
13
- function makeRequest(overrides = {}) {
14
- return {
15
- toolName: overrides.toolName ?? "agents.restart",
16
- action: overrides.action ?? "agents.restart",
17
- params: overrides.params ?? { agentId: "bot-1" },
18
- agentId: overrides.agentId ?? "agent-1",
19
- sessionKey: overrides.sessionKey ?? "default:user1:discord",
20
- trustLevel: overrides.trustLevel ?? "user",
21
- };
22
- }
23
- beforeEach(() => {
24
- vi.useFakeTimers();
25
- eventBus = new TypedEventBus();
26
- gate = createApprovalGate({
27
- eventBus,
28
- getTimeoutMs: () => DEFAULT_TIMEOUT_MS,
29
- });
30
- });
31
- afterEach(() => {
32
- gate.dispose();
33
- vi.useRealTimers();
34
- });
35
- // ---------------------------------------------------------------------------
36
- // 1. Request creation
37
- // ---------------------------------------------------------------------------
38
- describe("request creation", () => {
39
- it("requestApproval adds request to pending list", () => {
40
- gate.requestApproval(makeRequest());
41
- expect(gate.pending()).toHaveLength(1);
42
- });
43
- it("requestApproval returns a promise that does not resolve immediately", () => {
44
- const promise = gate.requestApproval(makeRequest());
45
- // Promise should be pending (not resolved yet)
46
- let resolved = false;
47
- promise.then(() => { resolved = true; });
48
- // Flush microtask queue
49
- expect(resolved).toBe(false);
50
- });
51
- it("pending() returns request with all expected fields", () => {
52
- gate.requestApproval(makeRequest({
53
- toolName: "system.reboot",
54
- action: "system.reboot",
55
- params: { target: "host-1" },
56
- agentId: "agent-2",
57
- sessionKey: "default:admin:telegram",
58
- trustLevel: "admin",
59
- }));
60
- const [request] = gate.pending();
61
- expect(request).toBeDefined();
62
- expect(request.toolName).toBe("system.reboot");
63
- expect(request.action).toBe("system.reboot");
64
- expect(request.params).toEqual({ target: "host-1" });
65
- expect(request.agentId).toBe("agent-2");
66
- expect(request.sessionKey).toBe("default:admin:telegram");
67
- expect(request.trustLevel).toBe("admin");
68
- expect(request.requestId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
69
- expect(typeof request.createdAt).toBe("number");
70
- expect(request.timeoutMs).toBe(DEFAULT_TIMEOUT_MS);
71
- });
72
- it("getRequest(id) returns the correct pending request", () => {
73
- gate.requestApproval(makeRequest());
74
- const [pending] = gate.pending();
75
- const found = gate.getRequest(pending.requestId);
76
- expect(found).toBe(pending);
77
- });
78
- it("getRequest(unknown) returns undefined", () => {
79
- expect(gate.getRequest("nonexistent-id")).toBeUndefined();
80
- });
81
- });
82
- // ---------------------------------------------------------------------------
83
- // 2. Event emission -- approval:requested
84
- // ---------------------------------------------------------------------------
85
- describe("approval:requested event", () => {
86
- it("requestApproval emits approval:requested event on the event bus", () => {
87
- const handler = vi.fn();
88
- eventBus.on("approval:requested", handler);
89
- gate.requestApproval(makeRequest());
90
- expect(handler).toHaveBeenCalledOnce();
91
- });
92
- it("event payload contains all APPR-07 fields", () => {
93
- const handler = vi.fn();
94
- eventBus.on("approval:requested", handler);
95
- gate.requestApproval(makeRequest({
96
- toolName: "config.write",
97
- action: "config.write",
98
- params: { section: "agents" },
99
- agentId: "agent-3",
100
- sessionKey: "default:op1:slack",
101
- trustLevel: "admin",
102
- }));
103
- const payload = handler.mock.calls[0][0];
104
- expect(payload.requestId).toMatch(/^[0-9a-f-]{36}$/);
105
- expect(payload.toolName).toBe("config.write");
106
- expect(payload.action).toBe("config.write");
107
- expect(payload.params).toEqual({ section: "agents" });
108
- expect(payload.agentId).toBe("agent-3");
109
- expect(payload.sessionKey).toBe("default:op1:slack");
110
- expect(payload.trustLevel).toBe("admin");
111
- expect(typeof payload.createdAt).toBe("number");
112
- expect(payload.timeoutMs).toBe(DEFAULT_TIMEOUT_MS);
113
- });
114
- });
115
- // ---------------------------------------------------------------------------
116
- // 3. Resolve -- approve
117
- // ---------------------------------------------------------------------------
118
- describe("resolve -- approve", () => {
119
- it("resolveApproval with approved=true resolves the promise with approved=true", async () => {
120
- const promise = gate.requestApproval(makeRequest());
121
- const [pending] = gate.pending();
122
- gate.resolveApproval(pending.requestId, true, "operator");
123
- const result = await promise;
124
- expect(result.approved).toBe(true);
125
- expect(result.approvedBy).toBe("operator");
126
- expect(result.requestId).toBe(pending.requestId);
127
- expect(typeof result.resolvedAt).toBe("number");
128
- });
129
- it("after approval, request is removed from pending list", async () => {
130
- const promise = gate.requestApproval(makeRequest());
131
- const [pending] = gate.pending();
132
- gate.resolveApproval(pending.requestId, true, "admin");
133
- await promise;
134
- expect(gate.pending()).toHaveLength(0);
135
- });
136
- it("pending() returns empty array after sole request resolved", async () => {
137
- const promise = gate.requestApproval(makeRequest());
138
- const [pending] = gate.pending();
139
- gate.resolveApproval(pending.requestId, true, "admin");
140
- await promise;
141
- expect(gate.pending()).toEqual([]);
142
- });
143
- });
144
- // ---------------------------------------------------------------------------
145
- // 4. Resolve -- deny
146
- // ---------------------------------------------------------------------------
147
- describe("resolve -- deny", () => {
148
- it("resolveApproval with approved=false resolves with denial and reason", async () => {
149
- const promise = gate.requestApproval(makeRequest());
150
- const [pending] = gate.pending();
151
- gate.resolveApproval(pending.requestId, false, "operator", "Too risky");
152
- const result = await promise;
153
- expect(result.approved).toBe(false);
154
- expect(result.approvedBy).toBe("operator");
155
- expect(result.reason).toBe("Too risky");
156
- expect(result.requestId).toBe(pending.requestId);
157
- });
158
- it("after denial, request is removed from pending list", async () => {
159
- const promise = gate.requestApproval(makeRequest());
160
- const [pending] = gate.pending();
161
- gate.resolveApproval(pending.requestId, false, "operator", "Denied");
162
- await promise;
163
- expect(gate.pending()).toHaveLength(0);
164
- });
165
- });
166
- // ---------------------------------------------------------------------------
167
- // 5. Event emission -- approval:resolved
168
- // ---------------------------------------------------------------------------
169
- describe("approval:resolved event", () => {
170
- it("resolveApproval emits approval:resolved event", () => {
171
- const handler = vi.fn();
172
- eventBus.on("approval:resolved", handler);
173
- gate.requestApproval(makeRequest());
174
- const [pending] = gate.pending();
175
- gate.resolveApproval(pending.requestId, true, "admin");
176
- expect(handler).toHaveBeenCalledOnce();
177
- });
178
- it("event payload includes requestId, approved, approvedBy, reason, resolvedAt", () => {
179
- const handler = vi.fn();
180
- eventBus.on("approval:resolved", handler);
181
- gate.requestApproval(makeRequest());
182
- const [pending] = gate.pending();
183
- gate.resolveApproval(pending.requestId, false, "operator", "Suspicious");
184
- const payload = handler.mock.calls[0][0];
185
- expect(payload.requestId).toBe(pending.requestId);
186
- expect(payload.approved).toBe(false);
187
- expect(payload.approvedBy).toBe("operator");
188
- expect(payload.reason).toBe("Suspicious");
189
- expect(typeof payload.resolvedAt).toBe("number");
190
- });
191
- });
192
- // ---------------------------------------------------------------------------
193
- // 6. Timeout auto-deny
194
- // ---------------------------------------------------------------------------
195
- describe("timeout auto-deny", () => {
196
- it("unanswered request auto-denies after timeout", async () => {
197
- const promise = gate.requestApproval(makeRequest());
198
- // Advance time past the timeout
199
- vi.advanceTimersByTime(DEFAULT_TIMEOUT_MS + 1);
200
- const result = await promise;
201
- expect(result.approved).toBe(false);
202
- expect(result.approvedBy).toBe("system:timeout");
203
- expect(result.reason).toBe("Approval request timed out");
204
- });
205
- it("approval:resolved event emitted with approvedBy system:timeout", () => {
206
- const handler = vi.fn();
207
- eventBus.on("approval:resolved", handler);
208
- gate.requestApproval(makeRequest());
209
- vi.advanceTimersByTime(DEFAULT_TIMEOUT_MS + 1);
210
- const payload = handler.mock.calls[0][0];
211
- expect(payload.approved).toBe(false);
212
- expect(payload.approvedBy).toBe("system:timeout");
213
- });
214
- it("request removed from pending list after timeout", () => {
215
- gate.requestApproval(makeRequest());
216
- expect(gate.pending()).toHaveLength(1);
217
- vi.advanceTimersByTime(DEFAULT_TIMEOUT_MS + 1);
218
- expect(gate.pending()).toHaveLength(0);
219
- });
220
- });
221
- // ---------------------------------------------------------------------------
222
- // 7. Idempotent resolve
223
- // ---------------------------------------------------------------------------
224
- describe("idempotent resolve", () => {
225
- it("resolving an already-resolved request does not throw", async () => {
226
- const promise = gate.requestApproval(makeRequest());
227
- const [pending] = gate.pending();
228
- const id = pending.requestId;
229
- gate.resolveApproval(id, true, "admin");
230
- await promise;
231
- // Second resolve should be silently ignored
232
- expect(() => gate.resolveApproval(id, false, "admin2", "Late denial")).not.toThrow();
233
- });
234
- it("resolving a timed-out request does not throw", () => {
235
- gate.requestApproval(makeRequest());
236
- const [pending] = gate.pending();
237
- const id = pending.requestId;
238
- vi.advanceTimersByTime(DEFAULT_TIMEOUT_MS + 1);
239
- // Manual resolve after timeout should be silently ignored
240
- expect(() => gate.resolveApproval(id, true, "admin")).not.toThrow();
241
- });
242
- it("resolving an unknown requestId does not throw", () => {
243
- expect(() => gate.resolveApproval("unknown-id", true, "admin")).not.toThrow();
244
- });
245
- });
246
- // ---------------------------------------------------------------------------
247
- // 8. Multiple concurrent requests
248
- // ---------------------------------------------------------------------------
249
- describe("multiple concurrent requests", () => {
250
- it("two requests can be pending simultaneously", () => {
251
- gate.requestApproval(makeRequest({ toolName: "tool-a", action: "tool-a.run" }));
252
- gate.requestApproval(makeRequest({ toolName: "tool-b", action: "tool-b.run" }));
253
- expect(gate.pending()).toHaveLength(2);
254
- });
255
- it("resolving one does not affect the other", async () => {
256
- const promise1 = gate.requestApproval(makeRequest({ toolName: "tool-a", action: "tool-a.run" }));
257
- gate.requestApproval(makeRequest({ toolName: "tool-b", action: "tool-b.run" }));
258
- const pending = gate.pending();
259
- expect(pending).toHaveLength(2);
260
- const first = pending[0];
261
- gate.resolveApproval(first.requestId, true, "admin");
262
- await promise1;
263
- // Second request should still be pending
264
- expect(gate.pending()).toHaveLength(1);
265
- expect(gate.pending()[0].toolName).toBe("tool-b");
266
- });
267
- it("pending() returns correct count throughout lifecycle", async () => {
268
- expect(gate.pending()).toHaveLength(0);
269
- const p1 = gate.requestApproval(makeRequest({ toolName: "tool-1", action: "tool-1.run" }));
270
- expect(gate.pending()).toHaveLength(1);
271
- const p2 = gate.requestApproval(makeRequest({ toolName: "tool-2", action: "tool-2.run" }));
272
- expect(gate.pending()).toHaveLength(2);
273
- const p3 = gate.requestApproval(makeRequest({ toolName: "tool-3", action: "tool-3.run" }));
274
- expect(gate.pending()).toHaveLength(3);
275
- // Resolve first
276
- gate.resolveApproval(gate.pending()[0].requestId, true, "admin");
277
- await p1;
278
- expect(gate.pending()).toHaveLength(2);
279
- // Timeout second
280
- vi.advanceTimersByTime(DEFAULT_TIMEOUT_MS + 1);
281
- await p2;
282
- await p3;
283
- expect(gate.pending()).toHaveLength(0);
284
- });
285
- });
286
- // ---------------------------------------------------------------------------
287
- // 9. dispose()
288
- // ---------------------------------------------------------------------------
289
- describe("dispose", () => {
290
- it("dispose() clears all timers (no lingering timeouts)", () => {
291
- gate.requestApproval(makeRequest({ toolName: "tool-x" }));
292
- gate.requestApproval(makeRequest({ toolName: "tool-y" }));
293
- gate.dispose();
294
- // Advancing timers should not trigger any timeout auto-deny (timers cleared)
295
- const resolvedHandler = vi.fn();
296
- eventBus.on("approval:resolved", resolvedHandler);
297
- vi.advanceTimersByTime(DEFAULT_TIMEOUT_MS * 2);
298
- expect(resolvedHandler).not.toHaveBeenCalled();
299
- });
300
- it("pending() returns empty after dispose", () => {
301
- gate.requestApproval(makeRequest());
302
- expect(gate.pending()).toHaveLength(1);
303
- gate.dispose();
304
- expect(gate.pending()).toHaveLength(0);
305
- });
306
- });
307
- // ---------------------------------------------------------------------------
308
- // 10. Denial cache
309
- // ---------------------------------------------------------------------------
310
- describe("denial cache", () => {
311
- const DENIAL_CACHE_TTL = 30_000;
312
- let gateWithTtl;
313
- beforeEach(() => {
314
- gateWithTtl = createApprovalGate({
315
- eventBus,
316
- getTimeoutMs: () => DEFAULT_TIMEOUT_MS,
317
- getDenialCacheTtlMs: () => DENIAL_CACHE_TTL,
318
- getBatchApprovalTtlMs: () => 0, // Disable approval cache for denial cache tests
319
- });
320
- });
321
- afterEach(() => {
322
- gateWithTtl.dispose();
323
- });
324
- it("second requestApproval for same sessionKey+action returns cached denial instantly after first denial", async () => {
325
- // First request: create and deny
326
- const promise1 = gateWithTtl.requestApproval(makeRequest());
327
- const [pending1] = gateWithTtl.pending();
328
- gateWithTtl.resolveApproval(pending1.requestId, false, "operator", "Not allowed");
329
- const result1 = await promise1;
330
- expect(result1.approved).toBe(false);
331
- // Second request: same sessionKey + action should resolve instantly from cache
332
- const promise2 = gateWithTtl.requestApproval(makeRequest());
333
- const result2 = await promise2;
334
- expect(result2.approved).toBe(false);
335
- // Should NOT appear in pending list (resolved instantly from cache)
336
- expect(gateWithTtl.pending()).toHaveLength(0);
337
- });
338
- it("cached denial uses approvedBy 'system:cached-denial'", async () => {
339
- const promise1 = gateWithTtl.requestApproval(makeRequest());
340
- const [pending1] = gateWithTtl.pending();
341
- gateWithTtl.resolveApproval(pending1.requestId, false, "operator", "No");
342
- await promise1;
343
- const promise2 = gateWithTtl.requestApproval(makeRequest());
344
- const result2 = await promise2;
345
- expect(result2.approvedBy).toBe("system:cached-denial");
346
- expect(result2.reason).toContain("Auto-denied");
347
- expect(result2.reason).toContain("agents.restart");
348
- });
349
- it("cached denial expires after TTL and next request creates a real pending entry", async () => {
350
- const promise1 = gateWithTtl.requestApproval(makeRequest());
351
- const [pending1] = gateWithTtl.pending();
352
- gateWithTtl.resolveApproval(pending1.requestId, false, "operator", "No");
353
- await promise1;
354
- // Advance past denial cache TTL
355
- vi.advanceTimersByTime(DENIAL_CACHE_TTL + 1);
356
- // Next request should create a real pending entry (cache expired)
357
- gateWithTtl.requestApproval(makeRequest());
358
- expect(gateWithTtl.pending()).toHaveLength(1);
359
- });
360
- it("approval (not denial) does NOT populate the denial cache", async () => {
361
- // Approve the first request
362
- const promise1 = gateWithTtl.requestApproval(makeRequest());
363
- const [pending1] = gateWithTtl.pending();
364
- gateWithTtl.resolveApproval(pending1.requestId, true, "operator");
365
- await promise1;
366
- // Second request should create a real pending entry (no cache)
367
- gateWithTtl.requestApproval(makeRequest());
368
- expect(gateWithTtl.pending()).toHaveLength(1);
369
- });
370
- it("different action on same sessionKey is NOT cached", async () => {
371
- const promise1 = gateWithTtl.requestApproval(makeRequest({ action: "agents.create" }));
372
- const [pending1] = gateWithTtl.pending();
373
- gateWithTtl.resolveApproval(pending1.requestId, false, "operator", "No");
374
- await promise1;
375
- // Same sessionKey but different action — should create a real pending entry
376
- gateWithTtl.requestApproval(makeRequest({ action: "agents.delete" }));
377
- expect(gateWithTtl.pending()).toHaveLength(1);
378
- });
379
- it("clearDenialCache(sessionKey) removes entries for that session", async () => {
380
- const promise1 = gateWithTtl.requestApproval(makeRequest());
381
- const [pending1] = gateWithTtl.pending();
382
- gateWithTtl.resolveApproval(pending1.requestId, false, "operator", "No");
383
- await promise1;
384
- // Clear cache for this session
385
- gateWithTtl.clearDenialCache("default:user1:discord");
386
- // Next request should create a real pending entry (cache cleared)
387
- gateWithTtl.requestApproval(makeRequest());
388
- expect(gateWithTtl.pending()).toHaveLength(1);
389
- });
390
- it("clearDenialCache() with no args clears all entries", async () => {
391
- // Deny for two different sessions
392
- const promise1 = gateWithTtl.requestApproval(makeRequest({ sessionKey: "default:user1:discord" }));
393
- const [p1] = gateWithTtl.pending();
394
- gateWithTtl.resolveApproval(p1.requestId, false, "op", "No");
395
- await promise1;
396
- const promise2 = gateWithTtl.requestApproval(makeRequest({ sessionKey: "default:user2:telegram" }));
397
- const [p2] = gateWithTtl.pending();
398
- gateWithTtl.resolveApproval(p2.requestId, false, "op", "No");
399
- await promise2;
400
- // Clear all denial cache entries
401
- gateWithTtl.clearDenialCache();
402
- // Both sessions should create real pending entries
403
- gateWithTtl.requestApproval(makeRequest({ sessionKey: "default:user1:discord" }));
404
- gateWithTtl.requestApproval(makeRequest({ sessionKey: "default:user2:telegram" }));
405
- expect(gateWithTtl.pending()).toHaveLength(2);
406
- });
407
- it("dispose() clears denial cache", async () => {
408
- const promise1 = gateWithTtl.requestApproval(makeRequest());
409
- const [pending1] = gateWithTtl.pending();
410
- gateWithTtl.resolveApproval(pending1.requestId, false, "operator", "No");
411
- await promise1;
412
- gateWithTtl.dispose();
413
- // After dispose, create a fresh gate with same eventBus to test that
414
- // the old gate's denial cache was cleared. However, since denial cache
415
- // is internal to the gate instance, we test by creating a new request
416
- // on a NEW gate (the old gate's cache is irrelevant after dispose).
417
- // Instead, test that the old gate does not return cached denials after
418
- // dispose by checking that a new request on a fresh gate works normally.
419
- const freshGate = createApprovalGate({
420
- eventBus,
421
- getTimeoutMs: () => DEFAULT_TIMEOUT_MS,
422
- getDenialCacheTtlMs: () => DENIAL_CACHE_TTL,
423
- });
424
- freshGate.requestApproval(makeRequest());
425
- expect(freshGate.pending()).toHaveLength(1);
426
- freshGate.dispose();
427
- });
428
- it("cached denial emits approval:resolved event", async () => {
429
- const promise1 = gateWithTtl.requestApproval(makeRequest());
430
- const [pending1] = gateWithTtl.pending();
431
- gateWithTtl.resolveApproval(pending1.requestId, false, "operator", "No");
432
- await promise1;
433
- const resolvedHandler = vi.fn();
434
- eventBus.on("approval:resolved", resolvedHandler);
435
- const promise2 = gateWithTtl.requestApproval(makeRequest());
436
- await promise2;
437
- expect(resolvedHandler).toHaveBeenCalledOnce();
438
- const payload = resolvedHandler.mock.calls[0][0];
439
- expect(payload.approved).toBe(false);
440
- expect(payload.approvedBy).toBe("system:cached-denial");
441
- });
442
- it("timeout denial does NOT populate the denial cache", async () => {
443
- // First request: let it time out
444
- const promise1 = gateWithTtl.requestApproval(makeRequest());
445
- vi.advanceTimersByTime(DEFAULT_TIMEOUT_MS + 1);
446
- const result1 = await promise1;
447
- expect(result1.approved).toBe(false);
448
- expect(result1.approvedBy).toBe("system:timeout");
449
- // Second identical request: should create a real pending entry (no cached denial)
450
- gateWithTtl.requestApproval(makeRequest());
451
- expect(gateWithTtl.pending()).toHaveLength(1);
452
- });
453
- it("explicit denial still populates cache even after timeout fix", async () => {
454
- // First request: deny explicitly
455
- const promise1 = gateWithTtl.requestApproval(makeRequest());
456
- const [pending1] = gateWithTtl.pending();
457
- gateWithTtl.resolveApproval(pending1.requestId, false, "operator", "Nope");
458
- await promise1;
459
- // Second identical request: should return from cache
460
- const promise2 = gateWithTtl.requestApproval(makeRequest());
461
- const result2 = await promise2;
462
- expect(result2.approved).toBe(false);
463
- expect(result2.approvedBy).toBe("system:cached-denial");
464
- });
465
- });
466
- // ---------------------------------------------------------------------------
467
- // 11. Batch parallel requests
468
- // ---------------------------------------------------------------------------
469
- describe("batch parallel requests", () => {
470
- it("second request for same sessionKey::action joins existing pending instead of creating new entry", async () => {
471
- const promise1 = gate.requestApproval(makeRequest());
472
- const promise2 = gate.requestApproval(makeRequest());
473
- // Only one pending entry (second batched onto first)
474
- expect(gate.pending()).toHaveLength(1);
475
- // Resolve the single pending entry
476
- const [pending] = gate.pending();
477
- gate.resolveApproval(pending.requestId, true, "admin");
478
- const [result1, result2] = await Promise.all([promise1, promise2]);
479
- expect(result1.approved).toBe(true);
480
- expect(result1.approvedBy).toBe("admin");
481
- expect(result2.approved).toBe(true);
482
- expect(result2.approvedBy).toBe("admin");
483
- });
484
- it("batched follower resolves when primary is approved", async () => {
485
- const promise1 = gate.requestApproval(makeRequest());
486
- const promise2 = gate.requestApproval(makeRequest());
487
- const [pending] = gate.pending();
488
- gate.resolveApproval(pending.requestId, true, "operator");
489
- const [result1, result2] = await Promise.all([promise1, promise2]);
490
- expect(result1.approved).toBe(true);
491
- expect(result2.approved).toBe(true);
492
- });
493
- it("batched follower resolves when primary times out", async () => {
494
- const promise1 = gate.requestApproval(makeRequest());
495
- const promise2 = gate.requestApproval(makeRequest());
496
- vi.advanceTimersByTime(DEFAULT_TIMEOUT_MS + 1);
497
- const [result1, result2] = await Promise.all([promise1, promise2]);
498
- expect(result1.approved).toBe(false);
499
- expect(result1.approvedBy).toBe("system:timeout");
500
- expect(result2.approved).toBe(false);
501
- expect(result2.approvedBy).toBe("system:timeout");
502
- });
503
- it("different action on same sessionKey creates separate pending entries (no batching)", () => {
504
- gate.requestApproval(makeRequest({ action: "agents.create" }));
505
- gate.requestApproval(makeRequest({ action: "agents.delete" }));
506
- expect(gate.pending()).toHaveLength(2);
507
- });
508
- it("approval:requested event emitted only once for batched requests", () => {
509
- const handler = vi.fn();
510
- eventBus.on("approval:requested", handler);
511
- gate.requestApproval(makeRequest());
512
- gate.requestApproval(makeRequest());
513
- // Only the primary request emits the event; the follower joins silently
514
- expect(handler).toHaveBeenCalledOnce();
515
- });
516
- });
517
- // ---------------------------------------------------------------------------
518
- // 12. Serialization and restore
519
- // ---------------------------------------------------------------------------
520
- describe("serialization and restore", () => {
521
- it("serializePending() returns correct data for pending requests", () => {
522
- gate.requestApproval(makeRequest({ action: "agents.create", toolName: "agents_manage" }));
523
- gate.requestApproval(makeRequest({ action: "agents.delete", toolName: "agents_manage" }));
524
- const serialized = gate.serializePending();
525
- expect(serialized).toHaveLength(2);
526
- expect(serialized[0].action).toBe("agents.create");
527
- expect(serialized[0].toolName).toBe("agents_manage");
528
- expect(serialized[0].sessionKey).toBe("default:user1:discord");
529
- expect(serialized[0].trustLevel).toBe("user");
530
- expect(typeof serialized[0].requestId).toBe("string");
531
- expect(typeof serialized[0].createdAt).toBe("number");
532
- expect(serialized[0].timeoutMs).toBe(DEFAULT_TIMEOUT_MS);
533
- expect(serialized[1].action).toBe("agents.delete");
534
- });
535
- it("restorePending() restores with correct remaining timeout", async () => {
536
- const now = Date.now();
537
- const record = {
538
- requestId: "00000000-0000-0000-0000-000000000001",
539
- toolName: "agents_manage",
540
- action: "agents.create",
541
- params: { agent_id: "bot-1" },
542
- agentId: "agent-1",
543
- sessionKey: "default:user1:discord",
544
- trustLevel: "user",
545
- createdAt: now - 1000, // 1 second ago
546
- timeoutMs: 5000, // 5 second timeout -> 4 seconds remaining
547
- };
548
- const restored = gate.restorePending([record]);
549
- expect(restored).toBe(1);
550
- expect(gate.pending()).toHaveLength(1);
551
- expect(gate.pending()[0].requestId).toBe("00000000-0000-0000-0000-000000000001");
552
- // Advance 3999ms -- should still be pending (4000ms remaining)
553
- vi.advanceTimersByTime(3999);
554
- expect(gate.pending()).toHaveLength(1);
555
- // Advance 2 more ms -- should have timed out
556
- vi.advanceTimersByTime(2);
557
- expect(gate.pending()).toHaveLength(0);
558
- });
559
- it("restorePending() skips expired records", () => {
560
- const now = Date.now();
561
- const record = {
562
- requestId: "00000000-0000-0000-0000-000000000002",
563
- toolName: "agents_manage",
564
- action: "agents.create",
565
- params: { agent_id: "bot-2" },
566
- agentId: "agent-1",
567
- sessionKey: "default:user1:discord",
568
- trustLevel: "user",
569
- createdAt: now - 10000, // 10 seconds ago
570
- timeoutMs: 5000, // 5 second timeout -- already expired
571
- };
572
- const restored = gate.restorePending([record]);
573
- expect(restored).toBe(0);
574
- expect(gate.pending()).toHaveLength(0);
575
- });
576
- it("restorePending() emits approval:requested events for restored entries", () => {
577
- const handler = vi.fn();
578
- eventBus.on("approval:requested", handler);
579
- const now = Date.now();
580
- const record = {
581
- requestId: "00000000-0000-0000-0000-000000000003",
582
- toolName: "agents_manage",
583
- action: "agents.delete",
584
- params: { agent_id: "bot-3" },
585
- agentId: "agent-1",
586
- sessionKey: "default:user1:discord",
587
- trustLevel: "admin",
588
- createdAt: now - 500,
589
- timeoutMs: 10000,
590
- };
591
- gate.restorePending([record]);
592
- expect(handler).toHaveBeenCalledOnce();
593
- const payload = handler.mock.calls[0][0];
594
- expect(payload.requestId).toBe("00000000-0000-0000-0000-000000000003");
595
- expect(payload.action).toBe("agents.delete");
596
- expect(payload.toolName).toBe("agents_manage");
597
- });
598
- });
599
- // ---------------------------------------------------------------------------
600
- // 13. Dispose with system:shutdown
601
- // ---------------------------------------------------------------------------
602
- describe("dispose with system:shutdown", () => {
603
- it("dispose() resolves pending promises with system:shutdown denial", async () => {
604
- const promise = gate.requestApproval(makeRequest());
605
- gate.dispose();
606
- const result = await promise;
607
- expect(result.approved).toBe(false);
608
- expect(result.approvedBy).toBe("system:shutdown");
609
- expect(result.reason).toBe("Daemon shutting down");
610
- });
611
- it("system:shutdown denial does NOT populate denial cache", async () => {
612
- const DENIAL_CACHE_TTL = 30_000;
613
- const gateWithTtl = createApprovalGate({
614
- eventBus,
615
- getTimeoutMs: () => DEFAULT_TIMEOUT_MS,
616
- getDenialCacheTtlMs: () => DENIAL_CACHE_TTL,
617
- getBatchApprovalTtlMs: () => 0, // Disable approval cache for this test
618
- });
619
- // Create a request and resolve it with system:shutdown directly
620
- const promise = gateWithTtl.requestApproval(makeRequest());
621
- const [pending] = gateWithTtl.pending();
622
- gateWithTtl.resolveApproval(pending.requestId, false, "system:shutdown", "Shutting down");
623
- await promise;
624
- // Next identical request should create a real pending entry (not cached)
625
- gateWithTtl.requestApproval(makeRequest());
626
- expect(gateWithTtl.pending()).toHaveLength(1);
627
- gateWithTtl.dispose();
628
- });
629
- });
630
- // ---------------------------------------------------------------------------
631
- // 14. Approval cache
632
- // ---------------------------------------------------------------------------
633
- describe("approval cache", () => {
634
- const APPROVAL_CACHE_TTL = 15_000;
635
- let gateWithApprovalCache;
636
- beforeEach(() => {
637
- gateWithApprovalCache = createApprovalGate({
638
- eventBus,
639
- getTimeoutMs: () => DEFAULT_TIMEOUT_MS,
640
- getDenialCacheTtlMs: () => 30_000,
641
- getBatchApprovalTtlMs: () => APPROVAL_CACHE_TTL,
642
- });
643
- });
644
- afterEach(() => {
645
- gateWithApprovalCache.dispose();
646
- });
647
- // -- Cache hit tests --
648
- it("second requestApproval for same sessionKey+action returns cached approval instantly after first approval", async () => {
649
- // First request: create and approve
650
- const promise1 = gateWithApprovalCache.requestApproval(makeRequest());
651
- const [pending1] = gateWithApprovalCache.pending();
652
- gateWithApprovalCache.resolveApproval(pending1.requestId, true, "operator");
653
- const result1 = await promise1;
654
- expect(result1.approved).toBe(true);
655
- // Second request: same sessionKey + action should resolve instantly from cache
656
- const promise2 = gateWithApprovalCache.requestApproval(makeRequest());
657
- const result2 = await promise2;
658
- expect(result2.approved).toBe(true);
659
- // Should NOT appear in pending list (resolved instantly from cache)
660
- expect(gateWithApprovalCache.pending()).toHaveLength(0);
661
- });
662
- it("cached approval uses approvedBy 'system:cached-approval'", async () => {
663
- const promise1 = gateWithApprovalCache.requestApproval(makeRequest());
664
- const [pending1] = gateWithApprovalCache.pending();
665
- gateWithApprovalCache.resolveApproval(pending1.requestId, true, "operator");
666
- await promise1;
667
- const promise2 = gateWithApprovalCache.requestApproval(makeRequest());
668
- const result2 = await promise2;
669
- expect(result2.approvedBy).toBe("system:cached-approval");
670
- expect(result2.reason).toContain("Auto-approved");
671
- expect(result2.reason).toContain("agents.restart");
672
- });
673
- it("cached approval emits approval:resolved event", async () => {
674
- const promise1 = gateWithApprovalCache.requestApproval(makeRequest());
675
- const [pending1] = gateWithApprovalCache.pending();
676
- gateWithApprovalCache.resolveApproval(pending1.requestId, true, "operator");
677
- await promise1;
678
- const resolvedHandler = vi.fn();
679
- eventBus.on("approval:resolved", resolvedHandler);
680
- const promise2 = gateWithApprovalCache.requestApproval(makeRequest());
681
- await promise2;
682
- expect(resolvedHandler).toHaveBeenCalledOnce();
683
- const payload = resolvedHandler.mock.calls[0][0];
684
- expect(payload.approved).toBe(true);
685
- expect(payload.approvedBy).toBe("system:cached-approval");
686
- });
687
- // -- Cache miss tests --
688
- it("cached approval expires after TTL and next request creates a real pending entry", async () => {
689
- const promise1 = gateWithApprovalCache.requestApproval(makeRequest());
690
- const [pending1] = gateWithApprovalCache.pending();
691
- gateWithApprovalCache.resolveApproval(pending1.requestId, true, "operator");
692
- await promise1;
693
- // Advance past approval cache TTL
694
- vi.advanceTimersByTime(APPROVAL_CACHE_TTL + 1);
695
- // Next request should create a real pending entry (cache expired)
696
- gateWithApprovalCache.requestApproval(makeRequest());
697
- expect(gateWithApprovalCache.pending()).toHaveLength(1);
698
- });
699
- it("different action on same sessionKey is NOT cached", async () => {
700
- const promise1 = gateWithApprovalCache.requestApproval(makeRequest({ action: "agents.create" }));
701
- const [pending1] = gateWithApprovalCache.pending();
702
- gateWithApprovalCache.resolveApproval(pending1.requestId, true, "operator");
703
- await promise1;
704
- // Same sessionKey but different action -- should create a real pending entry
705
- gateWithApprovalCache.requestApproval(makeRequest({ action: "agents.delete" }));
706
- expect(gateWithApprovalCache.pending()).toHaveLength(1);
707
- });
708
- it("denial does NOT populate the approval cache", async () => {
709
- // Deny the first request
710
- const promise1 = gateWithApprovalCache.requestApproval(makeRequest());
711
- const [pending1] = gateWithApprovalCache.pending();
712
- gateWithApprovalCache.resolveApproval(pending1.requestId, false, "operator", "Not allowed");
713
- await promise1;
714
- // Clear denial cache so we can test approval cache in isolation
715
- gateWithApprovalCache.clearDenialCache();
716
- // Second request should create a real pending entry (no cached approval)
717
- gateWithApprovalCache.requestApproval(makeRequest());
718
- expect(gateWithApprovalCache.pending()).toHaveLength(1);
719
- });
720
- // -- Disable test --
721
- it("batchApprovalTtlMs=0 disables approval cache entirely", async () => {
722
- const disabledGate = createApprovalGate({
723
- eventBus,
724
- getTimeoutMs: () => DEFAULT_TIMEOUT_MS,
725
- getBatchApprovalTtlMs: () => 0,
726
- });
727
- // Approve first request
728
- const promise1 = disabledGate.requestApproval(makeRequest());
729
- const [pending1] = disabledGate.pending();
730
- disabledGate.resolveApproval(pending1.requestId, true, "operator");
731
- await promise1;
732
- // Second request should create a real pending entry (cache disabled)
733
- disabledGate.requestApproval(makeRequest());
734
- expect(disabledGate.pending()).toHaveLength(1);
735
- disabledGate.dispose();
736
- });
737
- // -- Clear tests --
738
- it("clearApprovalCache(sessionKey) removes entries for that session", async () => {
739
- const promise1 = gateWithApprovalCache.requestApproval(makeRequest());
740
- const [pending1] = gateWithApprovalCache.pending();
741
- gateWithApprovalCache.resolveApproval(pending1.requestId, true, "operator");
742
- await promise1;
743
- // Clear cache for this session
744
- gateWithApprovalCache.clearApprovalCache("default:user1:discord");
745
- // Next request should create a real pending entry (cache cleared)
746
- gateWithApprovalCache.requestApproval(makeRequest());
747
- expect(gateWithApprovalCache.pending()).toHaveLength(1);
748
- });
749
- it("clearApprovalCache() with no args clears all entries", async () => {
750
- // Approve for two different sessions
751
- const promise1 = gateWithApprovalCache.requestApproval(makeRequest({ sessionKey: "default:user1:discord" }));
752
- const [p1] = gateWithApprovalCache.pending();
753
- gateWithApprovalCache.resolveApproval(p1.requestId, true, "op");
754
- await promise1;
755
- const promise2 = gateWithApprovalCache.requestApproval(makeRequest({ sessionKey: "default:user2:telegram" }));
756
- const [p2] = gateWithApprovalCache.pending();
757
- gateWithApprovalCache.resolveApproval(p2.requestId, true, "op");
758
- await promise2;
759
- // Clear all approval cache entries
760
- gateWithApprovalCache.clearApprovalCache();
761
- // Both sessions should create real pending entries
762
- gateWithApprovalCache.requestApproval(makeRequest({ sessionKey: "default:user1:discord" }));
763
- gateWithApprovalCache.requestApproval(makeRequest({ sessionKey: "default:user2:telegram" }));
764
- expect(gateWithApprovalCache.pending()).toHaveLength(2);
765
- });
766
- // -- Dispose test --
767
- it("dispose() clears approval cache", async () => {
768
- const promise1 = gateWithApprovalCache.requestApproval(makeRequest());
769
- const [pending1] = gateWithApprovalCache.pending();
770
- gateWithApprovalCache.resolveApproval(pending1.requestId, true, "operator");
771
- await promise1;
772
- gateWithApprovalCache.dispose();
773
- // After dispose, create a fresh gate to verify old cache is gone
774
- const freshGate = createApprovalGate({
775
- eventBus,
776
- getTimeoutMs: () => DEFAULT_TIMEOUT_MS,
777
- getBatchApprovalTtlMs: () => APPROVAL_CACHE_TTL,
778
- });
779
- freshGate.requestApproval(makeRequest());
780
- expect(freshGate.pending()).toHaveLength(1);
781
- freshGate.dispose();
782
- });
783
- // -- Source-level mutual invalidation tests --
784
- // These tests read the source .ts file directly to verify structural correctness
785
- // of mutual invalidation branches that are hard to test purely behaviorally.
786
- const sourceFilePath = join(dirname(fileURLToPath(import.meta.url)), "approval-gate.ts");
787
- it("resolveApproval approved path calls denialCache.delete (source-level)", () => {
788
- const source = readFileSync(sourceFilePath, "utf-8");
789
- // In the approved branch, denialCache.delete should be called for mutual invalidation
790
- expect(source).toContain("denialCache.delete(cacheKey)");
791
- });
792
- it("resolveApproval explicit deny path calls approvalCache.delete (source-level)", () => {
793
- const source = readFileSync(sourceFilePath, "utf-8");
794
- // In the explicit deny branch, approvalCache.delete should be called for mutual invalidation
795
- expect(source).toContain("approvalCache.delete(cacheKey)");
796
- });
797
- it("resolveApproval timeout path calls approvalCache.delete (source-level)", () => {
798
- const source = readFileSync(sourceFilePath, "utf-8");
799
- // The timeout branch should also delete from approvalCache
800
- // Verify by checking that system:timeout branch contains approvalCache.delete
801
- const timeoutSection = source.split('"system:timeout"')[1];
802
- expect(timeoutSection).toBeDefined();
803
- expect(timeoutSection.substring(0, 200)).toContain("approvalCache.delete");
804
- });
805
- it("resolveApproval shutdown path does NOT call approvalCache.delete (source-level)", () => {
806
- const source = readFileSync(sourceFilePath, "utf-8");
807
- // The shutdown branch should NOT touch approvalCache
808
- // Extract the shutdown block between system:shutdown and system:timeout in resolveApproval
809
- const shutdownStart = source.indexOf('"system:shutdown"');
810
- const timeoutStart = source.indexOf('"system:timeout"');
811
- expect(shutdownStart).toBeGreaterThan(-1);
812
- expect(timeoutStart).toBeGreaterThan(-1);
813
- const shutdownBlock = source.substring(shutdownStart, timeoutStart);
814
- expect(shutdownBlock).not.toContain("approvalCache.delete");
815
- });
816
- it("cached approval does NOT re-populate cache (no infinite TTL extension) (source-level)", () => {
817
- const source = readFileSync(sourceFilePath, "utf-8");
818
- // Verify the guard: approvedBy !== "system:cached-approval" before populating approval cache
819
- expect(source).toContain('!== "system:cached-approval"');
820
- });
821
- });
822
- // ---------------------------------------------------------------------------
823
- // 15. Approval cache serialization and logging (Phase 437)
824
- // ---------------------------------------------------------------------------
825
- describe("approval cache serialization and logging", () => {
826
- const APPROVAL_CACHE_TTL = 15_000;
827
- let gateWithApprovalCache;
828
- beforeEach(() => {
829
- gateWithApprovalCache = createApprovalGate({
830
- eventBus,
831
- getTimeoutMs: () => DEFAULT_TIMEOUT_MS,
832
- getDenialCacheTtlMs: () => 30_000,
833
- getBatchApprovalTtlMs: () => APPROVAL_CACHE_TTL,
834
- });
835
- });
836
- afterEach(() => {
837
- gateWithApprovalCache.dispose();
838
- });
839
- // -- Serialization tests (SERIAL-01, TEST-02) --
840
- it("serializeApprovalCache returns empty array when no cached entries", () => {
841
- const entries = gateWithApprovalCache.serializeApprovalCache();
842
- expect(entries).toEqual([]);
843
- });
844
- it("serializeApprovalCache returns populated entries after approval", async () => {
845
- const promise = gateWithApprovalCache.requestApproval(makeRequest());
846
- const [pending] = gateWithApprovalCache.pending();
847
- gateWithApprovalCache.resolveApproval(pending.requestId, true, "operator");
848
- await promise;
849
- const entries = gateWithApprovalCache.serializeApprovalCache();
850
- expect(entries).toHaveLength(1);
851
- expect(entries[0].cacheKey).toBe("default:user1:discord::agents.restart");
852
- expect(entries[0].resolution.approved).toBe(true);
853
- expect(entries[0].resolution.approvedBy).toBe("operator");
854
- expect(entries[0].expiresAt).toBeGreaterThan(Date.now());
855
- });
856
- it("serializeApprovalCache skips expired entries", async () => {
857
- const promise = gateWithApprovalCache.requestApproval(makeRequest());
858
- const [pending] = gateWithApprovalCache.pending();
859
- gateWithApprovalCache.resolveApproval(pending.requestId, true, "operator");
860
- await promise;
861
- // Advance time past TTL
862
- vi.advanceTimersByTime(APPROVAL_CACHE_TTL + 1);
863
- const entries = gateWithApprovalCache.serializeApprovalCache();
864
- expect(entries).toEqual([]);
865
- });
866
- it("restoreApprovalCache restores valid entries and auto-approves subsequent requests", async () => {
867
- const entry = {
868
- cacheKey: "default:user1:discord::agents.restart",
869
- resolution: {
870
- requestId: "00000000-0000-0000-0000-000000000099",
871
- approved: true,
872
- approvedBy: "operator",
873
- resolvedAt: Date.now(),
874
- },
875
- expiresAt: Date.now() + 15_000,
876
- };
877
- const restored = gateWithApprovalCache.restoreApprovalCache([entry]);
878
- expect(restored).toBe(1);
879
- // Submit same request -- should resolve from cache
880
- const promise = gateWithApprovalCache.requestApproval(makeRequest());
881
- const result = await promise;
882
- expect(result.approved).toBe(true);
883
- expect(result.approvedBy).toBe("system:cached-approval");
884
- expect(gateWithApprovalCache.pending()).toHaveLength(0);
885
- });
886
- it("restoreApprovalCache skips entries with expired expiresAt", () => {
887
- const entry = {
888
- cacheKey: "default:user1:discord::agents.restart",
889
- resolution: {
890
- requestId: "00000000-0000-0000-0000-000000000098",
891
- approved: true,
892
- approvedBy: "operator",
893
- resolvedAt: Date.now() - 20_000,
894
- },
895
- expiresAt: Date.now() - 1000,
896
- };
897
- const restored = gateWithApprovalCache.restoreApprovalCache([entry]);
898
- expect(restored).toBe(0);
899
- // Submit request -- should go to pending (not cached)
900
- gateWithApprovalCache.requestApproval(makeRequest());
901
- expect(gateWithApprovalCache.pending()).toHaveLength(1);
902
- });
903
- it("serialize then restore round-trip preserves cache behavior", async () => {
904
- // Create and approve on first gate
905
- const promise1 = gateWithApprovalCache.requestApproval(makeRequest());
906
- const [pending1] = gateWithApprovalCache.pending();
907
- gateWithApprovalCache.resolveApproval(pending1.requestId, true, "operator");
908
- await promise1;
909
- // Serialize from first gate
910
- const entries = gateWithApprovalCache.serializeApprovalCache();
911
- expect(entries).toHaveLength(1);
912
- // Create a NEW gate and restore
913
- const newGate = createApprovalGate({
914
- eventBus,
915
- getTimeoutMs: () => DEFAULT_TIMEOUT_MS,
916
- getDenialCacheTtlMs: () => 30_000,
917
- getBatchApprovalTtlMs: () => APPROVAL_CACHE_TTL,
918
- });
919
- const restored = newGate.restoreApprovalCache(entries);
920
- expect(restored).toBe(1);
921
- // Submit same request on new gate -- should auto-approve from restored cache
922
- const promise2 = newGate.requestApproval(makeRequest());
923
- const result2 = await promise2;
924
- expect(result2.approved).toBe(true);
925
- expect(result2.approvedBy).toBe("system:cached-approval");
926
- newGate.dispose();
927
- });
928
- // -- Shutdown ordering test (SERIAL-02) --
929
- it("dispose clears approval cache -- serialization must happen before dispose", async () => {
930
- const promise = gateWithApprovalCache.requestApproval(makeRequest());
931
- const [pending] = gateWithApprovalCache.pending();
932
- gateWithApprovalCache.resolveApproval(pending.requestId, true, "operator");
933
- await promise;
934
- // Before dispose: serialization returns entries
935
- const before = gateWithApprovalCache.serializeApprovalCache();
936
- expect(before).toHaveLength(1);
937
- // After dispose: serialization returns empty
938
- gateWithApprovalCache.dispose();
939
- const after = gateWithApprovalCache.serializeApprovalCache();
940
- expect(after).toEqual([]);
941
- });
942
- // -- Cache logging tests (OBS-03) --
943
- it("logger.debug called on approval cache hit with cacheKey and ttlRemainingMs", async () => {
944
- const debugFn = vi.fn();
945
- const gateWithLogger = createApprovalGate({
946
- eventBus,
947
- getTimeoutMs: () => DEFAULT_TIMEOUT_MS,
948
- getDenialCacheTtlMs: () => 30_000,
949
- getBatchApprovalTtlMs: () => APPROVAL_CACHE_TTL,
950
- logger: { debug: debugFn },
951
- });
952
- // First: approve
953
- const promise1 = gateWithLogger.requestApproval(makeRequest());
954
- const [pending1] = gateWithLogger.pending();
955
- gateWithLogger.resolveApproval(pending1.requestId, true, "operator");
956
- await promise1;
957
- // Second: cache hit
958
- const promise2 = gateWithLogger.requestApproval(makeRequest());
959
- await promise2;
960
- expect(debugFn).toHaveBeenCalledWith(expect.objectContaining({
961
- cacheKey: "default:user1:discord::agents.restart",
962
- action: "agents.restart",
963
- }), "Approval cache hit");
964
- gateWithLogger.dispose();
965
- });
966
- it("expired approval cache entry does not produce cache hit", async () => {
967
- const debugFn = vi.fn();
968
- const gateWithLogger = createApprovalGate({
969
- eventBus,
970
- getTimeoutMs: () => DEFAULT_TIMEOUT_MS,
971
- getDenialCacheTtlMs: () => 30_000,
972
- getBatchApprovalTtlMs: () => APPROVAL_CACHE_TTL,
973
- logger: { debug: debugFn },
974
- });
975
- // First: approve
976
- const promise1 = gateWithLogger.requestApproval(makeRequest());
977
- const [pending1] = gateWithLogger.pending();
978
- gateWithLogger.resolveApproval(pending1.requestId, true, "operator");
979
- await promise1;
980
- // Advance past TTL
981
- vi.advanceTimersByTime(APPROVAL_CACHE_TTL + 1);
982
- // Submit same request -- TTLCache auto-evicts expired entry, so no cache hit
983
- gateWithLogger.requestApproval(makeRequest());
984
- // No "Approval cache hit" debug log should be emitted for the second request
985
- const cacheHitCalls = debugFn.mock.calls.filter((call) => call[1] === "Approval cache hit");
986
- expect(cacheHitCalls).toHaveLength(0);
987
- // The request should be pending (not auto-resolved from cache)
988
- expect(gateWithLogger.pending()).toHaveLength(1);
989
- gateWithLogger.dispose();
990
- });
991
- it("no logger crash when logger dep is omitted", async () => {
992
- // This gate uses the beforeEach gate which has NO logger dep
993
- const promise1 = gateWithApprovalCache.requestApproval(makeRequest());
994
- const [pending1] = gateWithApprovalCache.pending();
995
- gateWithApprovalCache.resolveApproval(pending1.requestId, true, "operator");
996
- await promise1;
997
- // Second request: cache hit -- should not throw even without logger
998
- const promise2 = gateWithApprovalCache.requestApproval(makeRequest());
999
- const result2 = await promise2;
1000
- expect(result2.approved).toBe(true);
1001
- expect(result2.approvedBy).toBe("system:cached-approval");
1002
- });
1003
- });