@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,132 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import {
3
+ findOneWithDecryption,
4
+ findWithDecryption,
5
+ } from '@open-mercato/shared/lib/encryption/find'
6
+ import { AiAgentPromptOverride } from '../entities'
7
+
8
+ export interface AiAgentPromptOverrideContext {
9
+ tenantId: string
10
+ organizationId?: string | null
11
+ userId?: string | null
12
+ }
13
+
14
+ export interface AiAgentPromptOverrideInput {
15
+ agentId: string
16
+ sections: Record<string, string>
17
+ notes?: string | null
18
+ }
19
+
20
+ /**
21
+ * Versioned prompt-override repository (Step 5.3).
22
+ *
23
+ * Every write produces a new row with a monotonically-increasing `version`
24
+ * scoped to `(tenantId, organizationId, agentId)`. We allocate the next
25
+ * version inside a transaction so two concurrent writers cannot collide on
26
+ * the same version number.
27
+ *
28
+ * Reads ALWAYS go through `findOneWithDecryption` / `findWithDecryption` —
29
+ * the `sections` column isn't encrypted today, but the repo sticks to the
30
+ * shared encrypted-read helpers so future GDPR-flagged columns are handled
31
+ * automatically.
32
+ */
33
+ export class AiAgentPromptOverrideRepository {
34
+ constructor(private readonly em: EntityManager) {}
35
+
36
+ async getLatest(
37
+ agentId: string,
38
+ ctx: AiAgentPromptOverrideContext,
39
+ ): Promise<AiAgentPromptOverride | null> {
40
+ if (!agentId || !ctx?.tenantId) return null
41
+ const row = await findOneWithDecryption<AiAgentPromptOverride>(
42
+ this.em,
43
+ AiAgentPromptOverride,
44
+ {
45
+ tenantId: ctx.tenantId,
46
+ organizationId: ctx.organizationId ?? null,
47
+ agentId,
48
+ } as any,
49
+ { orderBy: { version: 'desc' } as any },
50
+ { tenantId: ctx.tenantId ?? null, organizationId: ctx.organizationId ?? null },
51
+ )
52
+ return row ?? null
53
+ }
54
+
55
+ async listVersions(
56
+ agentId: string,
57
+ ctx: AiAgentPromptOverrideContext,
58
+ limit: number = 10,
59
+ ): Promise<AiAgentPromptOverride[]> {
60
+ if (!agentId || !ctx?.tenantId) return []
61
+ const capped = Math.max(1, Math.min(Math.floor(limit), 100))
62
+ const rows = await findWithDecryption<AiAgentPromptOverride>(
63
+ this.em,
64
+ AiAgentPromptOverride,
65
+ {
66
+ tenantId: ctx.tenantId,
67
+ organizationId: ctx.organizationId ?? null,
68
+ agentId,
69
+ } as any,
70
+ {
71
+ orderBy: { version: 'desc' } as any,
72
+ limit: capped,
73
+ },
74
+ { tenantId: ctx.tenantId ?? null, organizationId: ctx.organizationId ?? null },
75
+ )
76
+ return rows
77
+ }
78
+
79
+ async save(
80
+ input: AiAgentPromptOverrideInput,
81
+ ctx: AiAgentPromptOverrideContext,
82
+ ): Promise<AiAgentPromptOverride> {
83
+ if (!ctx?.tenantId) {
84
+ throw new Error('AiAgentPromptOverrideRepository.save requires tenantId')
85
+ }
86
+ if (!input?.agentId) {
87
+ throw new Error('AiAgentPromptOverrideRepository.save requires agentId')
88
+ }
89
+ const sanitizedSections = sanitizeSections(input.sections)
90
+ return this.em.transactional(async (tx) => {
91
+ const latest = await findOneWithDecryption<AiAgentPromptOverride>(
92
+ tx as unknown as EntityManager,
93
+ AiAgentPromptOverride,
94
+ {
95
+ tenantId: ctx.tenantId,
96
+ organizationId: ctx.organizationId ?? null,
97
+ agentId: input.agentId,
98
+ } as any,
99
+ { orderBy: { version: 'desc' } as any },
100
+ { tenantId: ctx.tenantId ?? null, organizationId: ctx.organizationId ?? null },
101
+ )
102
+ const nextVersion = (latest?.version ?? 0) + 1
103
+ const row = tx.create(AiAgentPromptOverride, {
104
+ tenantId: ctx.tenantId,
105
+ organizationId: ctx.organizationId ?? null,
106
+ agentId: input.agentId,
107
+ version: nextVersion,
108
+ sections: sanitizedSections,
109
+ notes: input.notes ?? null,
110
+ createdByUserId: ctx.userId ?? null,
111
+ } as unknown as AiAgentPromptOverride)
112
+ await tx.persist(row).flush()
113
+ return row
114
+ })
115
+ }
116
+ }
117
+
118
+ function sanitizeSections(
119
+ sections: Record<string, string> | null | undefined,
120
+ ): Record<string, string> {
121
+ if (!sections || typeof sections !== 'object') return {}
122
+ const out: Record<string, string> = {}
123
+ for (const [key, value] of Object.entries(sections)) {
124
+ if (typeof value !== 'string') continue
125
+ const trimmed = value.trim()
126
+ if (!trimmed) continue
127
+ out[key] = value
128
+ }
129
+ return out
130
+ }
131
+
132
+ export default AiAgentPromptOverrideRepository
@@ -0,0 +1,334 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import {
3
+ findOneWithDecryption,
4
+ findWithDecryption,
5
+ } from '@open-mercato/shared/lib/encryption/find'
6
+ import { AiPendingAction } from '../entities'
7
+ import {
8
+ AI_PENDING_ACTION_ALLOWED_TRANSITIONS,
9
+ AiPendingActionStateError,
10
+ resolveAiPendingActionTtlSeconds,
11
+ type AiPendingActionExecutionResult,
12
+ type AiPendingActionFailedRecord,
13
+ type AiPendingActionFieldDiff,
14
+ type AiPendingActionQueueMode,
15
+ type AiPendingActionRecordDiff,
16
+ type AiPendingActionStatus,
17
+ } from '../../lib/pending-action-types'
18
+
19
+ export interface AiPendingActionContext {
20
+ tenantId: string
21
+ organizationId?: string | null
22
+ userId?: string | null
23
+ }
24
+
25
+ export interface AiPendingActionCreateInput {
26
+ agentId: string
27
+ toolName: string
28
+ idempotencyKey: string
29
+ createdByUserId: string
30
+ normalizedInput: Record<string, unknown>
31
+ conversationId?: string | null
32
+ targetEntityType?: string | null
33
+ targetRecordId?: string | null
34
+ fieldDiff?: AiPendingActionFieldDiff[]
35
+ records?: AiPendingActionRecordDiff[] | null
36
+ sideEffectsSummary?: string | null
37
+ recordVersion?: string | null
38
+ attachmentIds?: string[]
39
+ queueMode?: AiPendingActionQueueMode
40
+ /** Optional explicit TTL in seconds; overrides the env/default TTL. */
41
+ ttlSeconds?: number
42
+ /** Optional explicit `now` for deterministic tests. */
43
+ now?: Date
44
+ }
45
+
46
+ export interface AiPendingActionSetStatusExtra {
47
+ resolvedByUserId?: string | null
48
+ executionResult?: AiPendingActionExecutionResult | null
49
+ failedRecords?: AiPendingActionFailedRecord[] | null
50
+ /** Optional explicit `now` for deterministic tests. */
51
+ now?: Date
52
+ }
53
+
54
+ /**
55
+ * Persistent store for the Phase 3 WS-C mutation approval gate (Step 5.5).
56
+ *
57
+ * Responsibilities:
58
+ * - Create new pending rows with a TTL-derived `expiresAt`, honoring
59
+ * idempotency within the window (same `idempotencyKey` returns the same
60
+ * row as long as it is still `pending`; any terminal state mints a new row).
61
+ * - Tenant-scoped lookups for the confirm/cancel/reconnect routes and the
62
+ * in-app UI's "open actions" list.
63
+ * - State-machine enforcement: `setStatus` rejects illegal transitions via
64
+ * {@link AiPendingActionStateError}. The runtime callers translate this
65
+ * to a 409 Conflict response.
66
+ * - `listExpired` for the cleanup worker (Step 5.12).
67
+ *
68
+ * Every read goes through `findOneWithDecryption` / `findWithDecryption`
69
+ * even though today no column is GDPR-flagged. This keeps the repo
70
+ * consistent with the rest of the module and preps for a future encrypted
71
+ * `normalizedInput` without a second refactor.
72
+ */
73
+ export class AiPendingActionRepository {
74
+ constructor(private readonly em: EntityManager) {}
75
+
76
+ async create(
77
+ input: AiPendingActionCreateInput,
78
+ ctx: AiPendingActionContext,
79
+ ): Promise<AiPendingAction> {
80
+ if (!ctx?.tenantId) {
81
+ throw new Error('AiPendingActionRepository.create requires tenantId')
82
+ }
83
+ if (!input?.agentId) {
84
+ throw new Error('AiPendingActionRepository.create requires agentId')
85
+ }
86
+ if (!input?.toolName) {
87
+ throw new Error('AiPendingActionRepository.create requires toolName')
88
+ }
89
+ if (!input?.idempotencyKey) {
90
+ throw new Error(
91
+ 'AiPendingActionRepository.create requires idempotencyKey',
92
+ )
93
+ }
94
+ if (!input?.createdByUserId) {
95
+ throw new Error(
96
+ 'AiPendingActionRepository.create requires createdByUserId',
97
+ )
98
+ }
99
+
100
+ const now = input.now ?? new Date()
101
+ const ttlSeconds = Math.max(
102
+ 1,
103
+ Math.floor(
104
+ typeof input.ttlSeconds === 'number' && Number.isFinite(input.ttlSeconds)
105
+ ? input.ttlSeconds
106
+ : resolveAiPendingActionTtlSeconds(),
107
+ ),
108
+ )
109
+ const expiresAt = new Date(now.getTime() + ttlSeconds * 1000)
110
+
111
+ return this.em.transactional(async (tx) => {
112
+ const existing = await findOneWithDecryption<AiPendingAction>(
113
+ tx as unknown as EntityManager,
114
+ AiPendingAction,
115
+ {
116
+ tenantId: ctx.tenantId,
117
+ organizationId: ctx.organizationId ?? null,
118
+ idempotencyKey: input.idempotencyKey,
119
+ } as any,
120
+ { orderBy: { createdAt: 'desc' } as any },
121
+ {
122
+ tenantId: ctx.tenantId ?? null,
123
+ organizationId: ctx.organizationId ?? null,
124
+ },
125
+ )
126
+ if (existing && existing.status === 'pending') {
127
+ return existing
128
+ }
129
+ // Terminal stale row would collide on the unique
130
+ // `(tenantId, organizationId, idempotencyKey)` constraint when we
131
+ // try to insert a fresh one with the same hash — and that exact
132
+ // collision happens whenever the operator clicks "Fix with AI"
133
+ // and the model retries the SAME tool with the SAME args. Remove
134
+ // the stale row first so a retry can always proceed; success rows
135
+ // stay (they represent a real, applied change), and failed /
136
+ // cancelled / expired rows are cleared because they're blocking
137
+ // exactly the recovery flow they were created to enable.
138
+ if (
139
+ existing &&
140
+ (existing.status === 'failed' ||
141
+ existing.status === 'cancelled' ||
142
+ existing.status === 'expired')
143
+ ) {
144
+ await tx.remove(existing).flush()
145
+ }
146
+ const row = tx.create(AiPendingAction, {
147
+ tenantId: ctx.tenantId,
148
+ organizationId: ctx.organizationId ?? null,
149
+ agentId: input.agentId,
150
+ toolName: input.toolName,
151
+ conversationId: input.conversationId ?? null,
152
+ targetEntityType: input.targetEntityType ?? null,
153
+ targetRecordId: input.targetRecordId ?? null,
154
+ normalizedInput: input.normalizedInput ?? {},
155
+ fieldDiff: Array.isArray(input.fieldDiff) ? input.fieldDiff : [],
156
+ records: normalizeRecords(input.records),
157
+ failedRecords: null,
158
+ sideEffectsSummary: input.sideEffectsSummary ?? null,
159
+ recordVersion: input.recordVersion ?? null,
160
+ attachmentIds: Array.isArray(input.attachmentIds)
161
+ ? input.attachmentIds
162
+ : [],
163
+ idempotencyKey: input.idempotencyKey,
164
+ createdByUserId: input.createdByUserId,
165
+ status: 'pending' as AiPendingActionStatus,
166
+ queueMode: (input.queueMode ?? 'inline') as AiPendingActionQueueMode,
167
+ executionResult: null,
168
+ createdAt: now,
169
+ expiresAt,
170
+ resolvedAt: null,
171
+ resolvedByUserId: null,
172
+ } as unknown as AiPendingAction)
173
+ await tx.persist(row).flush()
174
+ return row
175
+ })
176
+ }
177
+
178
+ async getById(
179
+ id: string,
180
+ ctx: AiPendingActionContext,
181
+ ): Promise<AiPendingAction | null> {
182
+ if (!id || !ctx?.tenantId) return null
183
+ const row = await findOneWithDecryption<AiPendingAction>(
184
+ this.em,
185
+ AiPendingAction,
186
+ {
187
+ id,
188
+ tenantId: ctx.tenantId,
189
+ organizationId: ctx.organizationId ?? null,
190
+ } as any,
191
+ {},
192
+ {
193
+ tenantId: ctx.tenantId ?? null,
194
+ organizationId: ctx.organizationId ?? null,
195
+ },
196
+ )
197
+ return row ?? null
198
+ }
199
+
200
+ async listPendingForAgent(
201
+ agentId: string,
202
+ ctx: AiPendingActionContext,
203
+ limit: number = 50,
204
+ ): Promise<AiPendingAction[]> {
205
+ if (!agentId || !ctx?.tenantId) return []
206
+ const capped = Math.max(1, Math.min(Math.floor(limit), 200))
207
+ const rows = await findWithDecryption<AiPendingAction>(
208
+ this.em,
209
+ AiPendingAction,
210
+ {
211
+ tenantId: ctx.tenantId,
212
+ organizationId: ctx.organizationId ?? null,
213
+ agentId,
214
+ status: 'pending',
215
+ } as any,
216
+ {
217
+ orderBy: { createdAt: 'desc' } as any,
218
+ limit: capped,
219
+ },
220
+ {
221
+ tenantId: ctx.tenantId ?? null,
222
+ organizationId: ctx.organizationId ?? null,
223
+ },
224
+ )
225
+ return rows
226
+ }
227
+
228
+ async setStatus(
229
+ id: string,
230
+ nextStatus: AiPendingActionStatus,
231
+ ctx: AiPendingActionContext,
232
+ extra?: AiPendingActionSetStatusExtra,
233
+ ): Promise<AiPendingAction> {
234
+ if (!ctx?.tenantId) {
235
+ throw new Error('AiPendingActionRepository.setStatus requires tenantId')
236
+ }
237
+ if (!id) {
238
+ throw new Error('AiPendingActionRepository.setStatus requires id')
239
+ }
240
+ return this.em.transactional(async (tx) => {
241
+ const existing = await findOneWithDecryption<AiPendingAction>(
242
+ tx as unknown as EntityManager,
243
+ AiPendingAction,
244
+ {
245
+ id,
246
+ tenantId: ctx.tenantId,
247
+ organizationId: ctx.organizationId ?? null,
248
+ } as any,
249
+ {},
250
+ {
251
+ tenantId: ctx.tenantId ?? null,
252
+ organizationId: ctx.organizationId ?? null,
253
+ },
254
+ )
255
+ if (!existing) {
256
+ throw new Error(`AiPendingAction not found: ${id}`)
257
+ }
258
+ if (existing.status === nextStatus) {
259
+ return existing
260
+ }
261
+ const allowed = AI_PENDING_ACTION_ALLOWED_TRANSITIONS[existing.status] ?? []
262
+ if (!allowed.includes(nextStatus)) {
263
+ throw new AiPendingActionStateError(existing.status, nextStatus)
264
+ }
265
+ const now = extra?.now ?? new Date()
266
+ existing.status = nextStatus
267
+ if (
268
+ nextStatus === 'confirmed' ||
269
+ nextStatus === 'cancelled' ||
270
+ nextStatus === 'expired' ||
271
+ nextStatus === 'failed'
272
+ ) {
273
+ existing.resolvedAt = existing.resolvedAt ?? now
274
+ if (extra && Object.prototype.hasOwnProperty.call(extra, 'resolvedByUserId')) {
275
+ existing.resolvedByUserId = extra.resolvedByUserId ?? null
276
+ } else if (nextStatus === 'expired') {
277
+ existing.resolvedByUserId = null
278
+ }
279
+ }
280
+ if (extra && Object.prototype.hasOwnProperty.call(extra, 'executionResult')) {
281
+ existing.executionResult = extra.executionResult ?? null
282
+ }
283
+ if (extra && Object.prototype.hasOwnProperty.call(extra, 'failedRecords')) {
284
+ existing.failedRecords = normalizeFailedRecords(extra.failedRecords)
285
+ }
286
+ await tx.persist(existing).flush()
287
+ return existing
288
+ })
289
+ }
290
+
291
+ async listExpired(
292
+ ctx: AiPendingActionContext,
293
+ now: Date,
294
+ limit: number = 100,
295
+ ): Promise<AiPendingAction[]> {
296
+ if (!ctx?.tenantId) return []
297
+ const capped = Math.max(1, Math.min(Math.floor(limit), 500))
298
+ const rows = await findWithDecryption<AiPendingAction>(
299
+ this.em,
300
+ AiPendingAction,
301
+ {
302
+ tenantId: ctx.tenantId,
303
+ organizationId: ctx.organizationId ?? null,
304
+ status: 'pending',
305
+ expiresAt: { $lt: now } as any,
306
+ } as any,
307
+ {
308
+ orderBy: { expiresAt: 'asc' } as any,
309
+ limit: capped,
310
+ },
311
+ {
312
+ tenantId: ctx.tenantId ?? null,
313
+ organizationId: ctx.organizationId ?? null,
314
+ },
315
+ )
316
+ return rows
317
+ }
318
+ }
319
+
320
+ function normalizeRecords(
321
+ records: AiPendingActionRecordDiff[] | null | undefined,
322
+ ): AiPendingActionRecordDiff[] | null {
323
+ if (!Array.isArray(records) || records.length === 0) return null
324
+ return records
325
+ }
326
+
327
+ function normalizeFailedRecords(
328
+ failed: AiPendingActionFailedRecord[] | null | undefined,
329
+ ): AiPendingActionFailedRecord[] | null {
330
+ if (!Array.isArray(failed) || failed.length === 0) return null
331
+ return failed
332
+ }
333
+
334
+ export default AiPendingActionRepository
@@ -0,0 +1,195 @@
1
+ import { AiAgentMutationPolicyOverrideRepository } from '../AiAgentMutationPolicyOverrideRepository'
2
+ import { AiAgentMutationPolicyOverride } from '../../entities'
3
+
4
+ type Row = {
5
+ id: string
6
+ tenantId: string
7
+ organizationId: string | null
8
+ agentId: string
9
+ mutationPolicy: string
10
+ notes: string | null
11
+ createdByUserId: string | null
12
+ createdAt: Date
13
+ updatedAt: Date
14
+ }
15
+
16
+ let idCounter = 0
17
+
18
+ function mockEm() {
19
+ const store: Row[] = []
20
+
21
+ const find = async (_entity: unknown, where: any): Promise<Row[]> => {
22
+ return store.filter((row) => {
23
+ if (where?.agentId && row.agentId !== where.agentId) return false
24
+ if (where?.tenantId && row.tenantId !== where.tenantId) return false
25
+ if (where && 'organizationId' in where) {
26
+ const expected = where.organizationId ?? null
27
+ if ((row.organizationId ?? null) !== expected) return false
28
+ }
29
+ return true
30
+ })
31
+ }
32
+
33
+ const em: any = {
34
+ find,
35
+ findOne: async (_entity: unknown, where: any, options?: any) => {
36
+ const rows = await find(_entity, where, options)
37
+ return rows[0] ?? null
38
+ },
39
+ create: (_entity: unknown, data: any) => {
40
+ idCounter += 1
41
+ const row: Row = {
42
+ id: `row-${idCounter}`,
43
+ tenantId: data.tenantId,
44
+ organizationId: data.organizationId ?? null,
45
+ agentId: data.agentId,
46
+ mutationPolicy: data.mutationPolicy,
47
+ notes: data.notes ?? null,
48
+ createdByUserId: data.createdByUserId ?? null,
49
+ createdAt: new Date(),
50
+ updatedAt: new Date(),
51
+ }
52
+ return row
53
+ },
54
+ persist: (row: Row) => {
55
+ em.__pendingPersist = row
56
+ return em
57
+ },
58
+ remove: (row: Row) => {
59
+ em.__pendingRemove = row
60
+ return em
61
+ },
62
+ flush: async () => {
63
+ if (em.__pendingPersist) {
64
+ const row = em.__pendingPersist as Row
65
+ const existingIndex = store.findIndex((r) => r.id === row.id)
66
+ if (existingIndex >= 0) store[existingIndex] = row
67
+ else store.push(row)
68
+ em.__pendingPersist = null
69
+ }
70
+ if (em.__pendingRemove) {
71
+ const row = em.__pendingRemove as Row
72
+ const index = store.findIndex((r) => r.id === row.id)
73
+ if (index >= 0) store.splice(index, 1)
74
+ em.__pendingRemove = null
75
+ }
76
+ },
77
+ transactional: async (fn: (tx: any) => Promise<unknown>) => fn(em),
78
+ __pendingPersist: null as Row | null,
79
+ __pendingRemove: null as Row | null,
80
+ __store: store,
81
+ }
82
+
83
+ return em
84
+ }
85
+
86
+ describe('AiAgentMutationPolicyOverrideRepository', () => {
87
+ it('set + get round-trip returns the persisted row', async () => {
88
+ const em = mockEm()
89
+ const repo = new AiAgentMutationPolicyOverrideRepository(em)
90
+ const ctx = { tenantId: 't1', organizationId: null }
91
+
92
+ await repo.set(
93
+ { agentId: 'catalog.assistant', mutationPolicy: 'read-only', notes: 'lock it down' },
94
+ ctx,
95
+ )
96
+
97
+ const row = await repo.get('catalog.assistant', ctx)
98
+ expect(row).not.toBeNull()
99
+ expect(row!.mutationPolicy).toBe('read-only')
100
+ expect(row!.notes).toBe('lock it down')
101
+ })
102
+
103
+ it('set replaces the existing row (one override per tenant+org+agent)', async () => {
104
+ const em = mockEm()
105
+ const repo = new AiAgentMutationPolicyOverrideRepository(em)
106
+ const ctx = { tenantId: 't1', organizationId: null }
107
+
108
+ await repo.set(
109
+ { agentId: 'catalog.assistant', mutationPolicy: 'read-only' },
110
+ ctx,
111
+ )
112
+ await repo.set(
113
+ { agentId: 'catalog.assistant', mutationPolicy: 'destructive-confirm-required' },
114
+ ctx,
115
+ )
116
+
117
+ const row = await repo.get('catalog.assistant', ctx)
118
+ expect(row).not.toBeNull()
119
+ expect(row!.mutationPolicy).toBe('destructive-confirm-required')
120
+ // Only one row exists for this tuple.
121
+ expect(em.__store.length).toBe(1)
122
+ })
123
+
124
+ it('clear returns null on subsequent get', async () => {
125
+ const em = mockEm()
126
+ const repo = new AiAgentMutationPolicyOverrideRepository(em)
127
+ const ctx = { tenantId: 't1', organizationId: null }
128
+
129
+ await repo.set(
130
+ { agentId: 'catalog.assistant', mutationPolicy: 'read-only' },
131
+ ctx,
132
+ )
133
+ const cleared = await repo.clear('catalog.assistant', ctx)
134
+ expect(cleared).toBe(true)
135
+
136
+ const row = await repo.get('catalog.assistant', ctx)
137
+ expect(row).toBeNull()
138
+ })
139
+
140
+ it('clear returns false when no override exists', async () => {
141
+ const em = mockEm()
142
+ const repo = new AiAgentMutationPolicyOverrideRepository(em)
143
+ const cleared = await repo.clear('catalog.assistant', {
144
+ tenantId: 't1',
145
+ organizationId: null,
146
+ })
147
+ expect(cleared).toBe(false)
148
+ })
149
+
150
+ it('scopes per tenant — get for a different tenant returns null', async () => {
151
+ const em = mockEm()
152
+ const repo = new AiAgentMutationPolicyOverrideRepository(em)
153
+
154
+ await repo.set(
155
+ { agentId: 'catalog.assistant', mutationPolicy: 'read-only' },
156
+ { tenantId: 't1', organizationId: null },
157
+ )
158
+ const rowA = await repo.get('catalog.assistant', {
159
+ tenantId: 't1',
160
+ organizationId: null,
161
+ })
162
+ const rowB = await repo.get('catalog.assistant', {
163
+ tenantId: 't2',
164
+ organizationId: null,
165
+ })
166
+ expect(rowA?.mutationPolicy).toBe('read-only')
167
+ expect(rowB).toBeNull()
168
+ })
169
+
170
+ it('throws when tenantId is missing on set', async () => {
171
+ const em = mockEm()
172
+ const repo = new AiAgentMutationPolicyOverrideRepository(em)
173
+ await expect(
174
+ repo.set(
175
+ { agentId: 'catalog.assistant', mutationPolicy: 'read-only' },
176
+ { tenantId: '', organizationId: null } as any,
177
+ ),
178
+ ).rejects.toThrow(/tenantId/)
179
+ })
180
+
181
+ it('returns an AiAgentMutationPolicyOverride-shaped payload', async () => {
182
+ const em = mockEm()
183
+ const repo = new AiAgentMutationPolicyOverrideRepository(em)
184
+ const saved = await repo.set(
185
+ { agentId: 'catalog.assistant', mutationPolicy: 'read-only', notes: 'note' },
186
+ { tenantId: 't1', organizationId: 'o1', userId: 'u1' },
187
+ )
188
+ expect(saved.agentId).toBe('catalog.assistant')
189
+ expect(saved.mutationPolicy).toBe('read-only')
190
+ expect(saved.organizationId).toBe('o1')
191
+ expect(saved.createdByUserId).toBe('u1')
192
+ expect(saved.notes).toBe('note')
193
+ void AiAgentMutationPolicyOverride
194
+ })
195
+ })