@open-mercato/ai-assistant 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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 (273) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +361 -0
  3. package/README.md +5 -0
  4. package/dist/index.js +154 -0
  5. package/dist/index.js.map +2 -2
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js +73 -0
  7. package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js.map +7 -0
  8. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js +484 -0
  9. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js.map +7 -0
  10. package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js +251 -0
  11. package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js.map +7 -0
  12. package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js +91 -0
  13. package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js.map +7 -0
  14. package/dist/modules/ai_assistant/ai-tools/attachments-pack.js +202 -0
  15. package/dist/modules/ai_assistant/ai-tools/attachments-pack.js.map +7 -0
  16. package/dist/modules/ai_assistant/ai-tools/meta-pack.js +121 -0
  17. package/dist/modules/ai_assistant/ai-tools/meta-pack.js.map +7 -0
  18. package/dist/modules/ai_assistant/ai-tools/search-pack.js +94 -0
  19. package/dist/modules/ai_assistant/ai-tools/search-pack.js.map +7 -0
  20. package/dist/modules/ai_assistant/ai-tools.js +14 -0
  21. package/dist/modules/ai_assistant/ai-tools.js.map +7 -0
  22. package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js +175 -0
  23. package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js.map +7 -0
  24. package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js +174 -0
  25. package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js.map +7 -0
  26. package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js +101 -0
  27. package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js.map +7 -0
  28. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js +311 -0
  29. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js.map +7 -0
  30. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js +246 -0
  31. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js.map +7 -0
  32. package/dist/modules/ai_assistant/api/ai/agents/route.js +94 -0
  33. package/dist/modules/ai_assistant/api/ai/agents/route.js.map +7 -0
  34. package/dist/modules/ai_assistant/api/ai/chat/route.js +173 -0
  35. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +7 -0
  36. package/dist/modules/ai_assistant/api/ai/run-object/route.js +167 -0
  37. package/dist/modules/ai_assistant/api/ai/run-object/route.js.map +7 -0
  38. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +1111 -0
  39. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +7 -0
  40. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js +10 -0
  41. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js.map +7 -0
  42. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js +28 -0
  43. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js.map +7 -0
  44. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +10 -0
  45. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +7 -0
  46. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js +30 -0
  47. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js.map +7 -0
  48. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +4 -6
  49. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +2 -2
  50. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +1 -21
  51. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +2 -2
  52. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +462 -0
  53. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +7 -0
  54. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js +10 -0
  55. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js.map +7 -0
  56. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js +28 -0
  57. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js.map +7 -0
  58. package/dist/modules/ai_assistant/cli.js +78 -12
  59. package/dist/modules/ai_assistant/cli.js.map +2 -2
  60. package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js +5 -0
  61. package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js.map +7 -0
  62. package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js +5 -0
  63. package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js.map +7 -0
  64. package/dist/modules/ai_assistant/data/entities/AiPendingAction.js +5 -0
  65. package/dist/modules/ai_assistant/data/entities/AiPendingAction.js.map +7 -0
  66. package/dist/modules/ai_assistant/data/entities.js +228 -0
  67. package/dist/modules/ai_assistant/data/entities.js.map +7 -0
  68. package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js +95 -0
  69. package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js.map +7 -0
  70. package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js +95 -0
  71. package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js.map +7 -0
  72. package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js +223 -0
  73. package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js.map +7 -0
  74. package/dist/modules/ai_assistant/events.js +33 -0
  75. package/dist/modules/ai_assistant/events.js.map +7 -0
  76. package/dist/modules/ai_assistant/i18n/de.json +252 -0
  77. package/dist/modules/ai_assistant/i18n/en.json +252 -0
  78. package/dist/modules/ai_assistant/i18n/es.json +252 -0
  79. package/dist/modules/ai_assistant/i18n/pl.json +252 -0
  80. package/dist/modules/ai_assistant/lib/agent-policy.js +168 -0
  81. package/dist/modules/ai_assistant/lib/agent-policy.js.map +7 -0
  82. package/dist/modules/ai_assistant/lib/agent-registry.js +195 -0
  83. package/dist/modules/ai_assistant/lib/agent-registry.js.map +7 -0
  84. package/dist/modules/ai_assistant/lib/agent-runtime.js +451 -0
  85. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +7 -0
  86. package/dist/modules/ai_assistant/lib/agent-tools.js +223 -0
  87. package/dist/modules/ai_assistant/lib/agent-tools.js.map +7 -0
  88. package/dist/modules/ai_assistant/lib/agent-transport.js +25 -0
  89. package/dist/modules/ai_assistant/lib/agent-transport.js.map +7 -0
  90. package/dist/modules/ai_assistant/lib/ai-agent-definition.js +11 -0
  91. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +7 -0
  92. package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js +1 -0
  93. package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js.map +7 -0
  94. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js +239 -0
  95. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js.map +7 -0
  96. package/dist/modules/ai_assistant/lib/ai-overrides.js +189 -0
  97. package/dist/modules/ai_assistant/lib/ai-overrides.js.map +7 -0
  98. package/dist/modules/ai_assistant/lib/ai-tool-definition.js +7 -0
  99. package/dist/modules/ai_assistant/lib/ai-tool-definition.js.map +7 -0
  100. package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js +1 -0
  101. package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js.map +7 -0
  102. package/dist/modules/ai_assistant/lib/api-backed-tool.js +48 -0
  103. package/dist/modules/ai_assistant/lib/api-backed-tool.js.map +7 -0
  104. package/dist/modules/ai_assistant/lib/attachment-bridge-types.js +1 -0
  105. package/dist/modules/ai_assistant/lib/attachment-bridge-types.js.map +7 -0
  106. package/dist/modules/ai_assistant/lib/attachment-parts.js +276 -0
  107. package/dist/modules/ai_assistant/lib/attachment-parts.js.map +7 -0
  108. package/dist/modules/ai_assistant/lib/model-factory.js +68 -0
  109. package/dist/modules/ai_assistant/lib/model-factory.js.map +7 -0
  110. package/dist/modules/ai_assistant/lib/pending-action-cancel.js +86 -0
  111. package/dist/modules/ai_assistant/lib/pending-action-cancel.js.map +7 -0
  112. package/dist/modules/ai_assistant/lib/pending-action-client.js +35 -0
  113. package/dist/modules/ai_assistant/lib/pending-action-client.js.map +7 -0
  114. package/dist/modules/ai_assistant/lib/pending-action-executor.js +243 -0
  115. package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +7 -0
  116. package/dist/modules/ai_assistant/lib/pending-action-recheck.js +246 -0
  117. package/dist/modules/ai_assistant/lib/pending-action-recheck.js.map +7 -0
  118. package/dist/modules/ai_assistant/lib/pending-action-types.js +70 -0
  119. package/dist/modules/ai_assistant/lib/pending-action-types.js.map +7 -0
  120. package/dist/modules/ai_assistant/lib/prepare-mutation.js +315 -0
  121. package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +7 -0
  122. package/dist/modules/ai_assistant/lib/prompt-composition-types.js +7 -0
  123. package/dist/modules/ai_assistant/lib/prompt-composition-types.js.map +7 -0
  124. package/dist/modules/ai_assistant/lib/prompt-override-merge.js +175 -0
  125. package/dist/modules/ai_assistant/lib/prompt-override-merge.js.map +7 -0
  126. package/dist/modules/ai_assistant/lib/schema-utils.js +5 -1
  127. package/dist/modules/ai_assistant/lib/schema-utils.js.map +2 -2
  128. package/dist/modules/ai_assistant/lib/tool-executor.js +13 -2
  129. package/dist/modules/ai_assistant/lib/tool-executor.js.map +2 -2
  130. package/dist/modules/ai_assistant/lib/tool-loader.js +86 -11
  131. package/dist/modules/ai_assistant/lib/tool-loader.js.map +2 -2
  132. package/dist/modules/ai_assistant/lib/tool-test-fixtures.js +120 -0
  133. package/dist/modules/ai_assistant/lib/tool-test-fixtures.js.map +7 -0
  134. package/dist/modules/ai_assistant/lib/tool-test-runner.js +418 -0
  135. package/dist/modules/ai_assistant/lib/tool-test-runner.js.map +7 -0
  136. package/dist/modules/ai_assistant/migrations/Migration20260419100521.js +17 -0
  137. package/dist/modules/ai_assistant/migrations/Migration20260419100521.js.map +7 -0
  138. package/dist/modules/ai_assistant/migrations/Migration20260419132948.js +16 -0
  139. package/dist/modules/ai_assistant/migrations/Migration20260419132948.js.map +7 -0
  140. package/dist/modules/ai_assistant/migrations/Migration20260419134235.js +17 -0
  141. package/dist/modules/ai_assistant/migrations/Migration20260419134235.js.map +7 -0
  142. package/dist/modules/ai_assistant/setup.js +36 -0
  143. package/dist/modules/ai_assistant/setup.js.map +2 -2
  144. package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js +161 -0
  145. package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js.map +7 -0
  146. package/generated/entities/ai_agent_mutation_policy_override/index.ts +9 -0
  147. package/generated/entities/ai_agent_prompt_override/index.ts +10 -0
  148. package/generated/entities/ai_pending_action/index.ts +24 -0
  149. package/generated/entities.ids.generated.ts +13 -0
  150. package/generated/entity-fields-registry.ts +57 -0
  151. package/jest.config.cjs +7 -0
  152. package/package.json +4 -4
  153. package/src/index.ts +215 -0
  154. package/src/modules/ai_assistant/__integration__/README.md +5 -0
  155. package/src/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.ts +115 -0
  156. package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.ts +574 -0
  157. package/src/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.ts +333 -0
  158. package/src/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.ts +135 -0
  159. package/src/modules/ai_assistant/__tests__/events.test.ts +145 -0
  160. package/src/modules/ai_assistant/__tests__/integration/pending-action-contract.test.ts +1015 -0
  161. package/src/modules/ai_assistant/__tests__/integration/ws-c-attachment-bridge.test.ts +235 -0
  162. package/src/modules/ai_assistant/__tests__/integration/ws-c-policy-and-tools.test.ts +330 -0
  163. package/src/modules/ai_assistant/__tests__/integration/ws-c-tool-pack-coverage.test.ts +285 -0
  164. package/src/modules/ai_assistant/ai-tools/__tests__/attachments-pack.test.ts +322 -0
  165. package/src/modules/ai_assistant/ai-tools/__tests__/meta-pack.test.ts +218 -0
  166. package/src/modules/ai_assistant/ai-tools/__tests__/search-pack.test.ts +192 -0
  167. package/src/modules/ai_assistant/ai-tools/attachments-pack.ts +269 -0
  168. package/src/modules/ai_assistant/ai-tools/meta-pack.ts +140 -0
  169. package/src/modules/ai_assistant/ai-tools/search-pack.ts +122 -0
  170. package/src/modules/ai_assistant/ai-tools.ts +21 -0
  171. package/src/modules/ai_assistant/api/ai/actions/[id]/__tests__/route.test.ts +222 -0
  172. package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/__tests__/route.test.ts +286 -0
  173. package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/route.ts +237 -0
  174. package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/__tests__/route.test.ts +339 -0
  175. package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/route.ts +229 -0
  176. package/src/modules/ai_assistant/api/ai/actions/[id]/route.ts +142 -0
  177. package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/__tests__/route.test.ts +367 -0
  178. package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.ts +380 -0
  179. package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/__tests__/route.test.ts +333 -0
  180. package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.ts +307 -0
  181. package/src/modules/ai_assistant/api/ai/agents/route.ts +107 -0
  182. package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +282 -0
  183. package/src/modules/ai_assistant/api/ai/chat/route.ts +207 -0
  184. package/src/modules/ai_assistant/api/ai/run-object/__tests__/route.test.ts +282 -0
  185. package/src/modules/ai_assistant/api/ai/run-object/route.ts +204 -0
  186. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +1419 -0
  187. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.ts +26 -0
  188. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.tsx +12 -0
  189. package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.ts +28 -0
  190. package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +12 -0
  191. package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +8 -23
  192. package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +15 -10
  193. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +604 -0
  194. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.ts +26 -0
  195. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.tsx +12 -0
  196. package/src/modules/ai_assistant/cli.ts +99 -24
  197. package/src/modules/ai_assistant/data/__tests__/schema-unique-indexes.test.ts +69 -0
  198. package/src/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.ts +7 -0
  199. package/src/modules/ai_assistant/data/entities/AiAgentPromptOverride.ts +7 -0
  200. package/src/modules/ai_assistant/data/entities/AiPendingAction.ts +7 -0
  201. package/src/modules/ai_assistant/data/entities.ts +270 -0
  202. package/src/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.ts +129 -0
  203. package/src/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.ts +132 -0
  204. package/src/modules/ai_assistant/data/repositories/AiPendingActionRepository.ts +334 -0
  205. package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentMutationPolicyOverrideRepository.test.ts +195 -0
  206. package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentPromptOverrideRepository.test.ts +197 -0
  207. package/src/modules/ai_assistant/data/repositories/__tests__/AiPendingActionRepository.test.ts +357 -0
  208. package/src/modules/ai_assistant/events.ts +112 -0
  209. package/src/modules/ai_assistant/i18n/de.json +252 -0
  210. package/src/modules/ai_assistant/i18n/en.json +252 -0
  211. package/src/modules/ai_assistant/i18n/es.json +252 -0
  212. package/src/modules/ai_assistant/i18n/pl.json +252 -0
  213. package/src/modules/ai_assistant/lib/__tests__/agent-policy.mutation-override.test.ts +203 -0
  214. package/src/modules/ai_assistant/lib/__tests__/agent-policy.test.ts +385 -0
  215. package/src/modules/ai_assistant/lib/__tests__/agent-registry.test.ts +217 -0
  216. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-object.test.ts +329 -0
  217. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-parity.test.ts +573 -0
  218. package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +291 -0
  219. package/src/modules/ai_assistant/lib/__tests__/agent-tools.test.ts +172 -0
  220. package/src/modules/ai_assistant/lib/__tests__/agent-transport.test.ts +41 -0
  221. package/src/modules/ai_assistant/lib/__tests__/ai-agent-definition.test.ts +183 -0
  222. package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +432 -0
  223. package/src/modules/ai_assistant/lib/__tests__/ai-overrides.test.ts +308 -0
  224. package/src/modules/ai_assistant/lib/__tests__/api-backed-tool.test.ts +302 -0
  225. package/src/modules/ai_assistant/lib/__tests__/attachment-bridge-and-prompt-types.test.ts +188 -0
  226. package/src/modules/ai_assistant/lib/__tests__/attachment-parts.test.ts +531 -0
  227. package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +263 -0
  228. package/src/modules/ai_assistant/lib/__tests__/model-factory.integration.test.ts +183 -0
  229. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +168 -0
  230. package/src/modules/ai_assistant/lib/__tests__/pending-action-cancel.test.ts +235 -0
  231. package/src/modules/ai_assistant/lib/__tests__/pending-action-client.test.ts +148 -0
  232. package/src/modules/ai_assistant/lib/__tests__/pending-action-executor.test.ts +348 -0
  233. package/src/modules/ai_assistant/lib/__tests__/pending-action-recheck.test.ts +378 -0
  234. package/src/modules/ai_assistant/lib/__tests__/phase-0-additive-contract.test.ts +299 -0
  235. package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +610 -0
  236. package/src/modules/ai_assistant/lib/__tests__/prompt-override-merge.test.ts +136 -0
  237. package/src/modules/ai_assistant/lib/__tests__/tool-loader.test.ts +125 -0
  238. package/src/modules/ai_assistant/lib/agent-policy.ts +270 -0
  239. package/src/modules/ai_assistant/lib/agent-registry.ts +277 -0
  240. package/src/modules/ai_assistant/lib/agent-runtime.ts +751 -0
  241. package/src/modules/ai_assistant/lib/agent-tools.ts +396 -0
  242. package/src/modules/ai_assistant/lib/agent-transport.ts +51 -0
  243. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +86 -0
  244. package/src/modules/ai_assistant/lib/ai-agents-generated.d.ts +18 -0
  245. package/src/modules/ai_assistant/lib/ai-api-operation-runner.ts +333 -0
  246. package/src/modules/ai_assistant/lib/ai-overrides.ts +389 -0
  247. package/src/modules/ai_assistant/lib/ai-tool-definition.ts +7 -0
  248. package/src/modules/ai_assistant/lib/ai-tools-generated.d.ts +7 -0
  249. package/src/modules/ai_assistant/lib/api-backed-tool.ts +85 -0
  250. package/src/modules/ai_assistant/lib/attachment-bridge-types.ts +24 -0
  251. package/src/modules/ai_assistant/lib/attachment-parts.ts +433 -0
  252. package/src/modules/ai_assistant/lib/model-factory.ts +212 -0
  253. package/src/modules/ai_assistant/lib/pending-action-cancel.ts +179 -0
  254. package/src/modules/ai_assistant/lib/pending-action-client.ts +126 -0
  255. package/src/modules/ai_assistant/lib/pending-action-executor.ts +424 -0
  256. package/src/modules/ai_assistant/lib/pending-action-recheck.ts +410 -0
  257. package/src/modules/ai_assistant/lib/pending-action-types.ts +194 -0
  258. package/src/modules/ai_assistant/lib/prepare-mutation.ts +448 -0
  259. package/src/modules/ai_assistant/lib/prompt-composition-types.ts +24 -0
  260. package/src/modules/ai_assistant/lib/prompt-override-merge.ts +253 -0
  261. package/src/modules/ai_assistant/lib/schema-utils.ts +14 -2
  262. package/src/modules/ai_assistant/lib/tool-executor.ts +25 -3
  263. package/src/modules/ai_assistant/lib/tool-loader.ts +159 -13
  264. package/src/modules/ai_assistant/lib/tool-test-fixtures.ts +160 -0
  265. package/src/modules/ai_assistant/lib/tool-test-runner.ts +596 -0
  266. package/src/modules/ai_assistant/lib/types.ts +105 -2
  267. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +871 -0
  268. package/src/modules/ai_assistant/migrations/Migration20260419100521.ts +17 -0
  269. package/src/modules/ai_assistant/migrations/Migration20260419132948.ts +16 -0
  270. package/src/modules/ai_assistant/migrations/Migration20260419134235.ts +17 -0
  271. package/src/modules/ai_assistant/setup.ts +53 -0
  272. package/src/modules/ai_assistant/workers/__tests__/ai-pending-action-cleanup.test.ts +333 -0
  273. package/src/modules/ai_assistant/workers/ai-pending-action-cleanup.ts +269 -0
@@ -0,0 +1,333 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';
3
+
4
+ /**
5
+ * TC-AI-PLAYGROUND-004: AI Playground page smoke (Step 4.4 / Phase 2 WS-B).
6
+ *
7
+ * Covers the first user-facing embedding of `<AiChat>` in the backoffice. The
8
+ * route is guarded by `ai_assistant.settings.manage`; superadmin always holds
9
+ * it. The agent registry may be empty in CI (generated file depends on
10
+ * module authors registering agents), so the spec handles BOTH branches:
11
+ * - populated registry: assert the agent picker, debug toggle, and chat
12
+ * surface render;
13
+ * - empty registry: assert the `EmptyState` copy renders instead.
14
+ *
15
+ * The chat SSE response is stubbed via Playwright's route interception so the
16
+ * test never has to hit a live LLM provider. The object-mode run-object route
17
+ * is also stubbed to assert wiring without invoking a real model.
18
+ */
19
+ test.describe('TC-AI-PLAYGROUND-004: AI Playground', () => {
20
+ const playgroundPath = '/backend/config/ai-assistant/playground';
21
+
22
+ test('playground renders agent picker or empty state for superadmin', async ({ page }) => {
23
+ await login(page, 'superadmin');
24
+
25
+ // Stub the agents list so the test behavior is deterministic regardless of
26
+ // what the generated registry holds at run-time.
27
+ await page.route('**/api/ai_assistant/ai/agents', async (route) => {
28
+ await route.fulfill({
29
+ status: 200,
30
+ contentType: 'application/json',
31
+ body: JSON.stringify({
32
+ agents: [
33
+ {
34
+ id: 'customers.assistant',
35
+ moduleId: 'customers',
36
+ label: 'Customers assistant',
37
+ description: 'Answers questions about customer records.',
38
+ executionMode: 'chat',
39
+ mutationPolicy: 'read-only',
40
+ allowedTools: ['customers.list_people'],
41
+ requiredFeatures: [],
42
+ acceptedMediaTypes: [],
43
+ hasOutputSchema: false,
44
+ },
45
+ {
46
+ id: 'catalog.extract',
47
+ moduleId: 'catalog',
48
+ label: 'Catalog extractor',
49
+ description: 'Extracts structured product metadata.',
50
+ executionMode: 'object',
51
+ mutationPolicy: 'read-only',
52
+ allowedTools: [],
53
+ requiredFeatures: [],
54
+ acceptedMediaTypes: [],
55
+ hasOutputSchema: true,
56
+ },
57
+ ],
58
+ total: 2,
59
+ }),
60
+ });
61
+ });
62
+
63
+ // Stub the chat SSE so the composer has something to "succeed" against.
64
+ await page.route('**/api/ai_assistant/ai/chat**', async (route) => {
65
+ await route.fulfill({
66
+ status: 200,
67
+ contentType: 'text/event-stream',
68
+ body: 'ok from stubbed SSE\n',
69
+ });
70
+ });
71
+
72
+ // Stub the run-object dispatcher so the object-mode tab has a deterministic response.
73
+ await page.route('**/api/ai_assistant/ai/run-object', async (route) => {
74
+ await route.fulfill({
75
+ status: 200,
76
+ contentType: 'application/json',
77
+ body: JSON.stringify({
78
+ object: { title: 'Stubbed title' },
79
+ finishReason: 'stop',
80
+ usage: { inputTokens: 1, outputTokens: 2 },
81
+ }),
82
+ });
83
+ });
84
+
85
+ await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });
86
+
87
+ const container = page.locator('[data-ai-playground]');
88
+ const empty = page.getByText(/No AI agents are registered/i).first();
89
+ const loadError = page.locator('[data-ai-playground-error]');
90
+
91
+ // Wait until the page has stabilized (either container, empty state, or a load error).
92
+ await expect(container.or(empty).or(loadError)).toBeVisible({ timeout: 15_000 });
93
+
94
+ if (await container.isVisible().catch(() => false)) {
95
+ const picker = page.locator('[data-ai-playground-agent-picker]');
96
+ await expect(picker).toBeVisible();
97
+ // Two stubbed agents should surface.
98
+ const optionCount = await picker.locator('option').count();
99
+ expect(optionCount).toBeGreaterThanOrEqual(2);
100
+
101
+ const debugToggle = page.locator('[data-ai-playground-debug-toggle]');
102
+ await expect(debugToggle).toBeVisible();
103
+
104
+ const composer = page.locator('#ai-chat-composer');
105
+ await expect(composer).toBeVisible();
106
+ await composer.fill('Hello from Playwright');
107
+ await expect(composer).toHaveValue('Hello from Playwright');
108
+
109
+ // Step 4.6: toggling the debug panel should reveal the three collapsible
110
+ // sections (tool map / prompt sections / last request), each addressable
111
+ // by its `data-ai-chat-debug-section` attribute.
112
+ const debugPanel = page.locator('[data-ai-chat-debug="true"]');
113
+ const initiallyVisible = await debugPanel.isVisible().catch(() => false);
114
+ if (!initiallyVisible) {
115
+ await debugToggle.click();
116
+ }
117
+ await expect(debugPanel).toBeVisible({ timeout: 5_000 });
118
+ await expect(
119
+ page.locator('[data-ai-chat-debug-section="tools"]'),
120
+ ).toBeVisible();
121
+ await expect(
122
+ page.locator('[data-ai-chat-debug-section="promptSections"]'),
123
+ ).toBeVisible();
124
+ await expect(
125
+ page.locator('[data-ai-chat-debug-section="lastRequest"]'),
126
+ ).toBeVisible();
127
+ } else if (await empty.isVisible().catch(() => false)) {
128
+ // Empty branch: agent registry is empty in this environment.
129
+ await expect(empty).toBeVisible();
130
+ } else {
131
+ // Fatal load error branch — still acceptable evidence that the guard fired.
132
+ await expect(loadError).toBeVisible();
133
+ }
134
+ });
135
+
136
+ test('picker lists all three Phase 2 agents and chat-mode selection disables object-mode tab', async ({
137
+ page,
138
+ }) => {
139
+ test.setTimeout(120_000);
140
+ await login(page, 'superadmin');
141
+
142
+ // Hit the live registry for this scenario — Phase 2 agents are all
143
+ // chat-mode so we can assert the "not supported" alert on the object
144
+ // tab without stubbing.
145
+ await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });
146
+
147
+ const container = page.locator('[data-ai-playground]');
148
+ await expect(container).toBeVisible({ timeout: 60_000 });
149
+
150
+ const picker = page.locator('[data-ai-playground-agent-picker]');
151
+ await expect(picker).toBeVisible();
152
+
153
+ // The three Phase 2 agents must be present.
154
+ await expect(
155
+ picker.locator('option[value="customers.account_assistant"]'),
156
+ ).toHaveCount(1);
157
+ await expect(
158
+ picker.locator('option[value="catalog.catalog_assistant"]'),
159
+ ).toHaveCount(1);
160
+ await expect(
161
+ picker.locator('option[value="catalog.merchandising_assistant"]'),
162
+ ).toHaveCount(1);
163
+
164
+ // Switch to the object-mode tab — Phase 2 agents are all chat-mode, so
165
+ // the "not supported" info Alert should render with the documented
166
+ // `data-ai-playground-unsupported="object"` marker.
167
+ await page.getByRole('tab', { name: /object mode/i }).click();
168
+ await expect(
169
+ page.locator('[data-ai-playground-unsupported="object"]'),
170
+ ).toBeVisible();
171
+
172
+ // Flip back to chat tab; the `AiChat` region for the currently selected
173
+ // agent must render.
174
+ await page.getByRole('tab', { name: /^chat$/i }).click();
175
+ const chatRegion = page.locator('[data-ai-chat-agent]').first();
176
+ await expect(chatRegion).toBeVisible();
177
+ });
178
+
179
+ test('chat happy path — stubbed SSE response appears in the transcript', async ({ page }) => {
180
+ test.setTimeout(120_000);
181
+ await login(page, 'superadmin');
182
+
183
+ // Stub agents so the picker is deterministic.
184
+ await page.route('**/api/ai_assistant/ai/agents', async (route) => {
185
+ await route.fulfill({
186
+ status: 200,
187
+ contentType: 'application/json',
188
+ body: JSON.stringify({
189
+ agents: [
190
+ {
191
+ id: 'customers.account_assistant',
192
+ moduleId: 'customers',
193
+ label: 'Customers account assistant',
194
+ description: 'Stubbed agent for playground chat smoke.',
195
+ executionMode: 'chat',
196
+ mutationPolicy: 'read-only',
197
+ allowedTools: [],
198
+ requiredFeatures: [],
199
+ acceptedMediaTypes: [],
200
+ hasOutputSchema: false,
201
+ },
202
+ ],
203
+ total: 1,
204
+ }),
205
+ });
206
+ });
207
+
208
+ // Stub the SSE dispatcher with a canned text stream the AiChat reader
209
+ // consumes. The AiChat component writes streamed deltas into the
210
+ // transcript; we assert the text surfaces regardless of the exact
211
+ // stream dialect by filling in both a `data:` framed payload and a
212
+ // plain-text fallback.
213
+ const streamBody = [
214
+ 'event: text',
215
+ 'data: {"content":"stubbed-playground-reply"}',
216
+ '',
217
+ 'event: done',
218
+ 'data: {}',
219
+ '',
220
+ ].join('\n');
221
+ await page.route('**/api/ai_assistant/ai/chat**', async (route) => {
222
+ await route.fulfill({
223
+ status: 200,
224
+ contentType: 'text/event-stream',
225
+ body: streamBody,
226
+ });
227
+ });
228
+
229
+ await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });
230
+
231
+ const composer = page.locator('#ai-chat-composer');
232
+ await expect(composer).toBeVisible({ timeout: 60_000 });
233
+ await composer.fill('Say hi please');
234
+ await composer.press('Meta+Enter');
235
+ // On non-mac runners Meta+Enter is a no-op; try Control+Enter too.
236
+ await page.waitForTimeout(200);
237
+ if (!(await page.locator('[data-ai-chat-state="thinking"]').count())) {
238
+ await composer.press('Control+Enter');
239
+ }
240
+
241
+ // The composer should have been cleared on submit, proving the
242
+ // handler fired. The SSE stream body itself is driven by the agent
243
+ // runtime which may surface the text either verbatim or through a
244
+ // rendered message row — accept either signal.
245
+ await expect(async () => {
246
+ const cleared = (await composer.inputValue()) === '';
247
+ const rendered = await page.getByText(/stubbed-playground-reply/i).count();
248
+ const thinking = await page.locator('[data-ai-chat-state="thinking"]').count();
249
+ expect(cleared || rendered > 0 || thinking > 0).toBe(true);
250
+ }).toPass({ timeout: 10_000 });
251
+ });
252
+
253
+ test('mutation-preview-card renders inside the playground transcript when a pending action is emitted', async ({
254
+ page,
255
+ }) => {
256
+ test.setTimeout(120_000);
257
+ await login(page, 'superadmin');
258
+
259
+ // Deterministic agent registry — a mutation-capable chat agent.
260
+ await page.route('**/api/ai_assistant/ai/agents', async (route) => {
261
+ await route.fulfill({
262
+ status: 200,
263
+ contentType: 'application/json',
264
+ body: JSON.stringify({
265
+ agents: [
266
+ {
267
+ id: 'customers.account_assistant',
268
+ moduleId: 'customers',
269
+ label: 'Customers account assistant',
270
+ description: 'Mutation-capable agent for Phase 3 approval flow.',
271
+ executionMode: 'chat',
272
+ mutationPolicy: 'require-approval',
273
+ allowedTools: ['customers.update_person'],
274
+ requiredFeatures: [],
275
+ acceptedMediaTypes: [],
276
+ hasOutputSchema: false,
277
+ },
278
+ ],
279
+ total: 1,
280
+ }),
281
+ });
282
+ });
283
+
284
+ // Stub the polling endpoint with a pending row so the preview card
285
+ // resolves its state. The Playwright test never hits a real DB.
286
+ const pendingRow = {
287
+ pendingAction: {
288
+ id: 'pa-stub-001',
289
+ agentId: 'customers.account_assistant',
290
+ toolName: 'customers.update_person',
291
+ status: 'pending',
292
+ fieldDiff: [
293
+ { field: 'name', before: 'Alice', after: 'Alicia' },
294
+ ],
295
+ records: null,
296
+ failedRecords: null,
297
+ sideEffectsSummary: 'Rename Alice to Alicia.',
298
+ attachmentIds: [],
299
+ targetEntityType: 'customers.person',
300
+ targetRecordId: 'p-1',
301
+ recordVersion: '1',
302
+ executionResult: null,
303
+ createdAt: new Date().toISOString(),
304
+ expiresAt: new Date(Date.now() + 600_000).toISOString(),
305
+ resolvedAt: null,
306
+ resolvedByUserId: null,
307
+ },
308
+ };
309
+ await page.route('**/api/ai_assistant/ai/actions/pa-stub-001', async (route) => {
310
+ await route.fulfill({
311
+ status: 200,
312
+ contentType: 'application/json',
313
+ body: JSON.stringify(pendingRow),
314
+ });
315
+ });
316
+
317
+ // Navigate with the debug seed that instructs the playground to inject
318
+ // the `mutation-preview-card` UI part. This is the Step 5.10 stub path
319
+ // until the dispatcher surfaces UI parts through the streamed body.
320
+ await page.goto(
321
+ `${playgroundPath}?uiPart=mutation-preview-card&pendingActionId=pa-stub-001`,
322
+ { waitUntil: 'domcontentloaded' },
323
+ );
324
+
325
+ const container = page.locator('[data-ai-playground]');
326
+ await expect(container).toBeVisible({ timeout: 60_000 });
327
+
328
+ const previewCard = page.locator('[data-ai-mutation-preview]').first();
329
+ await expect(previewCard).toBeVisible({ timeout: 15_000 });
330
+ await expect(page.locator('[data-ai-mutation-preview-confirm]')).toBeVisible();
331
+ await expect(page.locator('[data-ai-mutation-preview-cancel]')).toBeVisible();
332
+ });
333
+ });
@@ -0,0 +1,135 @@
1
+ import { spawn } from 'node:child_process'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { expect, test } from '@playwright/test'
5
+
6
+ const __filename = fileURLToPath(import.meta.url)
7
+ const __dirname = path.dirname(__filename)
8
+
9
+ /**
10
+ * TC-INT-AI-TOOLS: Smoke-test every AI tool registered via `defineAiTool`.
11
+ *
12
+ * Strategy: shells out to `yarn mercato ai_assistant test-tools --json` which
13
+ * runs the in-process tool runner (`packages/ai-assistant/.../lib/tool-test-runner.ts`).
14
+ * The runner iterates every entry in `apps/mercato/.mercato/generated/ai-tools.generated.ts`,
15
+ * invokes each handler with a small fixture input against a super-admin
16
+ * context, and returns a structured report. Mutation tools are exercised
17
+ * through `prepareMutation` only — the test asserts a pending-action envelope
18
+ * is returned and never confirms the action.
19
+ *
20
+ * No HTTP endpoint is added; the runner is CLI-only and never exposed.
21
+ */
22
+
23
+ interface ToolTestRecord {
24
+ module: string
25
+ tool: string
26
+ isMutation: boolean
27
+ status: 'pass' | 'fail' | 'skip'
28
+ durationMs: number
29
+ reason?: string
30
+ }
31
+
32
+ interface ToolTestReport {
33
+ tenantId: string | null
34
+ organizationId: string | null
35
+ total: number
36
+ passed: number
37
+ failed: number
38
+ skipped: number
39
+ records: ToolTestRecord[]
40
+ }
41
+
42
+ const REPORT_BEGIN = '---TOOL_TEST_REPORT_BEGIN---'
43
+ const REPORT_END = '---TOOL_TEST_REPORT_END---'
44
+
45
+ function findRepoRoot(): string {
46
+ return path.resolve(__dirname, '..', '..', '..', '..', '..', '..')
47
+ }
48
+
49
+ function findAppRoot(): string {
50
+ const testAppRoot = process.env.OM_TEST_APP_ROOT
51
+ if (testAppRoot && testAppRoot.length > 0) {
52
+ return testAppRoot
53
+ }
54
+
55
+ // The CLI loads its env from apps/mercato/.env (DB connection, JWT secret,
56
+ // encryption fallback). Spawning from there makes the bootstrap reach a
57
+ // ready state instead of bailing on the MFA-secret precondition.
58
+ return path.join(findRepoRoot(), 'apps', 'mercato')
59
+ }
60
+
61
+ function runToolTestsCli(): Promise<{ stdout: string; stderr: string; code: number }> {
62
+ return new Promise((resolveCmd, rejectCmd) => {
63
+ const cwd = findAppRoot()
64
+ const child = spawn(
65
+ 'yarn',
66
+ ['mercato', 'ai_assistant', 'test-tools', '--json'],
67
+ {
68
+ cwd,
69
+ env: { ...process.env, FORCE_COLOR: '0', NODE_NO_WARNINGS: '1' },
70
+ shell: false,
71
+ },
72
+ )
73
+ let stdout = ''
74
+ let stderr = ''
75
+ child.stdout.on('data', (chunk) => {
76
+ stdout += chunk.toString()
77
+ })
78
+ child.stderr.on('data', (chunk) => {
79
+ stderr += chunk.toString()
80
+ })
81
+ child.on('error', (err) => rejectCmd(err))
82
+ child.on('close', (code) => {
83
+ resolveCmd({ stdout, stderr, code: code ?? -1 })
84
+ })
85
+ })
86
+ }
87
+
88
+ function parseReport(stdout: string): ToolTestReport {
89
+ const beginIdx = stdout.indexOf(REPORT_BEGIN)
90
+ const endIdx = stdout.indexOf(REPORT_END)
91
+ if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) {
92
+ throw new Error(
93
+ `Could not find report markers in CLI output. stdout (first 500 chars):\n${stdout.slice(0, 500)}`,
94
+ )
95
+ }
96
+ const payload = stdout.slice(beginIdx + REPORT_BEGIN.length, endIdx).trim()
97
+ return JSON.parse(payload) as ToolTestReport
98
+ }
99
+
100
+ test.describe('TC-INT-AI-TOOLS: Every AI tool returns the expected shape', () => {
101
+ test('all registered tools either pass or skip with a reason; none fail', async () => {
102
+ test.slow()
103
+ const { stdout, stderr, code } = await runToolTestsCli()
104
+ if (code !== 0 && stdout.indexOf(REPORT_BEGIN) === -1) {
105
+ throw new Error(
106
+ `tool-test CLI failed (exit ${code}) before producing a report.\nstderr:\n${stderr.slice(0, 2000)}\nstdout:\n${stdout.slice(0, 1000)}`,
107
+ )
108
+ }
109
+ const report = parseReport(stdout)
110
+
111
+ // Surface a compact summary in the test log for triage.
112
+ const failures = report.records.filter((r) => r.status === 'fail')
113
+ const skips = report.records.filter((r) => r.status === 'skip')
114
+ if (failures.length > 0) {
115
+ console.log(
116
+ `[TC-INT-AI-TOOLS] Failures (${failures.length}):\n${failures
117
+ .map((r) => ` - ${r.tool}: ${r.reason ?? '<no reason>'}`)
118
+ .join('\n')}`,
119
+ )
120
+ }
121
+ if (skips.length > 0) {
122
+ console.log(
123
+ `[TC-INT-AI-TOOLS] Skips (${skips.length}): ${skips
124
+ .map((r) => `${r.tool} (${r.reason ?? 'skipped'})`)
125
+ .join(', ')}`,
126
+ )
127
+ }
128
+ console.log(
129
+ `[TC-INT-AI-TOOLS] Result: total=${report.total} pass=${report.passed} fail=${report.failed} skip=${report.skipped}`,
130
+ )
131
+
132
+ expect(failures, `Expected zero failing AI tools, got ${failures.length}`).toEqual([])
133
+ expect(report.passed, 'At least one tool must run successfully').toBeGreaterThan(0)
134
+ })
135
+ })
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Unit coverage for the ai_assistant module event declarations (Step 5.11).
3
+ *
4
+ * Asserts:
5
+ * - All three FROZEN event IDs (`ai.action.confirmed` /
6
+ * `ai.action.cancelled` / `ai.action.expired`) appear in
7
+ * `eventsConfig.events` under `category: 'system'`.
8
+ * - The typed `emitAiAssistantEvent` helper hands declared events off to
9
+ * the global event bus with the payload untouched.
10
+ * - Emitting an undeclared event id is rejected at the helper boundary
11
+ * via `createModuleEvents`' strict-mode validation.
12
+ */
13
+ import {
14
+ eventsConfig,
15
+ emitAiAssistantEvent,
16
+ type AiAssistantEventId,
17
+ type AiActionCancelledPayload,
18
+ type AiActionConfirmedPayload,
19
+ type AiActionExpiredPayload,
20
+ } from '../events'
21
+ import { setGlobalEventBus } from '@open-mercato/shared/modules/events'
22
+
23
+ const FROZEN_EVENT_IDS: ReadonlyArray<AiAssistantEventId> = [
24
+ 'ai.action.confirmed',
25
+ 'ai.action.cancelled',
26
+ 'ai.action.expired',
27
+ ]
28
+
29
+ describe('ai_assistant events module', () => {
30
+ it('declares the three FROZEN pending-action events under moduleId=ai_assistant', () => {
31
+ expect(eventsConfig.moduleId).toBe('ai_assistant')
32
+ const declaredIds = eventsConfig.events.map((event) => event.id).sort()
33
+ expect(declaredIds).toEqual([...FROZEN_EVENT_IDS].sort())
34
+ })
35
+
36
+ it('every declared event has category=system and entity=ai_pending_action', () => {
37
+ for (const event of eventsConfig.events) {
38
+ expect(event.category).toBe('system')
39
+ expect(event.entity).toBe('ai_pending_action')
40
+ expect(event.module).toBe('ai_assistant')
41
+ expect(typeof event.label).toBe('string')
42
+ expect(event.label.length).toBeGreaterThan(0)
43
+ }
44
+ })
45
+
46
+ describe('emitAiAssistantEvent', () => {
47
+ const emitSpy = jest.fn().mockResolvedValue(undefined)
48
+ let consoleErrorSpy: jest.SpyInstance
49
+
50
+ beforeEach(() => {
51
+ emitSpy.mockClear()
52
+ setGlobalEventBus({ emit: (id, payload, opts) => emitSpy(id, payload, opts) })
53
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
54
+ })
55
+
56
+ afterEach(() => {
57
+ consoleErrorSpy.mockRestore()
58
+ })
59
+
60
+ it('forwards ai.action.confirmed payloads to the global bus verbatim', async () => {
61
+ const payload: AiActionConfirmedPayload = {
62
+ pendingActionId: 'pa_1',
63
+ agentId: 'catalog.merchandising_assistant',
64
+ toolName: 'catalog.update_product',
65
+ status: 'confirmed',
66
+ tenantId: 'tenant-1',
67
+ organizationId: 'org-1',
68
+ userId: 'user-1',
69
+ resolvedByUserId: 'user-1',
70
+ resolvedAt: '2026-04-18T10:05:00.000Z',
71
+ executionResult: { recordId: 'p-1', commandName: 'catalog.product.update' },
72
+ }
73
+ await emitAiAssistantEvent(
74
+ 'ai.action.confirmed',
75
+ payload as unknown as Record<string, unknown>,
76
+ { persistent: true },
77
+ )
78
+ expect(emitSpy).toHaveBeenCalledTimes(1)
79
+ const [id, forwardedPayload, options] = emitSpy.mock.calls[0]
80
+ expect(id).toBe('ai.action.confirmed')
81
+ expect(forwardedPayload).toEqual(payload)
82
+ expect(options).toEqual({ persistent: true })
83
+ })
84
+
85
+ it('forwards ai.action.cancelled payloads to the global bus', async () => {
86
+ const payload: AiActionCancelledPayload = {
87
+ pendingActionId: 'pa_1',
88
+ agentId: 'catalog.merchandising_assistant',
89
+ toolName: 'catalog.update_product',
90
+ status: 'cancelled',
91
+ tenantId: 'tenant-1',
92
+ organizationId: 'org-1',
93
+ userId: 'user-1',
94
+ resolvedByUserId: 'user-1',
95
+ resolvedAt: '2026-04-18T10:05:00.000Z',
96
+ executionResult: {
97
+ error: { code: 'cancelled_by_user', message: 'Customer asked to abort' },
98
+ },
99
+ reason: 'Customer asked to abort',
100
+ }
101
+ await emitAiAssistantEvent(
102
+ 'ai.action.cancelled',
103
+ payload as unknown as Record<string, unknown>,
104
+ )
105
+ expect(emitSpy).toHaveBeenCalledTimes(1)
106
+ expect(emitSpy.mock.calls[0][0]).toBe('ai.action.cancelled')
107
+ expect(emitSpy.mock.calls[0][1]).toEqual(payload)
108
+ })
109
+
110
+ it('forwards ai.action.expired payloads to the global bus', async () => {
111
+ const payload: AiActionExpiredPayload = {
112
+ pendingActionId: 'pa_1',
113
+ agentId: 'catalog.merchandising_assistant',
114
+ toolName: 'catalog.update_product',
115
+ status: 'expired',
116
+ tenantId: 'tenant-1',
117
+ organizationId: 'org-1',
118
+ userId: null,
119
+ resolvedByUserId: null,
120
+ resolvedAt: '2026-04-18T10:05:00.000Z',
121
+ expiresAt: '2026-04-18T10:00:00.000Z',
122
+ expiredAt: '2026-04-18T10:05:00.000Z',
123
+ }
124
+ await emitAiAssistantEvent(
125
+ 'ai.action.expired',
126
+ payload as unknown as Record<string, unknown>,
127
+ )
128
+ expect(emitSpy).toHaveBeenCalledTimes(1)
129
+ expect(emitSpy.mock.calls[0][0]).toBe('ai.action.expired')
130
+ expect(emitSpy.mock.calls[0][1]).toEqual(payload)
131
+ })
132
+
133
+ it('logs an error and still forwards undeclared event ids (non-strict mode)', async () => {
134
+ await emitAiAssistantEvent(
135
+ // Deliberate cast: runtime test for undeclared-event path.
136
+ 'ai.action.nope' as unknown as AiAssistantEventId,
137
+ { pendingActionId: 'pa_1' },
138
+ )
139
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1)
140
+ const [message] = consoleErrorSpy.mock.calls[0]
141
+ expect(message).toContain('ai_assistant')
142
+ expect(message).toContain('ai.action.nope')
143
+ })
144
+ })
145
+ })