@pikku/core 0.12.79 → 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 (330) hide show
  1. package/CHANGELOG.md +161 -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.js +2 -0
  24. package/dist/services/local-content-request-handler.d.ts +21 -0
  25. package/dist/services/local-content-request-handler.js +72 -53
  26. package/dist/services/local-content.d.ts +6 -0
  27. package/dist/services/local-content.js +14 -1
  28. package/dist/services/workflow-service.d.ts +4 -2
  29. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  30. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  31. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  32. package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
  33. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  34. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  35. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  36. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  37. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  38. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  39. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  40. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  41. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  42. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  43. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  44. package/dist/testing/service-tests/session-store-tests.js +43 -0
  45. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  46. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  47. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  48. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  49. package/dist/testing/service-tests.d.ts +6 -0
  50. package/dist/testing/service-tests.js +26 -791
  51. package/dist/types/core.types.d.ts +11 -1
  52. package/dist/types/state.types.d.ts +1 -1
  53. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  54. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  55. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  56. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  57. package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
  58. package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
  59. package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
  60. package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
  61. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
  62. package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
  63. package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
  64. package/dist/wirings/ai-agent/voice-input.js +1 -6
  65. package/dist/wirings/ai-agent/voice-output.js +2 -12
  66. package/dist/wirings/channel/channel-common.js +1 -0
  67. package/dist/wirings/channel/channel-handler.js +3 -5
  68. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  69. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  70. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  71. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  72. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  73. package/dist/wirings/channel/channel-runner.js +16 -8
  74. package/dist/wirings/channel/channel.types.d.ts +2 -0
  75. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  76. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  77. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  78. package/dist/wirings/cli/cli-runner.js +4 -2
  79. package/dist/wirings/cli/cli.types.d.ts +0 -8
  80. package/dist/wirings/cli/command-parser.js +13 -0
  81. package/dist/wirings/gateway/gateway-runner.js +9 -2
  82. package/dist/wirings/http/http-routes.js +2 -0
  83. package/dist/wirings/http/http-runner.d.ts +0 -10
  84. package/dist/wirings/http/http-runner.js +6 -13
  85. package/dist/wirings/http/http.types.d.ts +0 -10
  86. package/dist/wirings/http/index.d.ts +1 -1
  87. package/dist/wirings/http/index.js +1 -1
  88. package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
  89. package/dist/wirings/mcp/mcp-runner.js +0 -6
  90. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  91. package/dist/wirings/rpc/rpc-runner.js +53 -86
  92. package/dist/wirings/rpc/rpc-types.d.ts +3 -0
  93. package/dist/wirings/secret/validate-secret-definitions.js +2 -2
  94. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  95. package/dist/wirings/trigger/trigger-runner.js +1 -0
  96. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  97. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  98. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  99. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
  100. package/dist/wirings/workflow/feature.js +7 -4
  101. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  102. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  103. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  104. package/dist/wirings/workflow/index.d.ts +7 -2
  105. package/dist/wirings/workflow/index.js +5 -1
  106. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  107. package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
  108. package/dist/wirings/workflow/pikku-workflow-service.d.ts +28 -147
  109. package/dist/wirings/workflow/pikku-workflow-service.js +71 -493
  110. package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
  111. package/dist/wirings/workflow/workflow-approval.js +114 -0
  112. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  113. package/dist/wirings/workflow/workflow-constants.js +35 -0
  114. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  115. package/dist/wirings/workflow/workflow-errors.js +112 -0
  116. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  117. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  118. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  119. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  120. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  121. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  122. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  123. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  124. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  125. package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
  126. package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
  127. package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
  128. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  129. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  130. package/dist/wirings/workflow/workflow.types.d.ts +7 -0
  131. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  132. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  133. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  134. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  135. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  136. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  137. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  138. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  139. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  140. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  141. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  142. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  143. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  144. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  145. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  146. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  147. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  148. package/knowledge/decisions/internals/index.md +38 -26
  149. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  150. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  151. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  152. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  153. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  154. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  155. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  156. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  157. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  158. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  159. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  160. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  161. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  162. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  163. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  164. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  165. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  166. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  167. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  168. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  169. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  170. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  171. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  172. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
  173. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  174. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  175. package/knowledge/decisions/security/index.md +7 -0
  176. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  177. package/knowledge/questions/index.md +2 -1
  178. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  179. package/package.json +13 -3
  180. package/scripts/generate-api-report.d.mts +1 -0
  181. package/scripts/generate-api-report.mjs +89 -0
  182. package/scripts/generate-api-report.mts +218 -0
  183. package/src/api-report.test.ts +69 -0
  184. package/src/column-form.ts +1 -4
  185. package/src/crypto-utils.test.ts +15 -2
  186. package/src/dev/hot-reload.ts +2 -7
  187. package/src/ecosystem.ts +40 -0
  188. package/src/errors/error-handler.ts +5 -3
  189. package/src/errors/error.test.ts +4 -1
  190. package/src/function/function-runner.test.ts +1 -1
  191. package/src/function/function-runner.ts +28 -39
  192. package/src/function/functions.types.ts +15 -9
  193. package/src/handle-error.ts +1 -1
  194. package/src/index.ts +3 -22
  195. package/src/middleware/auth-cookie.test.ts +50 -0
  196. package/src/middleware/auth-cookie.ts +38 -2
  197. package/src/middleware/cors.ts +2 -1
  198. package/src/middleware/index.ts +1 -1
  199. package/src/middleware/remote-auth.test.ts +43 -0
  200. package/src/middleware/remote-auth.ts +17 -3
  201. package/src/middleware-runner.ts +4 -2
  202. package/src/no-any-casts.test.ts +57 -0
  203. package/src/permissions.test.ts +3 -1
  204. package/src/permissions.ts +22 -24
  205. package/src/pikku-state.ts +17 -6
  206. package/src/public-surface.json +578 -0
  207. package/src/public-surface.json.README +25 -0
  208. package/src/public-surface.test.ts +105 -0
  209. package/src/removed-legacy-exports.test.ts +62 -0
  210. package/src/schema.test.ts +78 -0
  211. package/src/schema.ts +36 -1
  212. package/src/services/ai-run-state-service.ts +7 -1
  213. package/src/services/audit-service.ts +2 -2
  214. package/src/services/in-memory-ai-run-state-service.ts +3 -2
  215. package/src/services/in-memory-workflow-service.ts +2 -0
  216. package/src/services/index.ts +1 -4
  217. package/src/services/local-content-request-handler.test.ts +43 -5
  218. package/src/services/local-content-request-handler.ts +103 -74
  219. package/src/services/local-content.ts +15 -1
  220. package/src/services/local-email-service.ts +5 -1
  221. package/src/services/system-role-guard.test.ts +4 -1
  222. package/src/services/workflow-service.ts +9 -2
  223. package/src/side-effects-are-declared.test.ts +84 -0
  224. package/src/source-files-stay-composable.test.ts +41 -0
  225. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  226. package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
  227. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  228. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  229. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  230. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  231. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  232. package/src/testing/service-tests/session-store-tests.ts +62 -0
  233. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  234. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  235. package/src/testing/service-tests.ts +31 -1111
  236. package/src/types/core.types.ts +15 -1
  237. package/src/types/state.types.ts +1 -1
  238. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  239. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  240. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  241. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  242. package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
  243. package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
  244. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  245. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  246. package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
  247. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  248. package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
  249. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  250. package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
  251. package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
  252. package/src/wirings/ai-agent/voice-input.ts +1 -6
  253. package/src/wirings/ai-agent/voice-output.ts +2 -12
  254. package/src/wirings/channel/channel-common.ts +5 -2
  255. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  256. package/src/wirings/channel/channel-handler.ts +7 -7
  257. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  258. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  259. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  260. package/src/wirings/channel/channel-runner.ts +33 -20
  261. package/src/wirings/channel/channel.types.ts +2 -0
  262. package/src/wirings/channel/local/local-channel-runner.ts +1 -1
  263. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  264. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  265. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  266. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  267. package/src/wirings/cli/cli-runner.ts +7 -4
  268. package/src/wirings/cli/cli.types.ts +0 -39
  269. package/src/wirings/cli/command-parser.test.ts +19 -0
  270. package/src/wirings/cli/command-parser.ts +17 -1
  271. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  272. package/src/wirings/gateway/gateway-runner.ts +27 -19
  273. package/src/wirings/http/http-routes.ts +4 -1
  274. package/src/wirings/http/http-runner.ts +11 -26
  275. package/src/wirings/http/http.types.ts +0 -15
  276. package/src/wirings/http/index.ts +1 -7
  277. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  278. package/src/wirings/http/web-request.ts +1 -1
  279. package/src/wirings/mcp/mcp-runner.ts +2 -16
  280. package/src/wirings/persona/persona-environments.test.ts +14 -3
  281. package/src/wirings/persona/persona.test.ts +13 -3
  282. package/src/wirings/persona/validate-personas.ts +5 -1
  283. package/src/wirings/queue/queue-runner.ts +1 -1
  284. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  285. package/src/wirings/rpc/rpc-runner.ts +58 -136
  286. package/src/wirings/rpc/rpc-types.ts +7 -0
  287. package/src/wirings/rpc/wire-addon.ts +3 -1
  288. package/src/wirings/secret/validate-secret-definitions.test.ts +22 -0
  289. package/src/wirings/secret/validate-secret-definitions.ts +2 -2
  290. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  291. package/src/wirings/trigger/trigger-runner.ts +7 -5
  292. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  293. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  294. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  295. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  296. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  297. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  298. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  299. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
  300. package/src/wirings/workflow/feature.ts +7 -4
  301. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  302. package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
  303. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  304. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  305. package/src/wirings/workflow/index.ts +12 -4
  306. package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
  307. package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
  308. package/src/wirings/workflow/pikku-workflow-service.ts +190 -743
  309. package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
  310. package/src/wirings/workflow/workflow-approval.ts +185 -0
  311. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  312. package/src/wirings/workflow/workflow-constants.ts +47 -0
  313. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  314. package/src/wirings/workflow/workflow-errors.ts +128 -0
  315. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  316. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  317. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  318. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  319. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  320. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  321. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  322. package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
  323. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  324. package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
  325. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  326. package/src/wirings/workflow/workflow.types.ts +7 -0
  327. package/src/wirings-stay-decoupled.test.ts +123 -0
  328. package/tsconfig.json +1 -1
  329. package/tsconfig.tsbuildinfo +1 -1
  330. package/src/internal.ts +0 -10
@@ -48,6 +48,9 @@ export const signedContentPath = (urlOrPath: string): string => {
48
48
  }
49
49
  }
50
50
 
51
+ /** How long a presigned upload URL stays valid. Short, like an S3 PUT URL. */
52
+ const UPLOAD_URL_TTL_MS = 15 * 60 * 1000
53
+
51
54
  export class LocalContent implements ContentService {
52
55
  constructor(
53
56
  private config: LocalContentConfig,
@@ -137,13 +140,24 @@ export class LocalContent implements ContentService {
137
140
  })
138
141
  }
139
142
 
143
+ /**
144
+ * Presigned upload, mirroring the read side: the URL carries a short-lived,
145
+ * path-bound signature that {@link createLocalContentRequestHandler} verifies
146
+ * before writing. An unsigned upload URL cannot be verified, and the handler
147
+ * now refuses one — so this must sign, or every upload 403s.
148
+ */
140
149
  public async getUploadURL(args: GetUploadURLArgs): Promise<UploadURLResult> {
141
150
  void args.visibility
142
151
  const fullKey = this.joinKey(args.bucket, args.fileKey)
143
152
  this.logger.debug(`Going to upload with key: ${fullKey}`)
153
+ const uploadUrl = await this.signURL({
154
+ url: `${this.config.uploadUrlPrefix}/${fullKey}`,
155
+ dateLessThan: new Date(Date.now() + UPLOAD_URL_TTL_MS),
156
+ })
144
157
  return {
145
- uploadUrl: `${this.config.uploadUrlPrefix}/${fullKey}`,
158
+ uploadUrl,
146
159
  assetKey: fullKey,
160
+ uploadMethod: 'PUT',
147
161
  }
148
162
  }
149
163
 
@@ -1,4 +1,8 @@
1
- import type { EmailService, SendEmailInput, SendEmailResult } from './email-service.js'
1
+ import type {
2
+ EmailService,
3
+ SendEmailInput,
4
+ SendEmailResult,
5
+ } from './email-service.js'
2
6
 
3
7
  export class LocalEmailService implements EmailService {
4
8
  async send(input: SendEmailInput): Promise<SendEmailResult> {
@@ -11,7 +11,10 @@ import {
11
11
  SystemRoleShadowedError,
12
12
  } from '../errors/errors.js'
13
13
 
14
- const system = (...names: string[]) => (name: string) => names.includes(name)
14
+ const system =
15
+ (...names: string[]) =>
16
+ (name: string) =>
17
+ names.includes(name)
15
18
 
16
19
  describe('mutating a role', () => {
17
20
  test('a declared role refuses to be deleted or re-scoped', async () => {
@@ -1,4 +1,4 @@
1
- import type { SerializedError } from '../types/core.types.js'
1
+ import type { CoreUserSession, SerializedError } from '../types/core.types.js'
2
2
  import type {
3
3
  WorkflowRun,
4
4
  WorkflowPlannedStep,
@@ -47,8 +47,15 @@ export interface WorkflowService {
47
47
  /**
48
48
  * The payload is stored as given and validated on replay inside the workflow
49
49
  * body, so an invalid one re-closes the gate rather than failing the run.
50
+ *
51
+ * The session must belong to the run's owner — see `assertWorkflowRunOwner`.
50
52
  */
51
- approveStep(runId: string, reason: string, decision: unknown): Promise<void>
53
+ approveStep(
54
+ runId: string,
55
+ reason: string,
56
+ decision: unknown,
57
+ session?: CoreUserSession
58
+ ): Promise<void>
52
59
  startWorkflow<I>(
53
60
  name: string,
54
61
  input: I,
@@ -0,0 +1,84 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { readFileSync, readdirSync } from 'node:fs'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { join, dirname, relative } from 'node:path'
6
+
7
+ const srcRoot = dirname(fileURLToPath(import.meta.url))
8
+ const packageRoot = join(srcRoot, '..')
9
+
10
+ /**
11
+ * A statement that runs when the module is imported, rather than when
12
+ * something in it is called. Today every one is an `addError(...)` registering
13
+ * an error class in the runtime registry.
14
+ */
15
+ const TOP_LEVEL_CALL = /^[a-zA-Z_$][\w.$]*\(/
16
+
17
+ const collectSourceFiles = (directory: string): string[] =>
18
+ readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
19
+ const entryPath = join(directory, entry.name)
20
+ if (entry.isDirectory()) {
21
+ return entry.name === 'dist' ? [] : collectSourceFiles(entryPath)
22
+ }
23
+ return entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')
24
+ ? [entryPath]
25
+ : []
26
+ })
27
+
28
+ /** Source modules that do something merely by being imported. */
29
+ const modulesWithSideEffects = (): string[] =>
30
+ collectSourceFiles(srcRoot)
31
+ .filter((file) =>
32
+ readFileSync(file, 'utf-8')
33
+ .split('\n')
34
+ .some((line) => TOP_LEVEL_CALL.test(line))
35
+ )
36
+ .map((file) => `./dist/${relative(srcRoot, file).replace(/\.ts$/, '.js')}`)
37
+ .sort()
38
+
39
+ const declared = (): string[] => {
40
+ const pkg = JSON.parse(
41
+ readFileSync(join(packageRoot, 'package.json'), 'utf-8')
42
+ )
43
+ return [...(pkg.sideEffects ?? [])].sort()
44
+ }
45
+
46
+ /**
47
+ * `sideEffects` tells a bundler which modules it may not drop. Claiming
48
+ * `false` outright would be untrue — the error registry is built by
49
+ * `addError(...)` calls that run on import, and a bundler that dropped
50
+ * `errors/errors.js` would leave `getErrorResponse` unable to find any of
51
+ * them. Naming the modules exactly lets everything else be tree-shaken.
52
+ *
53
+ * knowledge: decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md
54
+ */
55
+ describe('every module with import-time side effects is declared', () => {
56
+ test('the allowlist matches the modules that actually have them', () => {
57
+ const actual = modulesWithSideEffects()
58
+ const listed = declared()
59
+
60
+ assert.deepEqual(
61
+ { missing: actual.filter((m) => !listed.includes(m)) },
62
+ { missing: [] },
63
+ 'a module runs code on import but is not in package.json sideEffects. ' +
64
+ 'A bundler may drop it, and whatever it registered will be absent at ' +
65
+ 'runtime.'
66
+ )
67
+
68
+ assert.deepEqual(
69
+ { stale: listed.filter((m) => !actual.includes(m)) },
70
+ { stale: [] },
71
+ 'package.json sideEffects names a module that no longer has any — ' +
72
+ 'remove it so bundlers can tree-shake it.'
73
+ )
74
+ })
75
+
76
+ test('the detector finds a known side effect', () => {
77
+ // Guards the test above: a detector matching nothing would pass whatever
78
+ // package.json claimed.
79
+ assert.ok(
80
+ modulesWithSideEffects().includes('./dist/errors/errors.js'),
81
+ 'errors/errors.js registers every error class at import and must match'
82
+ )
83
+ })
84
+ })
@@ -0,0 +1,41 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { readFileSync, readdirSync } from 'node:fs'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { join, dirname, relative } from 'node:path'
6
+
7
+ const srcRoot = dirname(fileURLToPath(import.meta.url))
8
+
9
+ /**
10
+ * The ceiling a single module may reach before it has to be composed out of
11
+ * smaller ones.
12
+ */
13
+ const MAX_LINES = 2000
14
+
15
+ const collectSourceFiles = (directory: string): string[] =>
16
+ readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
17
+ const entryPath = join(directory, entry.name)
18
+ if (entry.isDirectory()) {
19
+ return entry.name === 'dist' ? [] : collectSourceFiles(entryPath)
20
+ }
21
+ return entry.name.endsWith('.ts') ? [entryPath] : []
22
+ })
23
+
24
+ describe('source files stay composable', () => {
25
+ test(`no module in @pikku/core exceeds ${MAX_LINES} lines`, () => {
26
+ const offenders = collectSourceFiles(srcRoot)
27
+ .map((file) => ({
28
+ file: relative(srcRoot, file),
29
+ lines: readFileSync(file, 'utf-8').split('\n').length,
30
+ }))
31
+ .filter(({ lines }) => lines > MAX_LINES)
32
+ .sort((a, b) => b.lines - a.lines)
33
+
34
+ assert.deepEqual(
35
+ offenders,
36
+ [],
37
+ `modules over ${MAX_LINES} lines:\n` +
38
+ offenders.map(({ file, lines }) => ` ${lines} ${file}`).join('\n')
39
+ )
40
+ })
41
+ })
@@ -0,0 +1,98 @@
1
+ import { describe, test, before } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import type { AgentRunService } from '../../wirings/ai-agent/ai-agent.types.js'
5
+ import type { ServiceTestConfig } from '../service-tests.js'
6
+
7
+ /** Conformance suite for `agentRunService`. Runs only when a backend supplies one. */
8
+ export const defineAgentRunServiceTests = (
9
+ name: string,
10
+ agentRunService: NonNullable<
11
+ ServiceTestConfig['services']['agentRunService']
12
+ >,
13
+ /**
14
+ * Thread-ownership is a property of the two services together — the run
15
+ * service lists what the storage service owns — so that half of the suite
16
+ * only runs when a backend supplies both. Passed explicitly rather than
17
+ * closed over, so the dependency is visible in the signature.
18
+ */
19
+ aiStorageService?: ServiceTestConfig['services']['aiStorageService']
20
+ ): void => {
21
+ const factory = agentRunService
22
+ describe(`AgentRunService [${name}]`, () => {
23
+ let agentService: AgentRunService
24
+
25
+ before(async () => {
26
+ agentService = await factory()
27
+ })
28
+
29
+ test('listThreads', async () => {
30
+ const threads = await agentService.listThreads()
31
+ assert.ok(Array.isArray(threads))
32
+ })
33
+
34
+ if (aiStorageService) {
35
+ const storageFactory = aiStorageService
36
+
37
+ // The `owners` constraint is what keeps the generated thread-management
38
+ // functions from leaking across tenants: a caller may only list threads
39
+ // owned by one of their session principals, matching the
40
+ // `principal:sub-partition` composition resolveOwnerResourceId writes.
41
+ test('listThreads scopes to the given owners, including sub-partitions', async () => {
42
+ const storage = await storageFactory()
43
+ await storage.createThread('owner-alice')
44
+ await storage.createThread('owner-alice:project-1')
45
+ await storage.createThread('owner-bob:secret')
46
+
47
+ const threads = await agentService.listThreads({
48
+ owners: ['owner-alice'],
49
+ })
50
+ const ids = threads.map((t) => t.resourceId)
51
+
52
+ assert.ok(ids.includes('owner-alice'))
53
+ assert.ok(ids.includes('owner-alice:project-1'))
54
+ assert.ok(
55
+ !ids.some((id) => id.startsWith('owner-bob')),
56
+ "another owner's threads must not be listed"
57
+ )
58
+ })
59
+
60
+ test('listThreads with an owner does not match a lookalike prefix', async () => {
61
+ const storage = await storageFactory()
62
+ await storage.createThread('owner-al')
63
+ await storage.createThread('owner-alice-evil:p')
64
+
65
+ const threads = await agentService.listThreads({
66
+ owners: ['owner-al'],
67
+ })
68
+ const ids = threads.map((t) => t.resourceId)
69
+
70
+ assert.ok(ids.includes('owner-al'))
71
+ assert.ok(!ids.includes('owner-alice-evil:p'))
72
+ })
73
+
74
+ test('listThreads with an empty owners list returns nothing', async () => {
75
+ const storage = await storageFactory()
76
+ await storage.createThread('owner-empty-check')
77
+
78
+ const threads = await agentService.listThreads({ owners: [] })
79
+ assert.deepEqual(threads, [])
80
+ })
81
+ }
82
+
83
+ test('getThread returns null for missing', async () => {
84
+ const thread = await agentService.getThread('missing-thread')
85
+ assert.equal(thread, null)
86
+ })
87
+
88
+ test('getDistinctAgentNames', async () => {
89
+ const names = await agentService.getDistinctAgentNames()
90
+ assert.ok(Array.isArray(names))
91
+ })
92
+
93
+ test('deleteThread returns false for missing', async () => {
94
+ const result = await agentService.deleteThread('missing-thread')
95
+ assert.equal(result, false)
96
+ })
97
+ })
98
+ }
@@ -0,0 +1,286 @@
1
+ import { describe, test, before } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import type { AIStorageService } from '../../services/ai-storage-service.js'
5
+ import type { AIRunStateService } from '../../services/ai-run-state-service.js'
6
+ import type { ServiceTestConfig } from '../service-tests.js'
7
+
8
+ /** Conformance suite for `aiStorageService`. Runs only when a backend supplies one. */
9
+ export const defineAiStorageServiceTests = (
10
+ name: string,
11
+ aiStorageService: NonNullable<
12
+ ServiceTestConfig['services']['aiStorageService']
13
+ >
14
+ ): void => {
15
+ const factory = aiStorageService
16
+ describe(`AIStorageService [${name}]`, () => {
17
+ let storage: AIStorageService & AIRunStateService
18
+
19
+ before(async () => {
20
+ storage = await factory()
21
+ })
22
+
23
+ test('createThread and getThread', async () => {
24
+ const thread = await storage.createThread('resource-1', {
25
+ title: 'Test Thread',
26
+ metadata: { key: 'val' },
27
+ })
28
+
29
+ assert.ok(thread.id)
30
+ assert.equal(thread.resourceId, 'resource-1')
31
+ assert.equal(thread.title, 'Test Thread')
32
+ assert.deepEqual(thread.metadata, { key: 'val' })
33
+
34
+ const fetched = await storage.getThread(thread.id)
35
+ assert.equal(fetched.id, thread.id)
36
+ assert.equal(fetched.title, 'Test Thread')
37
+ })
38
+
39
+ test('getThreads', async () => {
40
+ const threads = await storage.getThreads('resource-1')
41
+ assert.ok(threads.length >= 1)
42
+ assert.ok(threads.every((t) => t.resourceId === 'resource-1'))
43
+ })
44
+
45
+ test('getThread throws for missing', async () => {
46
+ await assert.rejects(() => storage.getThread('missing-thread'))
47
+ })
48
+
49
+ test('saveMessages and getMessages', async () => {
50
+ const thread = await storage.createThread('resource-2')
51
+ const now = new Date()
52
+
53
+ await storage.saveMessages(thread.id, [
54
+ { id: 'msg-1', role: 'user', content: 'Hello', createdAt: now },
55
+ {
56
+ id: 'msg-2',
57
+ role: 'assistant',
58
+ content: 'Hi there',
59
+ createdAt: new Date(now.getTime() + 1000),
60
+ },
61
+ ])
62
+
63
+ const messages = await storage.getMessages(thread.id)
64
+ assert.equal(messages.length, 2)
65
+ assert.equal(messages[0]!.role, 'user')
66
+ assert.equal(messages[0]!.content, 'Hello')
67
+ assert.equal(messages[1]!.role, 'assistant')
68
+ assert.equal(messages[1]!.content, 'Hi there')
69
+ })
70
+
71
+ test('saveMessages with tool calls and results', async () => {
72
+ const thread = await storage.createThread('resource-3')
73
+ const now = new Date()
74
+
75
+ await storage.saveMessages(thread.id, [
76
+ {
77
+ id: 'msg-tc',
78
+ role: 'assistant',
79
+ content: 'Let me call a tool',
80
+ toolCalls: [{ id: 'tc-1', name: 'search', args: { query: 'test' } }],
81
+ createdAt: now,
82
+ },
83
+ ])
84
+
85
+ await storage.saveMessages(thread.id, [
86
+ {
87
+ id: 'tool-results-msg-tc',
88
+ role: 'tool',
89
+ toolResults: [{ id: 'tc-1', name: 'search', result: 'found it' }],
90
+ createdAt: new Date(now.getTime() + 1000),
91
+ },
92
+ ])
93
+
94
+ const messages = await storage.getMessages(thread.id)
95
+ assert.equal(messages.length, 2)
96
+
97
+ const assistantMsg = messages[0]!
98
+ assert.equal(assistantMsg.role, 'assistant')
99
+ assert.ok(assistantMsg.toolCalls)
100
+ assert.equal(assistantMsg.toolCalls[0]!.name, 'search')
101
+
102
+ const toolMsg = messages[1]!
103
+ assert.equal(toolMsg.role, 'tool')
104
+ assert.ok(toolMsg.toolResults)
105
+ assert.equal(toolMsg.toolResults[0]!.result, 'found it')
106
+ })
107
+
108
+ test('getMessages with lastN', async () => {
109
+ const thread = await storage.createThread('resource-4')
110
+ const base = Date.now()
111
+
112
+ await storage.saveMessages(
113
+ thread.id,
114
+ Array.from({ length: 5 }, (_, i) => ({
115
+ id: `bulk-msg-${i}`,
116
+ role: 'user' as const,
117
+ content: `Message ${i}`,
118
+ createdAt: new Date(base + i * 1000),
119
+ }))
120
+ )
121
+
122
+ const messages = await storage.getMessages(thread.id, { lastN: 2 })
123
+ assert.equal(messages.length, 2)
124
+ assert.equal(messages[0]!.content, 'Message 3')
125
+ assert.equal(messages[1]!.content, 'Message 4')
126
+ })
127
+
128
+ test('working memory: save and get', async () => {
129
+ await storage.saveWorkingMemory('res-1', 'resource', {
130
+ key: 'value',
131
+ })
132
+
133
+ const mem = await storage.getWorkingMemory('res-1', 'resource')
134
+ assert.deepEqual(mem, { key: 'value' })
135
+ })
136
+
137
+ test('working memory: upsert overwrites', async () => {
138
+ await storage.saveWorkingMemory('res-1', 'resource', {
139
+ key: 'updated',
140
+ })
141
+
142
+ const mem = await storage.getWorkingMemory('res-1', 'resource')
143
+ assert.deepEqual(mem, { key: 'updated' })
144
+ })
145
+
146
+ test('working memory: returns null for missing', async () => {
147
+ const mem = await storage.getWorkingMemory('missing', 'thread')
148
+ assert.equal(mem, null)
149
+ })
150
+
151
+ test('createRun and getRun', async () => {
152
+ const thread = await storage.createThread('resource-5')
153
+ const now = new Date()
154
+
155
+ const runId = await storage.createRun({
156
+ agentName: 'test-agent',
157
+ threadId: thread.id,
158
+ resourceId: 'resource-5',
159
+ status: 'running',
160
+ usage: { inputTokens: 100, outputTokens: 50, model: 'test-model' },
161
+ createdAt: now,
162
+ updatedAt: now,
163
+ })
164
+
165
+ assert.ok(runId)
166
+
167
+ const run = await storage.getRun(runId)
168
+ assert.ok(run)
169
+ assert.equal(run.agentName, 'test-agent')
170
+ assert.equal(run.status, 'running')
171
+ assert.equal(run.usage.inputTokens, 100)
172
+ })
173
+
174
+ test('updateRun', async () => {
175
+ const thread = await storage.createThread('resource-6')
176
+ const now = new Date()
177
+
178
+ const runId = await storage.createRun({
179
+ agentName: 'update-agent',
180
+ threadId: thread.id,
181
+ resourceId: 'resource-6',
182
+ status: 'running',
183
+ usage: { inputTokens: 0, outputTokens: 0, model: 'test' },
184
+ createdAt: now,
185
+ updatedAt: now,
186
+ })
187
+
188
+ await storage.updateRun(runId, {
189
+ status: 'completed',
190
+ usage: { inputTokens: 200, outputTokens: 100, model: 'test-v2' },
191
+ })
192
+
193
+ const run = await storage.getRun(runId)
194
+ assert.ok(run)
195
+ assert.equal(run.status, 'completed')
196
+ assert.equal(run.usage.inputTokens, 200)
197
+ assert.equal(run.usage.model, 'test-v2')
198
+ })
199
+
200
+ test('getRunsByThread', async () => {
201
+ const thread = await storage.createThread('resource-7')
202
+ const now = new Date()
203
+
204
+ await storage.createRun({
205
+ agentName: 'multi-agent',
206
+ threadId: thread.id,
207
+ resourceId: 'resource-7',
208
+ status: 'completed',
209
+ usage: { inputTokens: 10, outputTokens: 5, model: 'test' },
210
+ createdAt: now,
211
+ updatedAt: now,
212
+ })
213
+
214
+ const runs = await storage.getRunsByThread(thread.id)
215
+ assert.ok(runs.length >= 1)
216
+ assert.ok(runs.every((r) => r.threadId === thread.id))
217
+ })
218
+
219
+ test('resolveApproval', async () => {
220
+ const thread = await storage.createThread('resource-8')
221
+ const now = new Date()
222
+
223
+ await storage.saveMessages(thread.id, [
224
+ {
225
+ id: 'approval-msg',
226
+ role: 'assistant',
227
+ toolCalls: [{ id: 'approval-tc', name: 'dangerous-tool', args: {} }],
228
+ createdAt: now,
229
+ },
230
+ ])
231
+
232
+ const runId = await storage.createRun({
233
+ agentName: 'approval-agent',
234
+ threadId: thread.id,
235
+ resourceId: 'resource-8',
236
+ status: 'suspended',
237
+ suspendReason: 'approval',
238
+ pendingApprovals: [
239
+ {
240
+ type: 'tool-call',
241
+ toolCallId: 'approval-tc',
242
+ toolName: 'dangerous-tool',
243
+ args: {},
244
+ },
245
+ ],
246
+ usage: { inputTokens: 0, outputTokens: 0, model: 'test' },
247
+ createdAt: now,
248
+ updatedAt: now,
249
+ })
250
+
251
+ let run = await storage.getRun(runId)
252
+ assert.ok(run)
253
+ assert.ok(run.pendingApprovals)
254
+ assert.equal(run.pendingApprovals.length, 1)
255
+
256
+ const claimed = await storage.resolveApproval('approval-tc', 'approved')
257
+ assert.equal(claimed, true, 'the first approver claims the approval')
258
+
259
+ run = await storage.getRun(runId)
260
+ assert.ok(run)
261
+ assert.equal(run.pendingApprovals, undefined)
262
+
263
+ const second = await storage.resolveApproval('approval-tc', 'approved')
264
+ assert.equal(
265
+ second,
266
+ false,
267
+ 'a second approver of the same tool call claims nothing'
268
+ )
269
+ })
270
+
271
+ test('deleteThread cascades', async () => {
272
+ const thread = await storage.createThread('resource-del')
273
+ await storage.saveMessages(thread.id, [
274
+ {
275
+ id: 'del-msg',
276
+ role: 'user',
277
+ content: 'goodbye',
278
+ createdAt: new Date(),
279
+ },
280
+ ])
281
+
282
+ await storage.deleteThread(thread.id)
283
+ await assert.rejects(() => storage.getThread(thread.id))
284
+ })
285
+ })
286
+ }
@@ -0,0 +1,98 @@
1
+ import { describe, test, before } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import type { ChannelStore } from '../../wirings/channel/channel-store.js'
5
+ import type { ServiceTestConfig } from '../service-tests.js'
6
+
7
+ /** Conformance suite for `channelStore`. Runs only when a backend supplies one. */
8
+ export const defineChannelStoreTests = (
9
+ name: string,
10
+ channelStore: NonNullable<ServiceTestConfig['services']['channelStore']>
11
+ ): void => {
12
+ const factory = channelStore
13
+ describe(`ChannelStore [${name}]`, () => {
14
+ let store: ChannelStore
15
+
16
+ before(async () => {
17
+ store = await factory()
18
+ })
19
+
20
+ test('addChannel and getChannel', async () => {
21
+ await store.addChannel({
22
+ channelId: 'ch-1',
23
+ channelName: 'test-channel',
24
+ openingData: { foo: 'bar' },
25
+ })
26
+
27
+ const result = await store.getChannel('ch-1')
28
+ assert.equal(result.channelId, 'ch-1')
29
+ assert.equal(result.channelName, 'test-channel')
30
+ assert.deepEqual(result.openingData, { foo: 'bar' })
31
+ assert.equal(result.pikkuUserId, undefined)
32
+ })
33
+
34
+ test('setPikkuUserId', async () => {
35
+ await store.setPikkuUserId('ch-1', 'user-1')
36
+
37
+ const result = await store.getChannel('ch-1')
38
+ assert.equal(result.pikkuUserId, 'user-1')
39
+ })
40
+
41
+ test('setPikkuUserId to null', async () => {
42
+ await store.setPikkuUserId('ch-1', null)
43
+
44
+ const result = await store.getChannel('ch-1')
45
+ assert.equal(result.pikkuUserId, undefined)
46
+ })
47
+
48
+ test('getChannel throws for missing channel', async () => {
49
+ await assert.rejects(
50
+ async () => {
51
+ await store.getChannel('missing')
52
+ },
53
+ { message: 'Channel not found: missing' }
54
+ )
55
+ })
56
+
57
+ test('removeChannels', async () => {
58
+ await store.addChannel({
59
+ channelId: 'ch-2',
60
+ channelName: 'temp-channel',
61
+ })
62
+ await store.removeChannels(['ch-2'])
63
+ await assert.rejects(async () => {
64
+ await store.getChannel('ch-2')
65
+ })
66
+ })
67
+
68
+ test('removeChannels with empty array is no-op', async () => {
69
+ await store.removeChannels([])
70
+ })
71
+
72
+ test('session round-trip — set / get / clear', async () => {
73
+ await store.addChannel({
74
+ channelId: 'ch-state',
75
+ channelName: 'test-channel',
76
+ })
77
+
78
+ const empty = await store.getState('ch-state')
79
+ assert.equal(empty, undefined)
80
+
81
+ const payload = { userId: 'u-7', meta: { foo: 1 } }
82
+ await store.setState('ch-state', payload)
83
+ const got = await store.getState('ch-state')
84
+ assert.deepEqual(got, payload)
85
+
86
+ const next = { userId: 'u-8' }
87
+ await store.setState('ch-state', next)
88
+ const got2 = await store.getState('ch-state')
89
+ assert.deepEqual(got2, next)
90
+
91
+ await store.clearState('ch-state')
92
+ const after = await store.getState('ch-state')
93
+ assert.equal(after, undefined)
94
+
95
+ await store.removeChannels(['ch-state'])
96
+ })
97
+ })
98
+ }