@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,115 @@
1
+ import { test, expect, request as playwrightRequest } from '@playwright/test';
2
+ import { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api';
3
+
4
+ /**
5
+ * TC-AI-002: AI agent dispatcher policy gate (Step 3.13 / Phase 3 WS-C).
6
+ *
7
+ * Exercises the HTTP surface of the Step 3.2 / 3.3 runtime policy gate end-to-end
8
+ * against the authenticated superadmin session. The dispatcher is
9
+ * `POST /api/ai_assistant/ai/chat?agent=<module>.<agent>` (see
10
+ * `packages/ai-assistant/src/modules/ai_assistant/lib/agent-transport.ts`).
11
+ *
12
+ * Asserted:
13
+ * - Unknown agent id -> 404 + `{ code: 'agent_unknown' }` — verified against
14
+ * `packages/ai-assistant/src/modules/ai_assistant/lib/agent-policy.ts`.
15
+ * - Malformed agent query param -> 400 + `{ code: 'validation_error' }`.
16
+ * - Missing agent query param -> 400 + `{ code: 'validation_error' }`.
17
+ * - Unauthenticated caller -> 401 + `{ code: 'unauthenticated' }`.
18
+ *
19
+ * The chat dispatcher requires `ai_assistant.view` — superadmin always carries it.
20
+ * The "forbidden agent" branch (`agent_features_denied`) is exercised at the
21
+ * runtime-helper layer by the Jest integration suite at
22
+ * `packages/ai-assistant/src/modules/ai_assistant/__tests__/integration/
23
+ * ws-c-policy-and-tools.test.ts` because no seeded non-superadmin role has the
24
+ * shape required to create a deterministic forbidden-agent fixture without
25
+ * touching ACL tables.
26
+ */
27
+ test.describe('TC-AI-002: AI agent dispatcher policy gate', () => {
28
+ const chatPath = '/api/ai_assistant/ai/chat';
29
+
30
+ test('unknown agent returns 404 agent_unknown', async ({ request }) => {
31
+ const token = await getAuthToken(request, 'superadmin');
32
+
33
+ const response = await request.fetch(`${chatPath}?agent=does.not_exist`, {
34
+ method: 'POST',
35
+ headers: {
36
+ Authorization: `Bearer ${token}`,
37
+ 'Content-Type': 'application/json',
38
+ },
39
+ data: JSON.stringify({
40
+ messages: [{ role: 'user', content: 'hello' }],
41
+ }),
42
+ });
43
+
44
+ expect(response.status()).toBe(404);
45
+ const body = (await response.json()) as { code?: unknown; error?: unknown };
46
+ expect(body.code).toBe('agent_unknown');
47
+ });
48
+
49
+ test('malformed agent query param returns 400 validation_error', async ({ request }) => {
50
+ const token = await getAuthToken(request, 'superadmin');
51
+
52
+ const response = await request.fetch(`${chatPath}?agent=BadAgent`, {
53
+ method: 'POST',
54
+ headers: {
55
+ Authorization: `Bearer ${token}`,
56
+ 'Content-Type': 'application/json',
57
+ },
58
+ data: JSON.stringify({
59
+ messages: [{ role: 'user', content: 'hello' }],
60
+ }),
61
+ });
62
+
63
+ expect(response.status()).toBe(400);
64
+ const body = (await response.json()) as { code?: unknown };
65
+ expect(body.code).toBe('validation_error');
66
+ });
67
+
68
+ test('missing agent query param returns 400 validation_error', async ({ request }) => {
69
+ const token = await getAuthToken(request, 'superadmin');
70
+
71
+ const response = await request.fetch(chatPath, {
72
+ method: 'POST',
73
+ headers: {
74
+ Authorization: `Bearer ${token}`,
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ data: JSON.stringify({
78
+ messages: [{ role: 'user', content: 'hello' }],
79
+ }),
80
+ });
81
+
82
+ expect(response.status()).toBe(400);
83
+ const body = (await response.json()) as { code?: unknown };
84
+ expect(body.code).toBe('validation_error');
85
+ });
86
+
87
+ test('unauthenticated caller returns 401', async ({ baseURL }) => {
88
+ // Use a fresh request context with no cookies — shared `request` fixture
89
+ // accumulates the superadmin session cookie from prior getAuthToken calls.
90
+ const context = await playwrightRequest.newContext({ baseURL });
91
+ try {
92
+ const response = await context.fetch(`${chatPath}?agent=does.not_exist`, {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ },
97
+ data: JSON.stringify({
98
+ messages: [{ role: 'user', content: 'hello' }],
99
+ }),
100
+ });
101
+
102
+ expect(response.status()).toBe(401);
103
+ // The framework-level `requireAuth` guard (page metadata) short-circuits
104
+ // unauthenticated POSTs before the route handler runs, so the response
105
+ // carries the framework's `{ error: 'Unauthorized' }` envelope rather
106
+ // than the route-local `{ code: 'unauthenticated' }` envelope. Either
107
+ // shape satisfies the contract "unauth callers cannot invoke the
108
+ // dispatcher"; we accept whichever one the framework actually returns.
109
+ const body = (await response.json()) as { code?: unknown; error?: unknown };
110
+ expect(body.code === 'unauthenticated' || body.error === 'Unauthorized').toBe(true);
111
+ } finally {
112
+ await context.dispose();
113
+ }
114
+ });
115
+ });
@@ -0,0 +1,574 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';
3
+
4
+ /**
5
+ * TC-AI-AGENT-SETTINGS-005: AI Agent settings page smoke (Step 4.5 / Phase 2 WS-B).
6
+ *
7
+ * Covers the backend agent settings route at
8
+ * `/backend/config/ai-assistant/agents`. The page is guarded by
9
+ * `ai_assistant.settings.manage`; superadmin always holds it. The agent
10
+ * registry is empty by default in CI (first production agent lands in Step 4.7),
11
+ * so the primary assertion is that the empty-state renders.
12
+ *
13
+ * We also assert that an unauthenticated visit redirects to `/login`.
14
+ * The `POST /api/ai_assistant/ai/agents/:agentId/prompt-override` route is
15
+ * stubbed so the "save overrides" path is exercisable end-to-end when an
16
+ * agent happens to be present in the registry.
17
+ */
18
+ test.describe('TC-AI-AGENT-SETTINGS-005: AI Agent settings', () => {
19
+ const settingsPath = '/backend/config/ai-assistant/agents';
20
+
21
+ test('page loads and renders empty-state for superadmin when no agents are registered', async ({
22
+ page,
23
+ }) => {
24
+ await login(page, 'superadmin');
25
+
26
+ await page.route('**/api/ai_assistant/ai/agents', async (route) => {
27
+ await route.fulfill({
28
+ status: 200,
29
+ contentType: 'application/json',
30
+ body: JSON.stringify({ agents: [], total: 0 }),
31
+ });
32
+ });
33
+
34
+ await page.route('**/api/ai_assistant/ai/agents/*/prompt-override', async (route) => {
35
+ await route.fulfill({
36
+ status: 200,
37
+ contentType: 'application/json',
38
+ body: JSON.stringify({
39
+ pending: true,
40
+ agentId: 'stub.stub',
41
+ message: 'Persistence lands in Phase 3 Step 5.3.',
42
+ }),
43
+ });
44
+ });
45
+
46
+ await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });
47
+
48
+ const emptyState = page.getByText(/No AI agents are registered/i).first();
49
+ const container = page.locator('[data-ai-agent-settings]');
50
+ const loadError = page.locator('[data-ai-agent-settings-error]');
51
+
52
+ await expect(emptyState.or(container).or(loadError)).toBeVisible({ timeout: 15_000 });
53
+
54
+ if (await emptyState.isVisible().catch(() => false)) {
55
+ await expect(emptyState).toBeVisible();
56
+ } else if (await container.isVisible().catch(() => false)) {
57
+ // Registry non-empty in this env — the detail panel should have rendered.
58
+ const picker = page.locator('[data-ai-agent-settings-picker]');
59
+ await expect(picker).toBeVisible();
60
+ } else {
61
+ await expect(loadError).toBeVisible();
62
+ }
63
+ });
64
+
65
+ test('unauthenticated visit redirects to login', async ({ browser }) => {
66
+ const context = await browser.newContext();
67
+ const page = await context.newPage();
68
+ try {
69
+ await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });
70
+ await page.waitForURL(/\/login/, { timeout: 15_000 });
71
+ expect(page.url()).toMatch(/\/login/);
72
+ } finally {
73
+ await context.close();
74
+ }
75
+ });
76
+
77
+ test('Cmd/Ctrl+Enter inside a prompt-override textarea triggers the placeholder save', async ({
78
+ page,
79
+ }) => {
80
+ // CI cold-compile of the settings page + stubbed agents fetch can exceed
81
+ // the default 20s test timeout; give this test 2 minutes.
82
+ test.setTimeout(120_000);
83
+ await login(page, 'superadmin');
84
+
85
+ // Non-empty registry so the detail panel renders and the textareas exist.
86
+ await page.route('**/api/ai_assistant/ai/agents', async (route) => {
87
+ await route.fulfill({
88
+ status: 200,
89
+ contentType: 'application/json',
90
+ body: JSON.stringify({
91
+ agents: [
92
+ {
93
+ id: 'customers.assistant',
94
+ moduleId: 'customers',
95
+ label: 'Customers assistant',
96
+ description: 'Answers questions about customer records.',
97
+ systemPrompt: 'You are a customers assistant.',
98
+ executionMode: 'chat',
99
+ mutationPolicy: 'read-only',
100
+ readOnly: true,
101
+ maxSteps: 10,
102
+ allowedTools: ['customers.list_people'],
103
+ tools: [
104
+ {
105
+ name: 'customers.list_people',
106
+ displayName: 'List people',
107
+ isMutation: false,
108
+ registered: true,
109
+ },
110
+ ],
111
+ requiredFeatures: [],
112
+ acceptedMediaTypes: [],
113
+ hasOutputSchema: false,
114
+ },
115
+ ],
116
+ total: 1,
117
+ }),
118
+ });
119
+ });
120
+
121
+ let saveCalls = 0;
122
+ await page.route('**/api/ai_assistant/ai/agents/*/prompt-override', async (route) => {
123
+ const method = route.request().method();
124
+ if (method === 'GET') {
125
+ await route.fulfill({
126
+ status: 200,
127
+ contentType: 'application/json',
128
+ body: JSON.stringify({
129
+ agentId: 'customers.assistant',
130
+ override: null,
131
+ versions: [],
132
+ }),
133
+ });
134
+ return;
135
+ }
136
+ saveCalls += 1;
137
+ await route.fulfill({
138
+ status: 200,
139
+ contentType: 'application/json',
140
+ body: JSON.stringify({
141
+ pending: true,
142
+ agentId: 'customers.assistant',
143
+ message: 'Persistence lands in Phase 3 Step 5.3.',
144
+ }),
145
+ });
146
+ });
147
+
148
+ // Also stub the mutation-policy GET so selecting the fake agent doesn't
149
+ // 404 into an error panel on CI (the real route doesn't know our fake id).
150
+ await page.route('**/api/ai_assistant/ai/agents/*/mutation-policy', async (route) => {
151
+ await route.fulfill({
152
+ status: 200,
153
+ contentType: 'application/json',
154
+ body: JSON.stringify({
155
+ agentId: 'customers.assistant',
156
+ codeDeclared: 'read-only',
157
+ override: null,
158
+ }),
159
+ });
160
+ });
161
+
162
+ await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });
163
+
164
+ const container = page.locator('[data-ai-agent-settings]');
165
+ await expect(container).toBeVisible({ timeout: 60_000 });
166
+
167
+ // Flip the `role` section into override mode so a textarea is present.
168
+ const toggle = page.locator('[data-ai-agent-prompt-toggle="role"]');
169
+ await toggle.click({ timeout: 30_000 });
170
+
171
+ const textarea = page.locator('[data-ai-agent-prompt-override="role"]');
172
+ await expect(textarea).toBeVisible();
173
+ await textarea.fill('Custom role text.');
174
+
175
+ // Cmd+Enter (mac) / Ctrl+Enter (others) — Playwright supports both.
176
+ await textarea.press('Meta+Enter');
177
+ await page.waitForTimeout(250);
178
+ if (saveCalls === 0) {
179
+ await textarea.press('Control+Enter');
180
+ await page.waitForTimeout(250);
181
+ }
182
+
183
+ expect(saveCalls).toBeGreaterThanOrEqual(1);
184
+ });
185
+
186
+ test('selecting an agent renders detail panel with meta badges, tool toggles, and attachment policy', async ({
187
+ page,
188
+ }) => {
189
+ test.setTimeout(120_000);
190
+ await login(page, 'superadmin');
191
+
192
+ await page.route('**/api/ai_assistant/ai/agents', async (route) => {
193
+ await route.fulfill({
194
+ status: 200,
195
+ contentType: 'application/json',
196
+ body: JSON.stringify({
197
+ agents: [
198
+ {
199
+ id: 'customers.account_assistant',
200
+ moduleId: 'customers',
201
+ label: 'Customers account assistant',
202
+ description: 'Answers questions about customer records.',
203
+ systemPrompt: 'You are a helpful read-only customers assistant.',
204
+ executionMode: 'chat',
205
+ mutationPolicy: 'read-only',
206
+ readOnly: true,
207
+ maxSteps: 10,
208
+ allowedTools: ['customers.list_people', 'customers.get_person'],
209
+ tools: [
210
+ {
211
+ name: 'customers.list_people',
212
+ displayName: 'List people',
213
+ isMutation: false,
214
+ registered: true,
215
+ },
216
+ {
217
+ name: 'customers.get_person',
218
+ displayName: 'Get person',
219
+ isMutation: false,
220
+ registered: true,
221
+ },
222
+ ],
223
+ requiredFeatures: ['customers.people.view'],
224
+ acceptedMediaTypes: ['image/png', 'application/pdf'],
225
+ hasOutputSchema: false,
226
+ },
227
+ ],
228
+ total: 1,
229
+ }),
230
+ });
231
+ });
232
+
233
+ await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });
234
+
235
+ const container = page.locator('[data-ai-agent-settings]');
236
+ await expect(container).toBeVisible({ timeout: 60_000 });
237
+
238
+ // Detail panel renders for the first (only) agent.
239
+ const detailPanel = page.locator('[data-ai-agent-detail="customers.account_assistant"]');
240
+ await expect(detailPanel).toBeVisible();
241
+
242
+ // Tool rows render for every declared tool, with disabled switches.
243
+ const listRow = page.locator('[data-ai-agent-tool-row="customers.list_people"]');
244
+ await expect(listRow).toBeVisible();
245
+ const listSwitch = page.locator('[data-ai-agent-tool-switch="customers.list_people"]');
246
+ // Radix `Switch` primitives surface the disabled state via
247
+ // `aria-disabled`/`data-disabled` rather than the native attribute,
248
+ // so assert both. Either one is sufficient evidence the switch is
249
+ // read-only in Phase 2.
250
+ const ariaDisabled = await listSwitch.getAttribute('aria-disabled');
251
+ const dataDisabled = await listSwitch.getAttribute('data-disabled');
252
+ const disabledAttr = await listSwitch.getAttribute('disabled');
253
+ expect(
254
+ ariaDisabled === 'true' || dataDisabled !== null || disabledAttr !== null,
255
+ ).toBe(true);
256
+
257
+ // Attachment policy badges surface for each declared media type.
258
+ const pngBadge = page.locator('[data-ai-agent-attachment-badge="image/png"]');
259
+ await expect(pngBadge).toBeVisible();
260
+ const pdfBadge = page.locator('[data-ai-agent-attachment-badge="application/pdf"]');
261
+ await expect(pdfBadge).toBeVisible();
262
+ });
263
+
264
+ test('saving a valid override surfaces the new version in the history panel (Step 5.3)', async ({
265
+ page,
266
+ }) => {
267
+ test.setTimeout(120_000);
268
+ await login(page, 'superadmin');
269
+
270
+ await page.route('**/api/ai_assistant/ai/agents', async (route) => {
271
+ await route.fulfill({
272
+ status: 200,
273
+ contentType: 'application/json',
274
+ body: JSON.stringify({
275
+ agents: [
276
+ {
277
+ id: 'customers.account_assistant',
278
+ moduleId: 'customers',
279
+ label: 'Customers account assistant',
280
+ description: 'Answers questions about customer records.',
281
+ systemPrompt: 'You are a read-only customers assistant.',
282
+ executionMode: 'chat',
283
+ mutationPolicy: 'read-only',
284
+ readOnly: true,
285
+ maxSteps: 10,
286
+ allowedTools: [],
287
+ tools: [],
288
+ requiredFeatures: [],
289
+ acceptedMediaTypes: [],
290
+ hasOutputSchema: false,
291
+ },
292
+ ],
293
+ total: 1,
294
+ }),
295
+ });
296
+ });
297
+
298
+ let overrideState: { version: number; sections: Record<string, string> } | null = null;
299
+ await page.route('**/api/ai_assistant/ai/agents/*/prompt-override', async (route) => {
300
+ const request = route.request();
301
+ if (request.method() === 'GET') {
302
+ await route.fulfill({
303
+ status: 200,
304
+ contentType: 'application/json',
305
+ body: JSON.stringify(
306
+ overrideState
307
+ ? {
308
+ agentId: 'customers.account_assistant',
309
+ override: {
310
+ id: 'row-1',
311
+ agentId: 'customers.account_assistant',
312
+ version: overrideState.version,
313
+ sections: overrideState.sections,
314
+ notes: null,
315
+ createdByUserId: null,
316
+ createdAt: new Date().toISOString(),
317
+ updatedAt: new Date().toISOString(),
318
+ },
319
+ versions: [
320
+ {
321
+ id: 'row-1',
322
+ agentId: 'customers.account_assistant',
323
+ version: overrideState.version,
324
+ sections: overrideState.sections,
325
+ notes: null,
326
+ createdByUserId: null,
327
+ createdAt: new Date().toISOString(),
328
+ updatedAt: new Date().toISOString(),
329
+ },
330
+ ],
331
+ }
332
+ : {
333
+ agentId: 'customers.account_assistant',
334
+ override: null,
335
+ versions: [],
336
+ },
337
+ ),
338
+ });
339
+ return;
340
+ }
341
+ const body = JSON.parse(request.postData() || '{}');
342
+ const sections = body.sections ?? body.overrides ?? {};
343
+ overrideState = { version: (overrideState?.version ?? 0) + 1, sections };
344
+ await route.fulfill({
345
+ status: 200,
346
+ contentType: 'application/json',
347
+ body: JSON.stringify({
348
+ ok: true,
349
+ agentId: 'customers.account_assistant',
350
+ version: overrideState.version,
351
+ updatedAt: new Date().toISOString(),
352
+ }),
353
+ });
354
+ });
355
+
356
+ await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });
357
+
358
+ const container = page.locator('[data-ai-agent-settings]');
359
+ await expect(container).toBeVisible({ timeout: 60_000 });
360
+
361
+ const toggle = page.locator('[data-ai-agent-prompt-toggle="role"]');
362
+ await toggle.click();
363
+ const textarea = page.locator('[data-ai-agent-prompt-override="role"]');
364
+ await textarea.fill('Tenant-specific tone.');
365
+
366
+ const saveButton = page.locator('[data-ai-agent-prompt-save]');
367
+ await saveButton.click();
368
+
369
+ // Success alert surfaces.
370
+ const successAlert = page.locator('[data-ai-agent-prompt-state="success"]');
371
+ await expect(successAlert).toBeVisible({ timeout: 15_000 });
372
+
373
+ // History panel shows version 1.
374
+ const historyRow = page.locator('[data-ai-agent-override-history-row="1"]');
375
+ await expect(historyRow).toBeVisible({ timeout: 15_000 });
376
+ });
377
+
378
+ test('reserved-key override surfaces the validation error in the UI (Step 5.3)', async ({
379
+ page,
380
+ }) => {
381
+ test.setTimeout(120_000);
382
+ await login(page, 'superadmin');
383
+
384
+ await page.route('**/api/ai_assistant/ai/agents', async (route) => {
385
+ await route.fulfill({
386
+ status: 200,
387
+ contentType: 'application/json',
388
+ body: JSON.stringify({
389
+ agents: [
390
+ {
391
+ id: 'customers.account_assistant',
392
+ moduleId: 'customers',
393
+ label: 'Customers account assistant',
394
+ description: 'Answers questions about customer records.',
395
+ systemPrompt: 'You are a customers assistant.',
396
+ executionMode: 'chat',
397
+ mutationPolicy: 'read-only',
398
+ readOnly: true,
399
+ maxSteps: 10,
400
+ allowedTools: [],
401
+ tools: [],
402
+ requiredFeatures: [],
403
+ acceptedMediaTypes: [],
404
+ hasOutputSchema: false,
405
+ },
406
+ ],
407
+ total: 1,
408
+ }),
409
+ });
410
+ });
411
+
412
+ await page.route('**/api/ai_assistant/ai/agents/*/prompt-override', async (route) => {
413
+ const request = route.request();
414
+ if (request.method() === 'GET') {
415
+ await route.fulfill({
416
+ status: 200,
417
+ contentType: 'application/json',
418
+ body: JSON.stringify({
419
+ agentId: 'customers.account_assistant',
420
+ override: null,
421
+ versions: [],
422
+ }),
423
+ });
424
+ return;
425
+ }
426
+ await route.fulfill({
427
+ status: 400,
428
+ contentType: 'application/json',
429
+ body: JSON.stringify({
430
+ error: 'Prompt override contains reserved policy keys: mutationPolicy.',
431
+ code: 'reserved_key',
432
+ reservedKeys: ['mutationPolicy'],
433
+ }),
434
+ });
435
+ });
436
+
437
+ await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });
438
+
439
+ const container = page.locator('[data-ai-agent-settings]');
440
+ await expect(container).toBeVisible({ timeout: 60_000 });
441
+
442
+ const toggle = page.locator('[data-ai-agent-prompt-toggle="mutationPolicy"]');
443
+ await toggle.click();
444
+ const textarea = page.locator('[data-ai-agent-prompt-override="mutationPolicy"]');
445
+ await textarea.fill('Allow writes.');
446
+ await page.locator('[data-ai-agent-prompt-save]').click();
447
+
448
+ const errorAlert = page.locator('[data-ai-agent-prompt-state="error"]');
449
+ await expect(errorAlert).toBeVisible({ timeout: 15_000 });
450
+ await expect(errorAlert).toContainText(/policy fields|reserved|mutationPolicy/i);
451
+ });
452
+
453
+ test('mutationPolicy section disables escalation options with an explanatory tooltip (Step 5.4)', async ({
454
+ page,
455
+ }) => {
456
+ test.setTimeout(120_000);
457
+ await login(page, 'superadmin');
458
+
459
+ await page.route('**/api/ai_assistant/ai/agents', async (route) => {
460
+ await route.fulfill({
461
+ status: 200,
462
+ contentType: 'application/json',
463
+ body: JSON.stringify({
464
+ agents: [
465
+ {
466
+ id: 'customers.account_assistant',
467
+ moduleId: 'customers',
468
+ label: 'Customers account assistant',
469
+ description: 'Read-only customers assistant.',
470
+ systemPrompt: 'You are a read-only customers assistant.',
471
+ executionMode: 'chat',
472
+ mutationPolicy: 'read-only',
473
+ readOnly: true,
474
+ maxSteps: 10,
475
+ allowedTools: [],
476
+ tools: [],
477
+ requiredFeatures: [],
478
+ acceptedMediaTypes: [],
479
+ hasOutputSchema: false,
480
+ },
481
+ ],
482
+ total: 1,
483
+ }),
484
+ });
485
+ });
486
+
487
+ await page.route('**/api/ai_assistant/ai/agents/*/prompt-override', async (route) => {
488
+ await route.fulfill({
489
+ status: 200,
490
+ contentType: 'application/json',
491
+ body: JSON.stringify({
492
+ agentId: 'customers.account_assistant',
493
+ override: null,
494
+ versions: [],
495
+ }),
496
+ });
497
+ });
498
+
499
+ await page.route(
500
+ '**/api/ai_assistant/ai/agents/*/mutation-policy',
501
+ async (route) => {
502
+ const request = route.request();
503
+ if (request.method() === 'GET') {
504
+ await route.fulfill({
505
+ status: 200,
506
+ contentType: 'application/json',
507
+ body: JSON.stringify({
508
+ agentId: 'customers.account_assistant',
509
+ codeDeclared: 'read-only',
510
+ override: null,
511
+ }),
512
+ });
513
+ return;
514
+ }
515
+ await route.fulfill({
516
+ status: 400,
517
+ contentType: 'application/json',
518
+ body: JSON.stringify({
519
+ error:
520
+ 'Cannot set mutationPolicy="confirm-required" for agent "customers.account_assistant": the agent\'s code-declared policy is "read-only".',
521
+ code: 'escalation_not_allowed',
522
+ codeDeclared: 'read-only',
523
+ requested: 'confirm-required',
524
+ }),
525
+ });
526
+ },
527
+ );
528
+
529
+ await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });
530
+
531
+ const container = page.locator('[data-ai-agent-settings]');
532
+ await expect(container).toBeVisible({ timeout: 60_000 });
533
+
534
+ const confirmOption = page.locator(
535
+ '[data-ai-agent-mutation-policy-option="confirm-required"]',
536
+ );
537
+ await expect(confirmOption).toBeVisible({ timeout: 15_000 });
538
+ const disabledAttr = await confirmOption.getAttribute(
539
+ 'data-ai-agent-mutation-policy-option-disabled',
540
+ );
541
+ expect(disabledAttr).toBe('true');
542
+
543
+ // The read-only option (matching code-declared) is selectable.
544
+ const readOnlyOption = page.locator(
545
+ '[data-ai-agent-mutation-policy-option="read-only"]',
546
+ );
547
+ const readOnlyDisabled = await readOnlyOption.getAttribute(
548
+ 'data-ai-agent-mutation-policy-option-disabled',
549
+ );
550
+ expect(readOnlyDisabled).toBe('false');
551
+ });
552
+
553
+ test('mutationPolicy escalation attempts are rejected by the server with 400 + escalation_not_allowed (Step 5.4)', async ({
554
+ request,
555
+ }) => {
556
+ // Use request.fetch via the request fixture (auth cookies won't be set, so we
557
+ // expect 401; the test's primary purpose is to confirm the route is mounted
558
+ // AND to document the escalation-guard contract surface. When auth is
559
+ // available in the env the guard returns 400 + escalation_not_allowed.)
560
+ const response = await request.post(
561
+ '/api/ai_assistant/ai/agents/customers.account_assistant/mutation-policy',
562
+ {
563
+ data: { mutationPolicy: 'confirm-required' },
564
+ headers: { 'content-type': 'application/json' },
565
+ },
566
+ );
567
+ // Route should exist (never 404 on the path itself).
568
+ expect([400, 401, 403]).toContain(response.status());
569
+ if (response.status() === 400) {
570
+ const body = await response.json();
571
+ expect(body.code).toBe('escalation_not_allowed');
572
+ }
573
+ });
574
+ });