@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,285 @@
1
+ /**
2
+ * Step 3.13 — Phase 1 WS-C integration tests (tool-pack coverage).
3
+ *
4
+ * Re-exercises the cross-cutting shape of the `search.*`, `attachments.*`, and
5
+ * `meta.*` packs through the agent runtime so we can assert the invariants
6
+ * that per-pack unit tests cannot:
7
+ *
8
+ * - Every pack tool carries `requiredFeatures` (no unguarded read tool).
9
+ * - Tenant context is enforced by search/attachments handlers before any
10
+ * downstream resolve() call.
11
+ * - `meta.list_agents` degrades gracefully on an empty registry and honors
12
+ * RBAC filtering + super-admin bypass when wired through the same
13
+ * `listAgents()` API used by the chat dispatcher.
14
+ * - An agent that whitelists all three packs reaches the AI SDK with the
15
+ * full tool map and no extras.
16
+ *
17
+ * Customer/catalog tool packs are covered by their per-pack unit tests under
18
+ * `packages/core/src/modules/{customers,catalog}/__tests__/ai-tools/`. Those
19
+ * tests already verify tenant isolation, not-found shape, includeRelated
20
+ * aggregates, search_products routing, and suggest_price_adjustment's
21
+ * `isMutation: false` + `currentPrice: null` fallback. Re-testing them here
22
+ * would require cross-package Jest plumbing the ai-assistant harness does not
23
+ * currently support — documented as a deliberate scoping choice; see
24
+ * `${run_folder}/step-3.13-checks.md`.
25
+ */
26
+
27
+ import { z } from 'zod'
28
+ import type { AiAgentDefinition } from '../../lib/ai-agent-definition'
29
+ import type { AiToolDefinition } from '../../lib/types'
30
+
31
+ const streamTextMock = jest.fn()
32
+
33
+ jest.mock('ai', () => {
34
+ const actual = jest.requireActual('ai')
35
+ return {
36
+ ...actual,
37
+ streamText: (...args: unknown[]) => streamTextMock(...args),
38
+ }
39
+ })
40
+
41
+ const createModelMock = jest.fn(
42
+ (options: { modelId: string; apiKey: string }) => ({ id: options.modelId, apiKey: options.apiKey }),
43
+ )
44
+ const resolveApiKeyMock = jest.fn(() => 'test-api-key')
45
+
46
+ jest.mock('@open-mercato/shared/lib/ai/llm-provider-registry', () => ({
47
+ llmProviderRegistry: {
48
+ resolveFirstConfigured: () => ({
49
+ id: 'test-provider',
50
+ defaultModel: 'provider-default-model',
51
+ resolveApiKey: resolveApiKeyMock,
52
+ createModel: createModelMock,
53
+ }),
54
+ },
55
+ }))
56
+
57
+ import {
58
+ resetAgentRegistryForTests,
59
+ seedAgentRegistryForTests,
60
+ } from '../../lib/agent-registry'
61
+ import { toolRegistry, registerMcpTool } from '../../lib/tool-registry'
62
+ import { resolveAiAgentTools } from '../../lib/agent-tools'
63
+
64
+ import searchAiTools from '../../ai-tools/search-pack'
65
+ import attachmentsAiTools from '../../ai-tools/attachments-pack'
66
+ import metaAiTools from '../../ai-tools/meta-pack'
67
+
68
+ import { listAgents } from '../../lib/agent-registry'
69
+ import { hasRequiredFeatures } from '../../lib/auth'
70
+
71
+ function findTool(
72
+ pack: AiToolDefinition<any, any>[],
73
+ name: string,
74
+ ): AiToolDefinition<any, any> {
75
+ const tool = pack.find((entry) => entry.name === name)
76
+ if (!tool) throw new Error(`tool ${name} not registered in pack`)
77
+ return tool
78
+ }
79
+
80
+ function makeAgent(
81
+ overrides: Partial<AiAgentDefinition> & Pick<AiAgentDefinition, 'id' | 'moduleId'>,
82
+ ): AiAgentDefinition {
83
+ return {
84
+ label: `${overrides.id} label`,
85
+ description: `${overrides.id} description`,
86
+ systemPrompt: 'System prompt.',
87
+ allowedTools: [],
88
+ ...overrides,
89
+ }
90
+ }
91
+
92
+ function makeCtx(overrides: Partial<{
93
+ tenantId: string | null
94
+ organizationId: string | null
95
+ userId: string | null
96
+ userFeatures: string[]
97
+ isSuperAdmin: boolean
98
+ container: { resolve: (name: string) => unknown }
99
+ }> = {}) {
100
+ return {
101
+ tenantId: 'tenant-a',
102
+ organizationId: 'org-a',
103
+ userId: 'user-a',
104
+ container: { resolve: jest.fn() },
105
+ userFeatures: ['ai_assistant.view'],
106
+ isSuperAdmin: false,
107
+ ...overrides,
108
+ }
109
+ }
110
+
111
+ describe('WS-C integration — tool-pack coverage', () => {
112
+ beforeEach(() => {
113
+ jest.clearAllMocks()
114
+ resetAgentRegistryForTests()
115
+ toolRegistry.clear()
116
+ streamTextMock.mockImplementation(() => ({
117
+ toTextStreamResponse: jest.fn(() => new Response('ok')),
118
+ }))
119
+ })
120
+
121
+ afterAll(() => {
122
+ resetAgentRegistryForTests()
123
+ toolRegistry.clear()
124
+ })
125
+
126
+ describe('every read tool across the three packs carries requiredFeatures', () => {
127
+ it('search pack', () => {
128
+ for (const tool of searchAiTools) {
129
+ expect(tool.requiredFeatures).toBeDefined()
130
+ expect((tool.requiredFeatures as string[]).length).toBeGreaterThan(0)
131
+ }
132
+ })
133
+
134
+ it('attachments pack', () => {
135
+ for (const tool of attachmentsAiTools) {
136
+ expect(tool.requiredFeatures).toBeDefined()
137
+ expect((tool.requiredFeatures as string[]).length).toBeGreaterThan(0)
138
+ }
139
+ })
140
+
141
+ it('meta pack', () => {
142
+ for (const tool of metaAiTools) {
143
+ expect(tool.requiredFeatures).toEqual(['ai_assistant.view'])
144
+ }
145
+ })
146
+ })
147
+
148
+ describe('search.hybrid_search — tenant context enforcement', () => {
149
+ const tool = findTool(searchAiTools, 'search.hybrid_search')
150
+
151
+ it('throws when tenantId is missing (short-circuits before any search)', async () => {
152
+ await expect(
153
+ tool.handler({ q: 'anything' }, makeCtx({ tenantId: null }) as any),
154
+ ).rejects.toThrow(/tenant/i)
155
+ })
156
+
157
+ it('propagates tenantId + organizationId to the search service call', async () => {
158
+ const searchMock = jest.fn().mockResolvedValue([])
159
+ const ctx = makeCtx({
160
+ container: {
161
+ resolve: (name: string) => {
162
+ if (name === 'searchService') return { search: searchMock }
163
+ throw new Error(`Unknown registration: ${name}`)
164
+ },
165
+ },
166
+ })
167
+
168
+ await tool.handler({ q: 'hello', limit: 10 }, ctx as any)
169
+
170
+ expect(searchMock).toHaveBeenCalledTimes(1)
171
+ const [query, options] = searchMock.mock.calls[0] as [string, Record<string, unknown>]
172
+ expect(query).toBe('hello')
173
+ expect(options.tenantId).toBe('tenant-a')
174
+ expect(options.organizationId).toBe('org-a')
175
+ expect(options.limit).toBe(10)
176
+ })
177
+ })
178
+
179
+ describe('attachments.list_record_attachments — tenant context enforcement', () => {
180
+ const tool = findTool(attachmentsAiTools, 'attachments.list_record_attachments')
181
+
182
+ it('throws when tenantId is missing', async () => {
183
+ await expect(
184
+ tool.handler(
185
+ { entityType: 'customers:customer_person_profile', recordId: 'r1' },
186
+ makeCtx({ tenantId: null }) as any,
187
+ ),
188
+ ).rejects.toThrow(/tenant/i)
189
+ })
190
+ })
191
+
192
+ describe('meta.list_agents — RBAC + empty-registry graceful path', () => {
193
+ const tool = findTool(metaAiTools, 'meta.list_agents')
194
+
195
+ it('returns { agents: [], total: 0 } when the registry is empty', async () => {
196
+ const result = (await tool.handler({}, makeCtx() as any)) as {
197
+ agents: unknown[]
198
+ total: number
199
+ }
200
+ expect(result.agents).toEqual([])
201
+ expect(result.total).toBe(0)
202
+ })
203
+
204
+ it('filters by requiredFeatures using the same matcher the chat runtime uses', async () => {
205
+ seedAgentRegistryForTests([
206
+ makeAgent({ id: 'catalog.reader', moduleId: 'catalog', requiredFeatures: ['catalog.products.view'] }),
207
+ makeAgent({ id: 'catalog.writer', moduleId: 'catalog', requiredFeatures: ['catalog.products.manage'] }),
208
+ ])
209
+ const ctx = makeCtx({ userFeatures: ['ai_assistant.view', 'catalog.products.view'] })
210
+ const result = (await tool.handler({}, ctx as any)) as {
211
+ agents: Array<{ id: string }>
212
+ }
213
+ expect(result.agents.map((a) => a.id)).toEqual(['catalog.reader'])
214
+
215
+ // Parity with the chat runtime: the dispatcher uses the same helper.
216
+ const allowed = listAgents().filter((agent) =>
217
+ hasRequiredFeatures(agent.requiredFeatures ?? [], ctx.userFeatures, ctx.isSuperAdmin),
218
+ )
219
+ expect(allowed.map((a) => a.id)).toEqual(['catalog.reader'])
220
+ })
221
+
222
+ it('super-admin bypass: every agent is returned regardless of requiredFeatures', async () => {
223
+ seedAgentRegistryForTests([
224
+ makeAgent({ id: 'x.a', moduleId: 'x', requiredFeatures: ['x.secret'] }),
225
+ makeAgent({ id: 'y.b', moduleId: 'y', requiredFeatures: ['y.secret'] }),
226
+ ])
227
+ const ctx = makeCtx({ userFeatures: [], isSuperAdmin: true })
228
+ const result = (await tool.handler({}, ctx as any)) as {
229
+ agents: Array<{ id: string }>
230
+ }
231
+ expect(result.agents.map((a) => a.id).sort()).toEqual(['x.a', 'y.b'])
232
+ })
233
+ })
234
+
235
+ describe('agent whitelisting three packs reaches the SDK with the full tool map + no extras', () => {
236
+ it('resolveAiAgentTools picks only whitelisted tools across packs', async () => {
237
+ // Register one tool from each pack under a fresh registry.
238
+ for (const tool of [
239
+ findTool(searchAiTools, 'search.hybrid_search'),
240
+ findTool(attachmentsAiTools, 'attachments.list_record_attachments'),
241
+ findTool(metaAiTools, 'meta.list_agents'),
242
+ ]) {
243
+ registerMcpTool(tool as never)
244
+ }
245
+ // Plus an extra tool the agent does NOT whitelist; MUST NOT appear.
246
+ registerMcpTool({
247
+ name: 'catalog.update_product',
248
+ description: 'write tool',
249
+ inputSchema: z.object({}),
250
+ handler: async () => ({}),
251
+ isMutation: true,
252
+ requiredFeatures: ['catalog.products.manage'],
253
+ })
254
+
255
+ seedAgentRegistryForTests([
256
+ makeAgent({
257
+ id: 'multi.reader',
258
+ moduleId: 'multi',
259
+ allowedTools: [
260
+ 'search.hybrid_search',
261
+ 'attachments.list_record_attachments',
262
+ 'meta.list_agents',
263
+ ],
264
+ }),
265
+ ])
266
+
267
+ const resolved = await resolveAiAgentTools({
268
+ agentId: 'multi.reader',
269
+ authContext: {
270
+ tenantId: 'tenant-a',
271
+ organizationId: 'org-a',
272
+ userId: 'user-a',
273
+ features: ['*'],
274
+ isSuperAdmin: true,
275
+ },
276
+ })
277
+
278
+ const toolNames = Object.keys(resolved.tools).sort()
279
+ expect(toolNames).toEqual(
280
+ ['attachments__list_record_attachments', 'meta__list_agents', 'search__hybrid_search'].sort(),
281
+ )
282
+ expect(toolNames).not.toContain('catalog__update_product')
283
+ })
284
+ })
285
+ })
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Step 3.8 — `attachments.*` tool pack unit tests.
3
+ *
4
+ * Covers list happy/empty, read with/without extracted text, tenant
5
+ * isolation on reads, transfer happy path, and the mutation flag on
6
+ * `attachments.transfer_record_attachments`.
7
+ */
8
+ const findWithDecryptionMock = jest.fn()
9
+ const findOneWithDecryptionMock = jest.fn()
10
+
11
+ jest.mock('@open-mercato/shared/lib/encryption/find', () => ({
12
+ findWithDecryption: (...args: unknown[]) => findWithDecryptionMock(...args),
13
+ findOneWithDecryption: (...args: unknown[]) => findOneWithDecryptionMock(...args),
14
+ }))
15
+
16
+ jest.mock('@open-mercato/core/modules/attachments/data/entities', () => ({
17
+ Attachment: class AttachmentStub {},
18
+ }))
19
+
20
+ jest.mock('@open-mercato/core/modules/attachments/lib/metadata', () => ({
21
+ readAttachmentMetadata: (raw: unknown) => {
22
+ if (!raw || typeof raw !== 'object') return { tags: [], assignments: [] }
23
+ const value = raw as Record<string, unknown>
24
+ return {
25
+ tags: Array.isArray(value.tags) ? (value.tags as string[]) : [],
26
+ assignments: Array.isArray(value.assignments)
27
+ ? (value.assignments as Array<Record<string, unknown>>)
28
+ : [],
29
+ }
30
+ },
31
+ mergeAttachmentMetadata: (raw: unknown, patch: Record<string, unknown>) => {
32
+ const base =
33
+ raw && typeof raw === 'object' ? ({ ...(raw as Record<string, unknown>) }) : ({} as Record<string, unknown>)
34
+ return { ...base, ...patch }
35
+ },
36
+ }))
37
+
38
+ import attachmentsAiTools from '../attachments-pack'
39
+
40
+ function findTool(name: string) {
41
+ const tool = attachmentsAiTools.find((entry) => entry.name === name)
42
+ if (!tool) throw new Error(`tool ${name} missing`)
43
+ return tool
44
+ }
45
+
46
+ type Ctx = {
47
+ tenantId: string | null
48
+ organizationId: string | null
49
+ userId: string | null
50
+ container: { resolve: (name: string) => unknown }
51
+ userFeatures: string[]
52
+ isSuperAdmin: boolean
53
+ }
54
+
55
+ function makeCtx(overrides: Partial<Ctx> = {}): {
56
+ ctx: Ctx
57
+ em: { persist: jest.Mock; flush: jest.Mock }
58
+ } {
59
+ const em: any = {
60
+ persist: jest.fn(function (this: any) {
61
+ return em
62
+ }),
63
+ flush: jest.fn().mockResolvedValue(undefined),
64
+ }
65
+ const container = {
66
+ resolve: jest.fn((name: string) => {
67
+ if (name === 'em') return em
68
+ throw new Error(`unexpected resolve ${name}`)
69
+ }),
70
+ }
71
+ const ctx: Ctx = {
72
+ tenantId: 'tenant-1',
73
+ organizationId: 'org-1',
74
+ userId: 'user-1',
75
+ container,
76
+ userFeatures: ['attachments.view', 'attachments.manage'],
77
+ isSuperAdmin: false,
78
+ ...overrides,
79
+ }
80
+ return { ctx, em }
81
+ }
82
+
83
+ function makeRow(overrides: Record<string, unknown> = {}): Record<string, unknown> {
84
+ return {
85
+ id: 'att-1',
86
+ entityId: 'customers:customer_person_profile',
87
+ recordId: 'person-1',
88
+ fileName: 'passport.pdf',
89
+ mimeType: 'application/pdf',
90
+ fileSize: 2048,
91
+ partitionCode: 'default',
92
+ tenantId: 'tenant-1',
93
+ organizationId: 'org-1',
94
+ storageMetadata: null,
95
+ content: null,
96
+ createdAt: new Date('2026-04-18T10:00:00Z'),
97
+ ...overrides,
98
+ }
99
+ }
100
+
101
+ beforeEach(() => {
102
+ findWithDecryptionMock.mockReset()
103
+ findOneWithDecryptionMock.mockReset()
104
+ })
105
+
106
+ describe('attachments.list_record_attachments', () => {
107
+ const tool = findTool('attachments.list_record_attachments')
108
+
109
+ it('returns metadata-only items scoped by tenant + organization', async () => {
110
+ findWithDecryptionMock.mockResolvedValue([
111
+ makeRow(),
112
+ makeRow({ id: 'att-2', fileName: 'photo.jpg', mimeType: 'image/jpeg', fileSize: 512 }),
113
+ ])
114
+ const { ctx } = makeCtx()
115
+ const result = (await tool.handler(
116
+ { entityType: 'customers:customer_person_profile', recordId: 'person-1' },
117
+ ctx as any,
118
+ )) as Record<string, unknown>
119
+ expect(findWithDecryptionMock).toHaveBeenCalledTimes(1)
120
+ const callArgs = findWithDecryptionMock.mock.calls[0]
121
+ expect(callArgs[2]).toMatchObject({
122
+ entityId: 'customers:customer_person_profile',
123
+ recordId: 'person-1',
124
+ tenantId: 'tenant-1',
125
+ organizationId: 'org-1',
126
+ })
127
+ expect(result.total).toBe(2)
128
+ const items = result.items as Array<Record<string, unknown>>
129
+ expect(items[0]).toMatchObject({
130
+ id: 'att-1',
131
+ fileName: 'passport.pdf',
132
+ mediaType: 'application/pdf',
133
+ size: 2048,
134
+ })
135
+ expect(items[0]).not.toHaveProperty('url')
136
+ expect(items[0]).not.toHaveProperty('signedUrl')
137
+ })
138
+
139
+ it('handles empty record gracefully', async () => {
140
+ findWithDecryptionMock.mockResolvedValue([])
141
+ const { ctx } = makeCtx()
142
+ const result = (await tool.handler(
143
+ { entityType: 'customers:customer_person_profile', recordId: 'nobody' },
144
+ ctx as any,
145
+ )) as Record<string, unknown>
146
+ expect(result.total).toBe(0)
147
+ expect(result.items).toEqual([])
148
+ })
149
+
150
+ it('throws when tenant context is missing', async () => {
151
+ const { ctx } = makeCtx({ tenantId: null })
152
+ await expect(
153
+ tool.handler(
154
+ { entityType: 'customers:customer_person_profile', recordId: 'person-1' },
155
+ ctx as any,
156
+ ),
157
+ ).rejects.toThrow(/Tenant context/)
158
+ })
159
+ })
160
+
161
+ describe('attachments.read_attachment', () => {
162
+ const tool = findTool('attachments.read_attachment')
163
+
164
+ it('returns extracted text only when includeExtractedText is true', async () => {
165
+ const row = makeRow({ content: 'OCR TEXT HERE', id: 'att-77' })
166
+ findOneWithDecryptionMock.mockResolvedValueOnce(row).mockResolvedValueOnce(row)
167
+ const { ctx } = makeCtx()
168
+ const withoutText = (await tool.handler(
169
+ { attachmentId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8' },
170
+ ctx as any,
171
+ )) as Record<string, unknown>
172
+ expect(withoutText.found).toBe(true)
173
+ expect(withoutText.hasExtractedText).toBe(true)
174
+ expect(withoutText.extractedText).toBeNull()
175
+
176
+ const withText = (await tool.handler(
177
+ { attachmentId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', includeExtractedText: true },
178
+ ctx as any,
179
+ )) as Record<string, unknown>
180
+ expect(withText.extractedText).toBe('OCR TEXT HERE')
181
+ })
182
+
183
+ it('returns { found: false } when the attachment is not visible to the tenant', async () => {
184
+ findOneWithDecryptionMock.mockResolvedValueOnce(null)
185
+ const { ctx } = makeCtx()
186
+ const result = (await tool.handler(
187
+ { attachmentId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8' },
188
+ ctx as any,
189
+ )) as Record<string, unknown>
190
+ expect(result.found).toBe(false)
191
+ })
192
+
193
+ it('always scopes the query by tenantId (and organization when set)', async () => {
194
+ findOneWithDecryptionMock.mockResolvedValue(null)
195
+ const { ctx } = makeCtx({ tenantId: 'tenant-X', organizationId: 'org-X' })
196
+ await tool.handler(
197
+ { attachmentId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8' },
198
+ ctx as any,
199
+ )
200
+ const args = findOneWithDecryptionMock.mock.calls[0]
201
+ expect(args[2]).toMatchObject({
202
+ id: '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
203
+ tenantId: 'tenant-X',
204
+ organizationId: 'org-X',
205
+ })
206
+ })
207
+ })
208
+
209
+ describe('attachments.transfer_record_attachments', () => {
210
+ const tool = findTool('attachments.transfer_record_attachments')
211
+
212
+ it('declares isMutation=true', () => {
213
+ expect(tool.isMutation).toBe(true)
214
+ })
215
+
216
+ it('requires attachments.manage feature', () => {
217
+ expect(tool.requiredFeatures).toEqual(['attachments.manage'])
218
+ })
219
+
220
+ it('moves matching attachments to the target record and persists', async () => {
221
+ const row = makeRow({
222
+ id: 'att-1',
223
+ recordId: 'draft-1',
224
+ storageMetadata: {
225
+ assignments: [
226
+ { type: 'customers:customer_person_profile', id: 'draft-1' },
227
+ ],
228
+ },
229
+ })
230
+ findWithDecryptionMock.mockResolvedValue([row])
231
+ const { ctx, em } = makeCtx()
232
+ const result = (await tool.handler(
233
+ {
234
+ fromEntityType: 'customers:customer_person_profile',
235
+ fromRecordId: 'draft-1',
236
+ toEntityType: 'customers:customer_person_profile',
237
+ toRecordId: 'person-9',
238
+ },
239
+ ctx as any,
240
+ )) as Record<string, unknown>
241
+ expect(result.transferred).toBe(1)
242
+ expect((row as Record<string, unknown>).recordId).toBe('person-9')
243
+ const metadata = (row as Record<string, unknown>).storageMetadata as {
244
+ assignments: Array<{ type: string; id: string }>
245
+ }
246
+ expect(metadata.assignments[0]).toMatchObject({
247
+ type: 'customers:customer_person_profile',
248
+ id: 'person-9',
249
+ })
250
+ expect(em.persist).toHaveBeenCalledWith([row])
251
+ expect(em.flush).toHaveBeenCalled()
252
+ })
253
+
254
+ it('returns transferred: 0 when no matching attachments exist', async () => {
255
+ findWithDecryptionMock.mockResolvedValue([])
256
+ const { ctx, em } = makeCtx()
257
+ const result = (await tool.handler(
258
+ {
259
+ fromEntityType: 'customers:customer_person_profile',
260
+ fromRecordId: 'draft-x',
261
+ toEntityType: 'customers:customer_person_profile',
262
+ toRecordId: 'person-9',
263
+ },
264
+ ctx as any,
265
+ )) as Record<string, unknown>
266
+ expect(result.transferred).toBe(0)
267
+ expect(em.persist).not.toHaveBeenCalled()
268
+ expect(em.flush).not.toHaveBeenCalled()
269
+ })
270
+
271
+ it('rejects cross-entity transfers', async () => {
272
+ const { ctx } = makeCtx()
273
+ await expect(
274
+ tool.handler(
275
+ {
276
+ fromEntityType: 'a:b',
277
+ fromRecordId: 'r1',
278
+ toEntityType: 'c:d',
279
+ toRecordId: 'r2',
280
+ },
281
+ ctx as any,
282
+ ),
283
+ ).rejects.toThrow(/fromEntityType and toEntityType to match/)
284
+ })
285
+
286
+ it('respects caller tenant + organization when querying', async () => {
287
+ findWithDecryptionMock.mockResolvedValue([])
288
+ const { ctx } = makeCtx({ tenantId: 'tenant-Z', organizationId: 'org-Z' })
289
+ await tool.handler(
290
+ {
291
+ fromEntityType: 'x:y',
292
+ fromRecordId: 'r1',
293
+ toEntityType: 'x:y',
294
+ toRecordId: 'r2',
295
+ },
296
+ ctx as any,
297
+ )
298
+ const args = findWithDecryptionMock.mock.calls[0]
299
+ expect(args[2]).toMatchObject({
300
+ entityId: 'x:y',
301
+ recordId: 'r1',
302
+ tenantId: 'tenant-Z',
303
+ organizationId: 'org-Z',
304
+ })
305
+ })
306
+ })
307
+
308
+ describe('attachments-pack tool surface', () => {
309
+ it('exports the three expected tools with correct flags', () => {
310
+ const names = attachmentsAiTools.map((tool) => tool.name)
311
+ expect(names).toEqual([
312
+ 'attachments.list_record_attachments',
313
+ 'attachments.read_attachment',
314
+ 'attachments.transfer_record_attachments',
315
+ ])
316
+ const readOnly = attachmentsAiTools.filter((tool) => tool.isMutation !== true)
317
+ expect(readOnly.map((tool) => tool.name)).toEqual([
318
+ 'attachments.list_record_attachments',
319
+ 'attachments.read_attachment',
320
+ ])
321
+ })
322
+ })