@pikku/core 0.12.78 → 0.12.80

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 (341) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/dist/dev/hot-reload.js +1 -6
  3. package/dist/ecosystem.d.ts +27 -0
  4. package/dist/ecosystem.js +26 -0
  5. package/dist/errors/error-handler.js +4 -2
  6. package/dist/function/function-runner.js +20 -42
  7. package/dist/function/functions.types.d.ts +9 -5
  8. package/dist/index.d.ts +4 -9
  9. package/dist/index.js +3 -8
  10. package/dist/middleware/auth-cookie.d.ts +0 -4
  11. package/dist/middleware/auth-cookie.js +38 -2
  12. package/dist/middleware/cors.js +1 -0
  13. package/dist/middleware/index.d.ts +1 -1
  14. package/dist/middleware/index.js +1 -1
  15. package/dist/middleware/remote-auth.js +14 -3
  16. package/dist/permissions.d.ts +8 -10
  17. package/dist/permissions.js +0 -11
  18. package/dist/pikku-state.js +12 -5
  19. package/dist/schema.js +35 -1
  20. package/dist/services/ai-run-state-service.d.ts +7 -1
  21. package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
  22. package/dist/services/in-memory-ai-run-state-service.js +2 -1
  23. package/dist/services/in-memory-workflow-service.d.ts +10 -0
  24. package/dist/services/in-memory-workflow-service.js +25 -0
  25. package/dist/services/local-content-request-handler.d.ts +21 -0
  26. package/dist/services/local-content-request-handler.js +72 -53
  27. package/dist/services/local-content.d.ts +6 -0
  28. package/dist/services/local-content.js +14 -1
  29. package/dist/services/workflow-service.d.ts +4 -2
  30. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  31. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  32. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  33. package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
  34. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  35. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  36. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  37. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  38. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  39. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  40. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  41. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  42. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  43. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  44. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  45. package/dist/testing/service-tests/session-store-tests.js +43 -0
  46. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  47. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  48. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  49. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  50. package/dist/testing/service-tests.d.ts +6 -0
  51. package/dist/testing/service-tests.js +26 -791
  52. package/dist/types/core.types.d.ts +11 -1
  53. package/dist/types/state.types.d.ts +1 -1
  54. package/dist/utils/node-host-resolver.d.ts +12 -0
  55. package/dist/utils/node-host-resolver.js +16 -0
  56. package/dist/utils/safe-fetch.d.ts +18 -0
  57. package/dist/utils/safe-fetch.js +167 -29
  58. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  59. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  60. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  61. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  62. package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
  63. package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
  64. package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
  65. package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
  66. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
  67. package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
  68. package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
  69. package/dist/wirings/ai-agent/voice-input.js +1 -6
  70. package/dist/wirings/ai-agent/voice-output.js +2 -12
  71. package/dist/wirings/channel/channel-common.js +1 -0
  72. package/dist/wirings/channel/channel-handler.js +3 -5
  73. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  74. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  75. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  76. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  77. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  78. package/dist/wirings/channel/channel-runner.js +16 -8
  79. package/dist/wirings/channel/channel.types.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  82. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  83. package/dist/wirings/cli/cli-runner.js +4 -2
  84. package/dist/wirings/cli/cli.types.d.ts +0 -8
  85. package/dist/wirings/cli/command-parser.js +13 -0
  86. package/dist/wirings/gateway/gateway-runner.js +41 -5
  87. package/dist/wirings/http/http-routes.js +2 -0
  88. package/dist/wirings/http/http-runner.d.ts +0 -10
  89. package/dist/wirings/http/http-runner.js +6 -13
  90. package/dist/wirings/http/http.types.d.ts +0 -10
  91. package/dist/wirings/http/index.d.ts +1 -1
  92. package/dist/wirings/http/index.js +1 -1
  93. package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
  94. package/dist/wirings/mcp/mcp-runner.js +0 -6
  95. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  96. package/dist/wirings/rpc/rpc-runner.js +53 -86
  97. package/dist/wirings/rpc/rpc-types.d.ts +3 -0
  98. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  99. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  100. package/dist/wirings/trigger/trigger-runner.js +1 -0
  101. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  102. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  103. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  104. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
  105. package/dist/wirings/workflow/feature.js +7 -4
  106. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  107. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  108. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  109. package/dist/wirings/workflow/index.d.ts +7 -2
  110. package/dist/wirings/workflow/index.js +5 -1
  111. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  112. package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +44 -111
  114. package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
  115. package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
  116. package/dist/wirings/workflow/workflow-approval.js +114 -0
  117. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  118. package/dist/wirings/workflow/workflow-constants.js +35 -0
  119. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  120. package/dist/wirings/workflow/workflow-errors.js +112 -0
  121. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  122. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  123. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  124. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  125. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  126. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  127. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  128. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  129. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  130. package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
  131. package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
  132. package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
  133. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  134. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  135. package/dist/wirings/workflow/workflow.types.d.ts +7 -0
  136. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  137. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  138. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  139. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  140. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  141. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  142. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  143. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  144. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  145. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  146. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  147. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  148. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  149. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  150. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  151. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  152. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  153. package/knowledge/decisions/internals/index.md +38 -26
  154. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  155. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  156. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  157. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  158. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  159. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  160. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  161. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  162. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  163. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  164. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  165. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  166. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  167. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  168. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  169. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  170. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  171. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  172. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  173. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  174. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  175. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  176. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  177. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
  178. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  179. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  180. package/knowledge/decisions/security/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
  181. package/knowledge/decisions/security/index.md +7 -0
  182. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  183. package/knowledge/questions/index.md +2 -1
  184. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  185. package/package.json +14 -3
  186. package/scripts/generate-api-report.d.mts +1 -0
  187. package/scripts/generate-api-report.mjs +89 -0
  188. package/scripts/generate-api-report.mts +218 -0
  189. package/src/api-report.test.ts +69 -0
  190. package/src/column-form.ts +1 -4
  191. package/src/crypto-utils.test.ts +15 -2
  192. package/src/dev/hot-reload.ts +2 -7
  193. package/src/ecosystem.ts +40 -0
  194. package/src/errors/error-handler.ts +5 -3
  195. package/src/errors/error.test.ts +4 -1
  196. package/src/function/function-runner.test.ts +1 -1
  197. package/src/function/function-runner.ts +28 -39
  198. package/src/function/functions.types.ts +15 -9
  199. package/src/handle-error.ts +1 -1
  200. package/src/index.ts +3 -22
  201. package/src/middleware/auth-cookie.test.ts +50 -0
  202. package/src/middleware/auth-cookie.ts +38 -2
  203. package/src/middleware/cors.ts +2 -1
  204. package/src/middleware/index.ts +1 -1
  205. package/src/middleware/remote-auth.test.ts +43 -0
  206. package/src/middleware/remote-auth.ts +17 -3
  207. package/src/middleware-runner.ts +4 -2
  208. package/src/no-any-casts.test.ts +57 -0
  209. package/src/permissions.test.ts +3 -1
  210. package/src/permissions.ts +22 -24
  211. package/src/pikku-state.ts +17 -6
  212. package/src/public-surface.json +578 -0
  213. package/src/public-surface.json.README +25 -0
  214. package/src/public-surface.test.ts +105 -0
  215. package/src/removed-legacy-exports.test.ts +62 -0
  216. package/src/schema.test.ts +78 -0
  217. package/src/schema.ts +36 -1
  218. package/src/services/ai-run-state-service.ts +7 -1
  219. package/src/services/audit-service.ts +2 -2
  220. package/src/services/in-memory-ai-run-state-service.ts +3 -2
  221. package/src/services/in-memory-workflow-service.ts +25 -0
  222. package/src/services/index.ts +1 -4
  223. package/src/services/local-content-request-handler.test.ts +43 -5
  224. package/src/services/local-content-request-handler.ts +103 -74
  225. package/src/services/local-content.ts +15 -1
  226. package/src/services/local-email-service.ts +5 -1
  227. package/src/services/system-role-guard.test.ts +4 -1
  228. package/src/services/workflow-service.ts +9 -2
  229. package/src/side-effects-are-declared.test.ts +84 -0
  230. package/src/source-files-stay-composable.test.ts +41 -0
  231. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  232. package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
  233. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  234. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  235. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  236. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  237. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  238. package/src/testing/service-tests/session-store-tests.ts +62 -0
  239. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  240. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  241. package/src/testing/service-tests.ts +31 -1111
  242. package/src/types/core.types.ts +15 -1
  243. package/src/types/state.types.ts +1 -1
  244. package/src/utils/node-host-resolver.ts +20 -0
  245. package/src/utils/safe-fetch.test.ts +143 -1
  246. package/src/utils/safe-fetch.ts +200 -25
  247. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  248. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  249. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  250. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  251. package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
  252. package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
  253. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  254. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  255. package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
  256. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  257. package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
  258. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  259. package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
  260. package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
  261. package/src/wirings/ai-agent/voice-input.ts +1 -6
  262. package/src/wirings/ai-agent/voice-output.ts +2 -12
  263. package/src/wirings/channel/channel-common.ts +5 -2
  264. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  265. package/src/wirings/channel/channel-handler.ts +7 -7
  266. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  267. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  268. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  269. package/src/wirings/channel/channel-runner.ts +33 -20
  270. package/src/wirings/channel/channel.types.ts +2 -0
  271. package/src/wirings/channel/local/local-channel-runner.ts +1 -1
  272. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  273. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  274. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  275. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  276. package/src/wirings/cli/cli-runner.ts +7 -4
  277. package/src/wirings/cli/cli.types.ts +0 -39
  278. package/src/wirings/cli/command-parser.test.ts +19 -0
  279. package/src/wirings/cli/command-parser.ts +17 -1
  280. package/src/wirings/gateway/gateway-authorization.test.ts +131 -0
  281. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  282. package/src/wirings/gateway/gateway-runner.ts +62 -22
  283. package/src/wirings/http/http-routes.ts +4 -1
  284. package/src/wirings/http/http-runner.ts +11 -26
  285. package/src/wirings/http/http.types.ts +0 -15
  286. package/src/wirings/http/index.ts +1 -7
  287. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  288. package/src/wirings/http/web-request.ts +1 -1
  289. package/src/wirings/mcp/mcp-runner.ts +2 -16
  290. package/src/wirings/persona/persona-environments.test.ts +14 -3
  291. package/src/wirings/persona/persona.test.ts +13 -3
  292. package/src/wirings/persona/validate-personas.ts +5 -1
  293. package/src/wirings/queue/queue-runner.ts +1 -1
  294. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  295. package/src/wirings/rpc/rpc-runner.ts +58 -136
  296. package/src/wirings/rpc/rpc-types.ts +7 -0
  297. package/src/wirings/rpc/wire-addon.ts +3 -1
  298. package/src/wirings/secret/validate-secret-definitions.test.ts +69 -0
  299. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  300. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  301. package/src/wirings/trigger/trigger-runner.ts +7 -5
  302. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  303. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  304. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  305. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  306. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  307. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  308. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  309. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
  310. package/src/wirings/workflow/feature.ts +7 -4
  311. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  312. package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
  313. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  314. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  315. package/src/wirings/workflow/index.ts +12 -4
  316. package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
  317. package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
  318. package/src/wirings/workflow/pikku-workflow-service.ts +208 -630
  319. package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
  320. package/src/wirings/workflow/workflow-approval.ts +185 -0
  321. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  322. package/src/wirings/workflow/workflow-constants.ts +47 -0
  323. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  324. package/src/wirings/workflow/workflow-dispatch-relay.test.ts +128 -0
  325. package/src/wirings/workflow/workflow-errors.ts +128 -0
  326. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  327. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  328. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  329. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  330. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  331. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  332. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  333. package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
  334. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  335. package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
  336. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  337. package/src/wirings/workflow/workflow.types.ts +7 -0
  338. package/src/wirings-stay-decoupled.test.ts +123 -0
  339. package/tsconfig.json +1 -1
  340. package/tsconfig.tsbuildinfo +1 -1
  341. package/src/internal.ts +0 -10
@@ -1,6 +1,3 @@
1
- import { describe, test, before, after } from 'node:test'
2
- import assert from 'node:assert/strict'
3
-
4
1
  import type { ChannelStore } from '../wirings/channel/channel-store.js'
5
2
  import type { EventHubStore } from '../wirings/channel/eventhub-store.js'
6
3
  import type { PikkuWorkflowService } from '../wirings/workflow/pikku-workflow-service.js'
@@ -39,1135 +36,58 @@ export interface ServiceTestConfig {
39
36
  }
40
37
  }
41
38
 
39
+ import { defineChannelStoreTests } from './service-tests/channel-store-tests.js'
40
+ import { defineEventHubStoreTests } from './service-tests/event-hub-store-tests.js'
41
+ import { defineWorkflowServiceTests } from './service-tests/workflow-service-tests.js'
42
+ import { defineWorkflowRunServiceTests } from './service-tests/workflow-run-service-tests.js'
43
+ import { defineAiStorageServiceTests } from './service-tests/ai-storage-service-tests.js'
44
+ import { defineDeploymentServiceTests } from './service-tests/deployment-service-tests.js'
45
+ import { defineSecretServiceTests } from './service-tests/secret-service-tests.js'
46
+ import { defineCredentialServiceTests } from './service-tests/credential-service-tests.js'
47
+ import { defineAgentRunServiceTests } from './service-tests/agent-run-service-tests.js'
48
+ import { defineSessionStoreTests } from './service-tests/session-store-tests.js'
49
+
50
+ /**
51
+ * The shared conformance suite every storage backend runs.
52
+ *
53
+ * One module per service so a backend author can read the contract for the one
54
+ * they implement; this dispatches to whichever the caller supplied.
55
+ */
42
56
  export function defineServiceTests(config: ServiceTestConfig): void {
43
57
  const { name, services } = config
44
58
 
45
59
  if (services.channelStore) {
46
- const factory = services.channelStore
47
- describe(`ChannelStore [${name}]`, () => {
48
- let store: ChannelStore
49
-
50
- before(async () => {
51
- store = await factory()
52
- })
53
-
54
- test('addChannel and getChannel', async () => {
55
- await store.addChannel({
56
- channelId: 'ch-1',
57
- channelName: 'test-channel',
58
- openingData: { foo: 'bar' },
59
- })
60
-
61
- const result = await store.getChannel('ch-1')
62
- assert.equal(result.channelId, 'ch-1')
63
- assert.equal(result.channelName, 'test-channel')
64
- assert.deepEqual(result.openingData, { foo: 'bar' })
65
- assert.equal(result.pikkuUserId, undefined)
66
- })
67
-
68
- test('setPikkuUserId', async () => {
69
- await store.setPikkuUserId('ch-1', 'user-1')
70
-
71
- const result = await store.getChannel('ch-1')
72
- assert.equal(result.pikkuUserId, 'user-1')
73
- })
74
-
75
- test('setPikkuUserId to null', async () => {
76
- await store.setPikkuUserId('ch-1', null)
77
-
78
- const result = await store.getChannel('ch-1')
79
- assert.equal(result.pikkuUserId, undefined)
80
- })
81
-
82
- test('getChannel throws for missing channel', async () => {
83
- await assert.rejects(
84
- async () => {
85
- await store.getChannel('missing')
86
- },
87
- { message: 'Channel not found: missing' }
88
- )
89
- })
90
-
91
- test('removeChannels', async () => {
92
- await store.addChannel({
93
- channelId: 'ch-2',
94
- channelName: 'temp-channel',
95
- })
96
- await store.removeChannels(['ch-2'])
97
- await assert.rejects(async () => {
98
- await store.getChannel('ch-2')
99
- })
100
- })
101
-
102
- test('removeChannels with empty array is no-op', async () => {
103
- await store.removeChannels([])
104
- })
105
-
106
- test('session round-trip — set / get / clear', async () => {
107
- await store.addChannel({
108
- channelId: 'ch-state',
109
- channelName: 'test-channel',
110
- })
111
-
112
- const empty = await store.getState('ch-state')
113
- assert.equal(empty, undefined)
114
-
115
- const payload = { userId: 'u-7', meta: { foo: 1 } }
116
- await store.setState('ch-state', payload)
117
- const got = await store.getState('ch-state')
118
- assert.deepEqual(got, payload)
119
-
120
- const next = { userId: 'u-8' }
121
- await store.setState('ch-state', next)
122
- const got2 = await store.getState('ch-state')
123
- assert.deepEqual(got2, next)
124
-
125
- await store.clearState('ch-state')
126
- const after = await store.getState('ch-state')
127
- assert.equal(after, undefined)
128
-
129
- await store.removeChannels(['ch-state'])
130
- })
131
- })
60
+ defineChannelStoreTests(name, services.channelStore)
132
61
  }
133
-
134
62
  if (services.eventHubStore) {
135
- const factory = services.eventHubStore
136
- describe(`EventHubStore [${name}]`, () => {
137
- let store: EventHubStore<Record<string, any>>
138
-
139
- before(async () => {
140
- store = await factory()
141
- })
142
-
143
- test('subscribe and getChannelIdsForTopic', async () => {
144
- const result = await store.subscribe('topic-1', 'ch-1')
145
- assert.equal(result, true)
146
-
147
- const ids = await store.getChannelIdsForTopic('topic-1')
148
- assert.deepEqual(ids, ['ch-1'])
149
- })
150
-
151
- test('subscribe duplicate is idempotent', async () => {
152
- const result = await store.subscribe('topic-1', 'ch-1')
153
- assert.equal(result, true)
154
- })
155
-
156
- test('unsubscribe returns true when exists', async () => {
157
- const result = await store.unsubscribe('topic-1', 'ch-1')
158
- assert.equal(result, true)
159
- })
160
-
161
- test('unsubscribe returns false when not exists', async () => {
162
- const result = await store.unsubscribe('topic-1', 'ch-1')
163
- assert.equal(result, false)
164
- })
165
-
166
- test('getChannelIdsForTopic returns empty for unknown topic', async () => {
167
- const ids = await store.getChannelIdsForTopic('unknown')
168
- assert.deepEqual(ids, [])
169
- })
170
- })
63
+ defineEventHubStoreTests(name, services.eventHubStore)
171
64
  }
172
-
173
65
  if (services.workflowService) {
174
- const factory = services.workflowService
175
- const wire = { type: 'test' }
176
-
177
- describe(`WorkflowService [${name}]`, () => {
178
- let service: PikkuWorkflowService
179
-
180
- before(async () => {
181
- service = await factory()
182
- })
183
-
184
- test('createRun and getRun', async () => {
185
- const runId = await service.createRun(
186
- 'test-workflow',
187
- { key: 'value' },
188
- false,
189
- 'hash-1',
190
- wire
191
- )
192
-
193
- assert.ok(runId)
194
- const run = await service.getRun(runId)
195
- assert.ok(run)
196
- assert.equal(run.workflow, 'test-workflow')
197
- assert.equal(run.status, 'running')
198
- assert.deepEqual(run.input, { key: 'value' })
199
- assert.equal(run.graphHash, 'hash-1')
200
- })
201
-
202
- test('updateRunStatus', async () => {
203
- const runId = await service.createRun(
204
- 'status-workflow',
205
- {},
206
- false,
207
- 'hash-2',
208
- wire
209
- )
210
- await service.updateRunStatus(runId, 'completed', { result: 'done' })
211
-
212
- const run = await service.getRun(runId)
213
- assert.ok(run)
214
- assert.equal(run.status, 'completed')
215
- assert.deepEqual(run.output, { result: 'done' })
216
- })
217
-
218
- test('insertStepState and getStepState', async () => {
219
- const runId = await service.createRun(
220
- 'step-workflow',
221
- {},
222
- false,
223
- 'hash-3',
224
- wire
225
- )
226
-
227
- const step = await service.insertStepState(
228
- runId,
229
- 'step-1',
230
- 'myRpc',
231
- { data: 'test' },
232
- { retries: 3, retryDelay: '1000' }
233
- )
234
-
235
- assert.ok(step.stepId)
236
- assert.equal(step.status, 'pending')
237
- assert.equal(step.attemptCount, 1)
238
- assert.equal(step.retries, 3)
239
- assert.equal(step.retryDelay, '1000')
240
-
241
- const fetched = await service.getStepState(runId, 'step-1')
242
- assert.equal(fetched.stepId, step.stepId)
243
- assert.equal(fetched.status, 'pending')
244
- assert.equal(fetched.attemptCount, 1)
245
- })
246
-
247
- test('setStepRunning, setStepResult', async () => {
248
- const runId = await service.createRun(
249
- 'result-workflow',
250
- {},
251
- false,
252
- 'hash-4',
253
- wire
254
- )
255
- const step = await service.insertStepState(
256
- runId,
257
- 'result-step',
258
- 'myRpc',
259
- {}
260
- )
261
-
262
- await service.setStepRunning(step.stepId)
263
- let fetched = await service.getStepState(runId, 'result-step')
264
- assert.equal(fetched.status, 'running')
265
-
266
- await service.setStepResult(step.stepId, { answer: 42 })
267
- fetched = await service.getStepState(runId, 'result-step')
268
- assert.equal(fetched.status, 'succeeded')
269
- assert.deepEqual(fetched.result, { answer: 42 })
270
- })
271
-
272
- test('setStepError', async () => {
273
- const runId = await service.createRun(
274
- 'error-workflow',
275
- {},
276
- false,
277
- 'hash-5',
278
- wire
279
- )
280
- const step = await service.insertStepState(
281
- runId,
282
- 'error-step',
283
- 'myRpc',
284
- {}
285
- )
286
-
287
- await service.setStepRunning(step.stepId)
288
- await service.setStepError(step.stepId, new Error('boom'))
289
-
290
- const fetched = await service.getStepState(runId, 'error-step')
291
- assert.equal(fetched.status, 'failed')
292
- assert.equal(fetched.error?.message, 'boom')
293
- })
294
-
295
- test('createRetryAttempt', async () => {
296
- const runId = await service.createRun(
297
- 'retry-workflow',
298
- {},
299
- false,
300
- 'hash-6',
301
- wire
302
- )
303
- const step = await service.insertStepState(
304
- runId,
305
- 'retry-step',
306
- 'myRpc',
307
- {},
308
- { retries: 2 }
309
- )
310
-
311
- await service.setStepError(step.stepId, new Error('fail'))
312
- const retried = await service.createRetryAttempt(step.stepId, 'pending')
313
- assert.equal(retried.status, 'pending')
314
- assert.equal(retried.attemptCount, 2)
315
- assert.equal(retried.error, undefined)
316
- assert.equal(retried.result, undefined)
317
- })
318
-
319
- test('getRunStatus reports per-step duration and attempts', async () => {
320
- const runId = await service.createRun(
321
- 'status-timing-workflow',
322
- {},
323
- false,
324
- 'hash-timing',
325
- wire
326
- )
327
- const step = await service.insertStepState(
328
- runId,
329
- 'timed-step',
330
- 'myRpc',
331
- {}
332
- )
333
- await service.setStepRunning(step.stepId)
334
- await service.setStepResult(step.stepId, { ok: true })
335
-
336
- const status = await service.getRunStatus(runId)
337
- assert.ok(status)
338
- const timed = status.steps.find((s) => s.name === 'timed-step')
339
- assert.ok(timed, 'step appears in run status')
340
- // running→succeeded must stamp runningAt/succeededAt so a duration is
341
- // computable. Regression guard: the kysely store previously updated
342
- // only the status on transitions, leaving the timestamps null and
343
- // duration permanently undefined.
344
- assert.ok(
345
- timed.duration !== undefined,
346
- 'duration is computed from stamped transition timestamps'
347
- )
348
- assert.ok(timed.duration >= 0, 'duration is non-negative')
349
- assert.equal(timed.attempts, 1, 'single attempt')
350
- })
351
-
352
- test('getRunStatus surfaces retried attempt count', async () => {
353
- const runId = await service.createRun(
354
- 'status-retry-workflow',
355
- {},
356
- false,
357
- 'hash-retry-status',
358
- wire
359
- )
360
- const step = await service.insertStepState(
361
- runId,
362
- 'flaky-step',
363
- 'myRpc',
364
- {},
365
- { retries: 2 }
366
- )
367
- await service.setStepRunning(step.stepId)
368
- await service.setStepError(step.stepId, new Error('first try'))
369
- // Guarantee the retry's history row sorts after the failed attempt so
370
- // getRunStatus picks the latest attempt (it tie-breaks on timestamps).
371
- await new Promise((resolve) => setTimeout(resolve, 5))
372
- const retry = await service.createRetryAttempt(step.stepId, 'pending')
373
- await service.setStepRunning(retry.stepId)
374
- await service.setStepResult(retry.stepId, { ok: true })
375
-
376
- const status = await service.getRunStatus(runId)
377
- assert.ok(status)
378
- const flaky = status.steps.find((s) => s.name === 'flaky-step')
379
- assert.ok(flaky, 'retried step collapses to a single status entry')
380
- assert.equal(flaky.status, 'succeeded', 'latest attempt wins')
381
- assert.equal(flaky.attempts, 2, 'attempt count surfaces in status')
382
- })
383
-
384
- test('getNodeResults', async () => {
385
- const runId = await service.createRun(
386
- 'results-workflow',
387
- {},
388
- false,
389
- 'hash-8',
390
- wire
391
- )
392
- const step = await service.insertStepState(runId, 'node-a', 'rpc', {})
393
- await service.setStepResult(step.stepId, { out: 'hello' })
394
-
395
- const results = await service.getNodeResults(runId, ['node-a'])
396
- assert.deepEqual(results['node-a'], { out: 'hello' })
397
- })
398
-
399
- test('setBranchTaken and getCompletedGraphState', async () => {
400
- const runId = await service.createRun(
401
- 'branch-workflow',
402
- {},
403
- false,
404
- 'hash-9',
405
- wire
406
- )
407
- const step = await service.insertStepState(
408
- runId,
409
- 'branch-node',
410
- 'rpc',
411
- {}
412
- )
413
- await service.setStepResult(step.stepId, {})
414
- await service.setBranchTaken(step.stepId, 'left')
415
-
416
- const state = await service.getCompletedGraphState(runId)
417
- assert.ok(state.completedNodeIds.includes('branch-node'))
418
- assert.equal(state.branchKeys['branch-node'], 'left')
419
- })
420
-
421
- test('updateRunState and getRunState', async () => {
422
- const runId = await service.createRun(
423
- 'state-workflow',
424
- {},
425
- false,
426
- 'hash-10',
427
- wire
428
- )
429
-
430
- await service.updateRunState(runId, 'counter', 5)
431
- await service.updateRunState(runId, 'name', 'test')
432
-
433
- const state = await service.getRunState(runId)
434
- assert.equal(state.counter, 5)
435
- assert.equal(state.name, 'test')
436
- })
437
-
438
- test('upsertWorkflowVersion and getWorkflowVersion', async () => {
439
- await service.upsertWorkflowVersion(
440
- 'my-workflow',
441
- 'v1-hash',
442
- { nodes: ['a', 'b'] },
443
- 'dsl'
444
- )
445
-
446
- const version = await service.getWorkflowVersion(
447
- 'my-workflow',
448
- 'v1-hash'
449
- )
450
- assert.ok(version)
451
- assert.deepEqual(version.graph, { nodes: ['a', 'b'] })
452
- assert.equal(version.source, 'dsl')
453
- })
454
-
455
- test('upsertWorkflowVersion duplicate is no-op', async () => {
456
- await service.upsertWorkflowVersion(
457
- 'my-workflow',
458
- 'v1-hash',
459
- { nodes: ['changed'] },
460
- 'dsl'
461
- )
462
-
463
- const version = await service.getWorkflowVersion(
464
- 'my-workflow',
465
- 'v1-hash'
466
- )
467
- assert.ok(version)
468
- assert.deepEqual(version.graph, { nodes: ['a', 'b'] })
469
- })
470
-
471
- test('getWorkflowVersion returns null for missing', async () => {
472
- const version = await service.getWorkflowVersion('missing', 'missing')
473
- assert.equal(version, null)
474
- })
475
- })
66
+ defineWorkflowServiceTests(name, services.workflowService)
476
67
  }
477
-
478
68
  if (services.workflowRunService) {
479
- const factory = services.workflowRunService
480
- describe(`WorkflowRunService [${name}]`, () => {
481
- let runService: WorkflowRunService
482
-
483
- before(async () => {
484
- runService = await factory()
485
- })
486
-
487
- test('listRuns returns runs', async () => {
488
- const runs = await runService.listRuns()
489
- assert.ok(Array.isArray(runs))
490
- assert.ok(runs.length > 0)
491
- })
492
-
493
- test('listRuns with filter', async () => {
494
- const runs = await runService.listRuns({
495
- workflowName: 'test-workflow',
496
- })
497
- assert.ok(runs.every((r) => r.workflow === 'test-workflow'))
498
- })
499
-
500
- test('getDistinctWorkflowNames', async () => {
501
- const names = await runService.getDistinctWorkflowNames()
502
- assert.ok(Array.isArray(names))
503
- assert.ok(names.includes('test-workflow'))
504
- })
505
-
506
- test('deleteRun', async () => {
507
- const runs = await runService.listRuns({
508
- workflowName: 'status-workflow',
509
- })
510
- assert.ok(runs.length > 0)
511
-
512
- const deleted = await runService.deleteRun(runs[0]!.id)
513
- assert.equal(deleted, true)
514
-
515
- const afterDelete = await runService.getRun(runs[0]!.id)
516
- assert.equal(afterDelete, null)
517
- })
518
-
519
- test('deleteRun returns false for missing', async () => {
520
- const deleted = await runService.deleteRun('non-existent-id')
521
- assert.equal(deleted, false)
522
- })
523
- })
69
+ defineWorkflowRunServiceTests(name, services.workflowRunService)
524
70
  }
525
-
526
71
  if (services.aiStorageService) {
527
- const factory = services.aiStorageService
528
- describe(`AIStorageService [${name}]`, () => {
529
- let storage: AIStorageService & AIRunStateService
530
-
531
- before(async () => {
532
- storage = await factory()
533
- })
534
-
535
- test('createThread and getThread', async () => {
536
- const thread = await storage.createThread('resource-1', {
537
- title: 'Test Thread',
538
- metadata: { key: 'val' },
539
- })
540
-
541
- assert.ok(thread.id)
542
- assert.equal(thread.resourceId, 'resource-1')
543
- assert.equal(thread.title, 'Test Thread')
544
- assert.deepEqual(thread.metadata, { key: 'val' })
545
-
546
- const fetched = await storage.getThread(thread.id)
547
- assert.equal(fetched.id, thread.id)
548
- assert.equal(fetched.title, 'Test Thread')
549
- })
550
-
551
- test('getThreads', async () => {
552
- const threads = await storage.getThreads('resource-1')
553
- assert.ok(threads.length >= 1)
554
- assert.ok(threads.every((t) => t.resourceId === 'resource-1'))
555
- })
556
-
557
- test('getThread throws for missing', async () => {
558
- await assert.rejects(() => storage.getThread('missing-thread'))
559
- })
560
-
561
- test('saveMessages and getMessages', async () => {
562
- const thread = await storage.createThread('resource-2')
563
- const now = new Date()
564
-
565
- await storage.saveMessages(thread.id, [
566
- { id: 'msg-1', role: 'user', content: 'Hello', createdAt: now },
567
- {
568
- id: 'msg-2',
569
- role: 'assistant',
570
- content: 'Hi there',
571
- createdAt: new Date(now.getTime() + 1000),
572
- },
573
- ])
574
-
575
- const messages = await storage.getMessages(thread.id)
576
- assert.equal(messages.length, 2)
577
- assert.equal(messages[0]!.role, 'user')
578
- assert.equal(messages[0]!.content, 'Hello')
579
- assert.equal(messages[1]!.role, 'assistant')
580
- assert.equal(messages[1]!.content, 'Hi there')
581
- })
582
-
583
- test('saveMessages with tool calls and results', async () => {
584
- const thread = await storage.createThread('resource-3')
585
- const now = new Date()
586
-
587
- await storage.saveMessages(thread.id, [
588
- {
589
- id: 'msg-tc',
590
- role: 'assistant',
591
- content: 'Let me call a tool',
592
- toolCalls: [
593
- { id: 'tc-1', name: 'search', args: { query: 'test' } },
594
- ],
595
- createdAt: now,
596
- },
597
- ])
598
-
599
- await storage.saveMessages(thread.id, [
600
- {
601
- id: 'tool-results-msg-tc',
602
- role: 'tool',
603
- toolResults: [{ id: 'tc-1', name: 'search', result: 'found it' }],
604
- createdAt: new Date(now.getTime() + 1000),
605
- },
606
- ])
607
-
608
- const messages = await storage.getMessages(thread.id)
609
- assert.equal(messages.length, 2)
610
-
611
- const assistantMsg = messages[0]!
612
- assert.equal(assistantMsg.role, 'assistant')
613
- assert.ok(assistantMsg.toolCalls)
614
- assert.equal(assistantMsg.toolCalls[0]!.name, 'search')
615
-
616
- const toolMsg = messages[1]!
617
- assert.equal(toolMsg.role, 'tool')
618
- assert.ok(toolMsg.toolResults)
619
- assert.equal(toolMsg.toolResults[0]!.result, 'found it')
620
- })
621
-
622
- test('getMessages with lastN', async () => {
623
- const thread = await storage.createThread('resource-4')
624
- const base = Date.now()
625
-
626
- await storage.saveMessages(
627
- thread.id,
628
- Array.from({ length: 5 }, (_, i) => ({
629
- id: `bulk-msg-${i}`,
630
- role: 'user' as const,
631
- content: `Message ${i}`,
632
- createdAt: new Date(base + i * 1000),
633
- }))
634
- )
635
-
636
- const messages = await storage.getMessages(thread.id, { lastN: 2 })
637
- assert.equal(messages.length, 2)
638
- assert.equal(messages[0]!.content, 'Message 3')
639
- assert.equal(messages[1]!.content, 'Message 4')
640
- })
641
-
642
- test('working memory: save and get', async () => {
643
- await storage.saveWorkingMemory('res-1', 'resource', {
644
- key: 'value',
645
- })
646
-
647
- const mem = await storage.getWorkingMemory('res-1', 'resource')
648
- assert.deepEqual(mem, { key: 'value' })
649
- })
650
-
651
- test('working memory: upsert overwrites', async () => {
652
- await storage.saveWorkingMemory('res-1', 'resource', {
653
- key: 'updated',
654
- })
655
-
656
- const mem = await storage.getWorkingMemory('res-1', 'resource')
657
- assert.deepEqual(mem, { key: 'updated' })
658
- })
659
-
660
- test('working memory: returns null for missing', async () => {
661
- const mem = await storage.getWorkingMemory('missing', 'thread')
662
- assert.equal(mem, null)
663
- })
664
-
665
- test('createRun and getRun', async () => {
666
- const thread = await storage.createThread('resource-5')
667
- const now = new Date()
668
-
669
- const runId = await storage.createRun({
670
- agentName: 'test-agent',
671
- threadId: thread.id,
672
- resourceId: 'resource-5',
673
- status: 'running',
674
- usage: { inputTokens: 100, outputTokens: 50, model: 'test-model' },
675
- createdAt: now,
676
- updatedAt: now,
677
- })
678
-
679
- assert.ok(runId)
680
-
681
- const run = await storage.getRun(runId)
682
- assert.ok(run)
683
- assert.equal(run.agentName, 'test-agent')
684
- assert.equal(run.status, 'running')
685
- assert.equal(run.usage.inputTokens, 100)
686
- })
687
-
688
- test('updateRun', async () => {
689
- const thread = await storage.createThread('resource-6')
690
- const now = new Date()
691
-
692
- const runId = await storage.createRun({
693
- agentName: 'update-agent',
694
- threadId: thread.id,
695
- resourceId: 'resource-6',
696
- status: 'running',
697
- usage: { inputTokens: 0, outputTokens: 0, model: 'test' },
698
- createdAt: now,
699
- updatedAt: now,
700
- })
701
-
702
- await storage.updateRun(runId, {
703
- status: 'completed',
704
- usage: { inputTokens: 200, outputTokens: 100, model: 'test-v2' },
705
- })
706
-
707
- const run = await storage.getRun(runId)
708
- assert.ok(run)
709
- assert.equal(run.status, 'completed')
710
- assert.equal(run.usage.inputTokens, 200)
711
- assert.equal(run.usage.model, 'test-v2')
712
- })
713
-
714
- test('getRunsByThread', async () => {
715
- const thread = await storage.createThread('resource-7')
716
- const now = new Date()
717
-
718
- await storage.createRun({
719
- agentName: 'multi-agent',
720
- threadId: thread.id,
721
- resourceId: 'resource-7',
722
- status: 'completed',
723
- usage: { inputTokens: 10, outputTokens: 5, model: 'test' },
724
- createdAt: now,
725
- updatedAt: now,
726
- })
727
-
728
- const runs = await storage.getRunsByThread(thread.id)
729
- assert.ok(runs.length >= 1)
730
- assert.ok(runs.every((r) => r.threadId === thread.id))
731
- })
732
-
733
- test('resolveApproval', async () => {
734
- const thread = await storage.createThread('resource-8')
735
- const now = new Date()
736
-
737
- await storage.saveMessages(thread.id, [
738
- {
739
- id: 'approval-msg',
740
- role: 'assistant',
741
- toolCalls: [
742
- { id: 'approval-tc', name: 'dangerous-tool', args: {} },
743
- ],
744
- createdAt: now,
745
- },
746
- ])
747
-
748
- const runId = await storage.createRun({
749
- agentName: 'approval-agent',
750
- threadId: thread.id,
751
- resourceId: 'resource-8',
752
- status: 'suspended',
753
- suspendReason: 'approval',
754
- pendingApprovals: [
755
- {
756
- type: 'tool-call',
757
- toolCallId: 'approval-tc',
758
- toolName: 'dangerous-tool',
759
- args: {},
760
- },
761
- ],
762
- usage: { inputTokens: 0, outputTokens: 0, model: 'test' },
763
- createdAt: now,
764
- updatedAt: now,
765
- })
766
-
767
- let run = await storage.getRun(runId)
768
- assert.ok(run)
769
- assert.ok(run.pendingApprovals)
770
- assert.equal(run.pendingApprovals.length, 1)
771
-
772
- await storage.resolveApproval('approval-tc', 'approved')
773
-
774
- run = await storage.getRun(runId)
775
- assert.ok(run)
776
- assert.equal(run.pendingApprovals, undefined)
777
- })
778
-
779
- test('deleteThread cascades', async () => {
780
- const thread = await storage.createThread('resource-del')
781
- await storage.saveMessages(thread.id, [
782
- {
783
- id: 'del-msg',
784
- role: 'user',
785
- content: 'goodbye',
786
- createdAt: new Date(),
787
- },
788
- ])
789
-
790
- await storage.deleteThread(thread.id)
791
- await assert.rejects(() => storage.getThread(thread.id))
792
- })
793
- })
72
+ defineAiStorageServiceTests(name, services.aiStorageService)
794
73
  }
795
-
796
74
  if (services.deploymentService) {
797
- const factory = services.deploymentService
798
- describe(`DeploymentService [${name}]`, () => {
799
- let service: DeploymentService & { stop(): Promise<void> }
800
-
801
- before(async () => {
802
- service = await factory()
803
- })
804
-
805
- after(async () => {
806
- await service.stop()
807
- })
808
-
809
- test('start registers deployment', async () => {
810
- await service.start({
811
- deploymentId: 'deploy-1',
812
- endpoint: 'http://localhost:3000',
813
- functions: ['funcA', 'funcB'],
814
- })
815
- })
816
- })
75
+ defineDeploymentServiceTests(name, services.deploymentService)
817
76
  }
818
-
819
77
  if (services.secretService) {
820
- const factory = services.secretService
821
- const kek = 'test-key-encryption-key-32chars!'
822
-
823
- describe(`SecretService [${name}]`, () => {
824
- test('setSecret and getSecret', async () => {
825
- const service = await factory({ key: kek })
826
- await service.setSecret('api-key', {
827
- token: 'sk-123',
828
- endpoint: 'https://api.example.com',
829
- })
830
- const result = await service.getSecret<{
831
- token: string
832
- endpoint: string
833
- }>('api-key')
834
- assert.deepEqual(result.reveal(), {
835
- token: 'sk-123',
836
- endpoint: 'https://api.example.com',
837
- })
838
- })
839
-
840
- test('getSecret wraps the raw string rather than returning it', async () => {
841
- const service = await factory({ key: kek })
842
- await service.setSecret('string-secret', 'plain-value')
843
- const result = await service.getSecret('string-secret')
844
- assert.strictEqual(result.reveal(), 'plain-value')
845
- })
846
-
847
- test('hasSecret returns true/false', async () => {
848
- const service = await factory({ key: kek })
849
- assert.strictEqual(await service.hasSecret('api-key'), true)
850
- assert.strictEqual(await service.hasSecret('nonexistent'), false)
851
- })
852
-
853
- test('getSecret throws for missing key', async () => {
854
- const service = await factory({ key: kek })
855
- await assert.rejects(() => service.getSecret('nonexistent'), {
856
- message: 'Requested secret not found',
857
- })
858
- })
859
-
860
- test('setSecret upserts existing key', async () => {
861
- const service = await factory({ key: kek })
862
- await service.setSecret('upsert-key', { v: 1 })
863
- await service.setSecret('upsert-key', { v: 2 })
864
- const result = await service.getSecret<{ v: number }>('upsert-key')
865
- assert.deepEqual(result.reveal(), { v: 2 })
866
- })
867
-
868
- test('deleteSecret removes the key', async () => {
869
- const service = await factory({ key: kek })
870
- await service.setSecret('to-delete', 'bye')
871
- assert.strictEqual(await service.hasSecret('to-delete'), true)
872
- await service.deleteSecret('to-delete')
873
- assert.strictEqual(await service.hasSecret('to-delete'), false)
874
- })
875
-
876
- test('rotateKEK re-wraps all secrets', async () => {
877
- const newKEK = 'new-key-encryption-key-rotated!'
878
- const oldService = await factory({ key: kek })
879
- await oldService.setSecret('rotate-test', { important: 'data' })
880
-
881
- const rotatedService = await factory({
882
- key: newKEK,
883
- keyVersion: 2,
884
- previousKey: kek,
885
- })
886
-
887
- const before = await rotatedService.getSecret<{
888
- important: string
889
- }>('rotate-test')
890
- assert.deepEqual(before.reveal(), { important: 'data' })
891
-
892
- assert.ok(rotatedService.rotateKEK)
893
- const count = await rotatedService.rotateKEK!()
894
- assert.ok(count > 0)
895
-
896
- const newOnlyService = await factory({
897
- key: newKEK,
898
- keyVersion: 2,
899
- })
900
- const after = await newOnlyService.getSecret<{
901
- important: string
902
- }>('rotate-test')
903
- assert.deepEqual(after.reveal(), { important: 'data' })
904
- })
905
-
906
- test('rotateKEK throws without previousKey', async () => {
907
- const service = await factory({ key: kek })
908
- assert.ok(service.rotateKEK)
909
- await assert.rejects(() => service.rotateKEK!(), {
910
- message: 'No previousKey configured — nothing to rotate from',
911
- })
912
- })
913
- })
78
+ defineSecretServiceTests(name, services.secretService)
914
79
  }
915
-
916
80
  if (services.credentialService) {
917
- const factory = services.credentialService
918
- const kek = 'test-key-encryption-key-32chars!'
919
-
920
- describe(`CredentialService [${name}]`, () => {
921
- test('set and get (global)', async () => {
922
- const service = await factory({ key: kek })
923
- await service.set('stripe', { apiKey: 'sk-123' })
924
- const result = await service.get<{ apiKey: string }>('stripe')
925
- assert.deepEqual(result, { apiKey: 'sk-123' })
926
- })
927
-
928
- test('get returns null for missing', async () => {
929
- const service = await factory({ key: kek })
930
- const result = await service.get('nonexistent')
931
- assert.equal(result, null)
932
- })
933
-
934
- test('has returns true/false', async () => {
935
- const service = await factory({ key: kek })
936
- await service.set('exists', { key: 'val' })
937
- assert.strictEqual(await service.has('exists'), true)
938
- assert.strictEqual(await service.has('nope'), false)
939
- })
940
-
941
- test('set upserts existing credential', async () => {
942
- const service = await factory({ key: kek })
943
- await service.set('upsert', { v: 1 })
944
- await service.set('upsert', { v: 2 })
945
- const result = await service.get<{ v: number }>('upsert')
946
- assert.deepEqual(result, { v: 2 })
947
- })
948
-
949
- test('delete removes credential', async () => {
950
- const service = await factory({ key: kek })
951
- await service.set('to-delete', { bye: true })
952
- assert.strictEqual(await service.has('to-delete'), true)
953
- await service.delete('to-delete')
954
- assert.strictEqual(await service.has('to-delete'), false)
955
- })
956
-
957
- test('per-user isolation', async () => {
958
- const service = await factory({ key: kek })
959
- await service.set('token', { access: 'user1' }, 'user-1')
960
- await service.set('token', { access: 'user2' }, 'user-2')
961
-
962
- const u1 = await service.get<{ access: string }>('token', 'user-1')
963
- const u2 = await service.get<{ access: string }>('token', 'user-2')
964
- assert.deepEqual(u1, { access: 'user1' })
965
- assert.deepEqual(u2, { access: 'user2' })
966
- })
967
-
968
- test('per-user delete does not affect other users', async () => {
969
- const service = await factory({ key: kek })
970
- await service.set('cred', { val: 'a' }, 'user-a')
971
- await service.set('cred', { val: 'b' }, 'user-b')
972
-
973
- await service.delete('cred', 'user-a')
974
- assert.strictEqual(await service.has('cred', 'user-a'), false)
975
- assert.strictEqual(await service.has('cred', 'user-b'), true)
976
- })
977
-
978
- test('global and per-user are separate', async () => {
979
- const service = await factory({ key: kek })
980
- await service.set('shared', { scope: 'global' })
981
- await service.set('shared', { scope: 'user' }, 'user-1')
982
-
983
- const global = await service.get<{ scope: string }>('shared')
984
- const perUser = await service.get<{ scope: string }>('shared', 'user-1')
985
- assert.deepEqual(global, { scope: 'global' })
986
- assert.deepEqual(perUser, { scope: 'user' })
987
- })
988
-
989
- test('getAll returns all credentials for a user', async () => {
990
- const service = await factory({ key: kek })
991
- await service.set('cred-a', { a: 1 }, 'user-x')
992
- await service.set('cred-b', { b: 2 }, 'user-x')
993
- await service.set('cred-c', { c: 3 }, 'user-y')
994
-
995
- const all = await service.getAll('user-x')
996
- assert.deepEqual(all['cred-a'], { a: 1 })
997
- assert.deepEqual(all['cred-b'], { b: 2 })
998
- assert.strictEqual(all['cred-c'], undefined)
999
- })
1000
-
1001
- test('getAll returns empty for unknown user', async () => {
1002
- const service = await factory({ key: kek })
1003
- const all = await service.getAll('nobody')
1004
- assert.deepEqual(all, {})
1005
- })
1006
-
1007
- test('rotateKEK re-wraps all credentials', async () => {
1008
- const newKEK = 'new-key-encryption-key-rotated!'
1009
- const oldService = await factory({ key: kek })
1010
- await oldService.set('rotate-cred', { important: 'data' }, 'user-r')
1011
-
1012
- const rotatedService = await factory({
1013
- key: newKEK,
1014
- keyVersion: 2,
1015
- previousKey: kek,
1016
- })
1017
-
1018
- const before = await rotatedService.get<{ important: string }>(
1019
- 'rotate-cred',
1020
- 'user-r'
1021
- )
1022
- assert.deepEqual(before, { important: 'data' })
1023
-
1024
- assert.ok(rotatedService.rotateKEK)
1025
- const count = await rotatedService.rotateKEK!()
1026
- assert.ok(count > 0)
1027
-
1028
- const newOnlyService = await factory({
1029
- key: newKEK,
1030
- keyVersion: 2,
1031
- })
1032
- const after = await newOnlyService.get<{ important: string }>(
1033
- 'rotate-cred',
1034
- 'user-r'
1035
- )
1036
- assert.deepEqual(after, { important: 'data' })
1037
- })
1038
-
1039
- test('rotateKEK throws without previousKey', async () => {
1040
- const service = await factory({ key: kek })
1041
- assert.ok(service.rotateKEK)
1042
- await assert.rejects(() => service.rotateKEK!(), {
1043
- message: 'No previousKey configured — nothing to rotate from',
1044
- })
1045
- })
1046
- })
81
+ defineCredentialServiceTests(name, services.credentialService)
1047
82
  }
1048
-
1049
83
  if (services.agentRunService) {
1050
- const factory = services.agentRunService
1051
- describe(`AgentRunService [${name}]`, () => {
1052
- let agentService: AgentRunService
1053
-
1054
- before(async () => {
1055
- agentService = await factory()
1056
- })
1057
-
1058
- test('listThreads', async () => {
1059
- const threads = await agentService.listThreads()
1060
- assert.ok(Array.isArray(threads))
1061
- })
1062
-
1063
- if (services.aiStorageService) {
1064
- const storageFactory = services.aiStorageService
1065
-
1066
- // The `owners` constraint is what keeps the generated thread-management
1067
- // functions from leaking across tenants: a caller may only list threads
1068
- // owned by one of their session principals, matching the
1069
- // `principal:sub-partition` composition resolveOwnerResourceId writes.
1070
- test('listThreads scopes to the given owners, including sub-partitions', async () => {
1071
- const storage = await storageFactory()
1072
- await storage.createThread('owner-alice')
1073
- await storage.createThread('owner-alice:project-1')
1074
- await storage.createThread('owner-bob:secret')
1075
-
1076
- const threads = await agentService.listThreads({
1077
- owners: ['owner-alice'],
1078
- })
1079
- const ids = threads.map((t) => t.resourceId)
1080
-
1081
- assert.ok(ids.includes('owner-alice'))
1082
- assert.ok(ids.includes('owner-alice:project-1'))
1083
- assert.ok(
1084
- !ids.some((id) => id.startsWith('owner-bob')),
1085
- "another owner's threads must not be listed"
1086
- )
1087
- })
1088
-
1089
- test('listThreads with an owner does not match a lookalike prefix', async () => {
1090
- const storage = await storageFactory()
1091
- await storage.createThread('owner-al')
1092
- await storage.createThread('owner-alice-evil:p')
1093
-
1094
- const threads = await agentService.listThreads({
1095
- owners: ['owner-al'],
1096
- })
1097
- const ids = threads.map((t) => t.resourceId)
1098
-
1099
- assert.ok(ids.includes('owner-al'))
1100
- assert.ok(!ids.includes('owner-alice-evil:p'))
1101
- })
1102
-
1103
- test('listThreads with an empty owners list returns nothing', async () => {
1104
- const storage = await storageFactory()
1105
- await storage.createThread('owner-empty-check')
1106
-
1107
- const threads = await agentService.listThreads({ owners: [] })
1108
- assert.deepEqual(threads, [])
1109
- })
1110
- }
1111
-
1112
- test('getThread returns null for missing', async () => {
1113
- const thread = await agentService.getThread('missing-thread')
1114
- assert.equal(thread, null)
1115
- })
1116
-
1117
- test('getDistinctAgentNames', async () => {
1118
- const names = await agentService.getDistinctAgentNames()
1119
- assert.ok(Array.isArray(names))
1120
- })
1121
-
1122
- test('deleteThread returns false for missing', async () => {
1123
- const result = await agentService.deleteThread('missing-thread')
1124
- assert.equal(result, false)
1125
- })
1126
- })
84
+ defineAgentRunServiceTests(
85
+ name,
86
+ services.agentRunService,
87
+ services.aiStorageService
88
+ )
1127
89
  }
1128
-
1129
90
  if (services.sessionStore) {
1130
- const factory = services.sessionStore
1131
- describe(`SessionStore [${name}]`, () => {
1132
- let store: SessionStore
1133
-
1134
- before(async () => {
1135
- store = await factory()
1136
- })
1137
-
1138
- test('get returns undefined for unknown user', async () => {
1139
- const result = await store.get('unknown-user')
1140
- assert.equal(result, undefined)
1141
- })
1142
-
1143
- test('set and get round-trip', async () => {
1144
- const session = { userId: 'user-1', organizationId: 'org-1' } as any
1145
- await store.set('user-1', session)
1146
-
1147
- const result = await store.get('user-1')
1148
- assert.deepEqual(result, session)
1149
- })
1150
-
1151
- test('set overwrites previous session', async () => {
1152
- await store.set('user-2', { userId: 'user-2', role: 'admin' } as any)
1153
- await store.set('user-2', { userId: 'user-2', role: 'member' } as any)
1154
-
1155
- const result = await store.get('user-2')
1156
- assert.deepEqual(result, { userId: 'user-2', role: 'member' })
1157
- })
1158
-
1159
- test('clear removes session', async () => {
1160
- await store.set('user-3', { userId: 'user-3' } as any)
1161
- assert.ok(await store.get('user-3'))
1162
-
1163
- await store.clear('user-3')
1164
- const result = await store.get('user-3')
1165
- assert.equal(result, undefined)
1166
- })
1167
-
1168
- test('clear is no-op for unknown user', async () => {
1169
- await store.clear('nonexistent')
1170
- })
1171
- })
91
+ defineSessionStoreTests(name, services.sessionStore)
1172
92
  }
1173
93
  }