@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +361 -0
- package/README.md +5 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js +73 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js +484 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js +251 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js +91 -0
- package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools/attachments-pack.js +202 -0
- package/dist/modules/ai_assistant/ai-tools/attachments-pack.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools/meta-pack.js +121 -0
- package/dist/modules/ai_assistant/ai-tools/meta-pack.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools/search-pack.js +94 -0
- package/dist/modules/ai_assistant/ai-tools/search-pack.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools.js +14 -0
- package/dist/modules/ai_assistant/ai-tools.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js +175 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js +174 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js +101 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js +311 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js +246 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js +94 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/chat/route.js +173 -0
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/run-object/route.js +167 -0
- package/dist/modules/ai_assistant/api/ai/run-object/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +1111 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js +28 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js +30 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +4 -6
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +1 -21
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +462 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js +28 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +78 -12
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiPendingAction.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiPendingAction.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +228 -0
- package/dist/modules/ai_assistant/data/entities.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js +95 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js +95 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js +223 -0
- package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js.map +7 -0
- package/dist/modules/ai_assistant/events.js +33 -0
- package/dist/modules/ai_assistant/events.js.map +7 -0
- package/dist/modules/ai_assistant/i18n/de.json +252 -0
- package/dist/modules/ai_assistant/i18n/en.json +252 -0
- package/dist/modules/ai_assistant/i18n/es.json +252 -0
- package/dist/modules/ai_assistant/i18n/pl.json +252 -0
- package/dist/modules/ai_assistant/lib/agent-policy.js +168 -0
- package/dist/modules/ai_assistant/lib/agent-policy.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-registry.js +195 -0
- package/dist/modules/ai_assistant/lib/agent-registry.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-runtime.js +451 -0
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-tools.js +223 -0
- package/dist/modules/ai_assistant/lib/agent-tools.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-transport.js +25 -0
- package/dist/modules/ai_assistant/lib/agent-transport.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js +11 -0
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js +1 -0
- package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js +239 -0
- package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-overrides.js +189 -0
- package/dist/modules/ai_assistant/lib/ai-overrides.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-tool-definition.js +7 -0
- package/dist/modules/ai_assistant/lib/ai-tool-definition.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js +1 -0
- package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-backed-tool.js +48 -0
- package/dist/modules/ai_assistant/lib/api-backed-tool.js.map +7 -0
- package/dist/modules/ai_assistant/lib/attachment-bridge-types.js +1 -0
- package/dist/modules/ai_assistant/lib/attachment-bridge-types.js.map +7 -0
- package/dist/modules/ai_assistant/lib/attachment-parts.js +276 -0
- package/dist/modules/ai_assistant/lib/attachment-parts.js.map +7 -0
- package/dist/modules/ai_assistant/lib/model-factory.js +68 -0
- package/dist/modules/ai_assistant/lib/model-factory.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js +86 -0
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-client.js +35 -0
- package/dist/modules/ai_assistant/lib/pending-action-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-executor.js +243 -0
- package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-recheck.js +246 -0
- package/dist/modules/ai_assistant/lib/pending-action-recheck.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-types.js +70 -0
- package/dist/modules/ai_assistant/lib/pending-action-types.js.map +7 -0
- package/dist/modules/ai_assistant/lib/prepare-mutation.js +315 -0
- package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +7 -0
- package/dist/modules/ai_assistant/lib/prompt-composition-types.js +7 -0
- package/dist/modules/ai_assistant/lib/prompt-composition-types.js.map +7 -0
- package/dist/modules/ai_assistant/lib/prompt-override-merge.js +175 -0
- package/dist/modules/ai_assistant/lib/prompt-override-merge.js.map +7 -0
- package/dist/modules/ai_assistant/lib/schema-utils.js +5 -1
- package/dist/modules/ai_assistant/lib/schema-utils.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-executor.js +13 -2
- package/dist/modules/ai_assistant/lib/tool-executor.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-loader.js +86 -11
- package/dist/modules/ai_assistant/lib/tool-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-test-fixtures.js +120 -0
- package/dist/modules/ai_assistant/lib/tool-test-fixtures.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-test-runner.js +418 -0
- package/dist/modules/ai_assistant/lib/tool-test-runner.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419100521.js +17 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419100521.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419132948.js +16 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419132948.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419134235.js +17 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419134235.js.map +7 -0
- package/dist/modules/ai_assistant/setup.js +36 -0
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js +161 -0
- package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js.map +7 -0
- package/generated/entities/ai_agent_mutation_policy_override/index.ts +9 -0
- package/generated/entities/ai_agent_prompt_override/index.ts +10 -0
- package/generated/entities/ai_pending_action/index.ts +24 -0
- package/generated/entities.ids.generated.ts +13 -0
- package/generated/entity-fields-registry.ts +57 -0
- package/jest.config.cjs +7 -0
- package/package.json +4 -4
- package/src/index.ts +215 -0
- package/src/modules/ai_assistant/__integration__/README.md +5 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.ts +115 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.ts +574 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.ts +333 -0
- package/src/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.ts +135 -0
- package/src/modules/ai_assistant/__tests__/events.test.ts +145 -0
- package/src/modules/ai_assistant/__tests__/integration/pending-action-contract.test.ts +1015 -0
- package/src/modules/ai_assistant/__tests__/integration/ws-c-attachment-bridge.test.ts +235 -0
- package/src/modules/ai_assistant/__tests__/integration/ws-c-policy-and-tools.test.ts +330 -0
- package/src/modules/ai_assistant/__tests__/integration/ws-c-tool-pack-coverage.test.ts +285 -0
- package/src/modules/ai_assistant/ai-tools/__tests__/attachments-pack.test.ts +322 -0
- package/src/modules/ai_assistant/ai-tools/__tests__/meta-pack.test.ts +218 -0
- package/src/modules/ai_assistant/ai-tools/__tests__/search-pack.test.ts +192 -0
- package/src/modules/ai_assistant/ai-tools/attachments-pack.ts +269 -0
- package/src/modules/ai_assistant/ai-tools/meta-pack.ts +140 -0
- package/src/modules/ai_assistant/ai-tools/search-pack.ts +122 -0
- package/src/modules/ai_assistant/ai-tools.ts +21 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/__tests__/route.test.ts +222 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/__tests__/route.test.ts +286 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/route.ts +237 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/__tests__/route.test.ts +339 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/route.ts +229 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/route.ts +142 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/__tests__/route.test.ts +367 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.ts +380 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/__tests__/route.test.ts +333 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.ts +307 -0
- package/src/modules/ai_assistant/api/ai/agents/route.ts +107 -0
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +282 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +207 -0
- package/src/modules/ai_assistant/api/ai/run-object/__tests__/route.test.ts +282 -0
- package/src/modules/ai_assistant/api/ai/run-object/route.ts +204 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +1419 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.ts +26 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.ts +28 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +8 -23
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +15 -10
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +604 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.ts +26 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +99 -24
- package/src/modules/ai_assistant/data/__tests__/schema-unique-indexes.test.ts +69 -0
- package/src/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.ts +7 -0
- package/src/modules/ai_assistant/data/entities/AiAgentPromptOverride.ts +7 -0
- package/src/modules/ai_assistant/data/entities/AiPendingAction.ts +7 -0
- package/src/modules/ai_assistant/data/entities.ts +270 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.ts +129 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.ts +132 -0
- package/src/modules/ai_assistant/data/repositories/AiPendingActionRepository.ts +334 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentMutationPolicyOverrideRepository.test.ts +195 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentPromptOverrideRepository.test.ts +197 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiPendingActionRepository.test.ts +357 -0
- package/src/modules/ai_assistant/events.ts +112 -0
- package/src/modules/ai_assistant/i18n/de.json +252 -0
- package/src/modules/ai_assistant/i18n/en.json +252 -0
- package/src/modules/ai_assistant/i18n/es.json +252 -0
- package/src/modules/ai_assistant/i18n/pl.json +252 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-policy.mutation-override.test.ts +203 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-policy.test.ts +385 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-registry.test.ts +217 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-object.test.ts +329 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-parity.test.ts +573 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +291 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-tools.test.ts +172 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-transport.test.ts +41 -0
- package/src/modules/ai_assistant/lib/__tests__/ai-agent-definition.test.ts +183 -0
- package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +432 -0
- package/src/modules/ai_assistant/lib/__tests__/ai-overrides.test.ts +308 -0
- package/src/modules/ai_assistant/lib/__tests__/api-backed-tool.test.ts +302 -0
- package/src/modules/ai_assistant/lib/__tests__/attachment-bridge-and-prompt-types.test.ts +188 -0
- package/src/modules/ai_assistant/lib/__tests__/attachment-parts.test.ts +531 -0
- package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +263 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.integration.test.ts +183 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +168 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-cancel.test.ts +235 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-client.test.ts +148 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-executor.test.ts +348 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-recheck.test.ts +378 -0
- package/src/modules/ai_assistant/lib/__tests__/phase-0-additive-contract.test.ts +299 -0
- package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +610 -0
- package/src/modules/ai_assistant/lib/__tests__/prompt-override-merge.test.ts +136 -0
- package/src/modules/ai_assistant/lib/__tests__/tool-loader.test.ts +125 -0
- package/src/modules/ai_assistant/lib/agent-policy.ts +270 -0
- package/src/modules/ai_assistant/lib/agent-registry.ts +277 -0
- package/src/modules/ai_assistant/lib/agent-runtime.ts +751 -0
- package/src/modules/ai_assistant/lib/agent-tools.ts +396 -0
- package/src/modules/ai_assistant/lib/agent-transport.ts +51 -0
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +86 -0
- package/src/modules/ai_assistant/lib/ai-agents-generated.d.ts +18 -0
- package/src/modules/ai_assistant/lib/ai-api-operation-runner.ts +333 -0
- package/src/modules/ai_assistant/lib/ai-overrides.ts +389 -0
- package/src/modules/ai_assistant/lib/ai-tool-definition.ts +7 -0
- package/src/modules/ai_assistant/lib/ai-tools-generated.d.ts +7 -0
- package/src/modules/ai_assistant/lib/api-backed-tool.ts +85 -0
- package/src/modules/ai_assistant/lib/attachment-bridge-types.ts +24 -0
- package/src/modules/ai_assistant/lib/attachment-parts.ts +433 -0
- package/src/modules/ai_assistant/lib/model-factory.ts +212 -0
- package/src/modules/ai_assistant/lib/pending-action-cancel.ts +179 -0
- package/src/modules/ai_assistant/lib/pending-action-client.ts +126 -0
- package/src/modules/ai_assistant/lib/pending-action-executor.ts +424 -0
- package/src/modules/ai_assistant/lib/pending-action-recheck.ts +410 -0
- package/src/modules/ai_assistant/lib/pending-action-types.ts +194 -0
- package/src/modules/ai_assistant/lib/prepare-mutation.ts +448 -0
- package/src/modules/ai_assistant/lib/prompt-composition-types.ts +24 -0
- package/src/modules/ai_assistant/lib/prompt-override-merge.ts +253 -0
- package/src/modules/ai_assistant/lib/schema-utils.ts +14 -2
- package/src/modules/ai_assistant/lib/tool-executor.ts +25 -3
- package/src/modules/ai_assistant/lib/tool-loader.ts +159 -13
- package/src/modules/ai_assistant/lib/tool-test-fixtures.ts +160 -0
- package/src/modules/ai_assistant/lib/tool-test-runner.ts +596 -0
- package/src/modules/ai_assistant/lib/types.ts +105 -2
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +871 -0
- package/src/modules/ai_assistant/migrations/Migration20260419100521.ts +17 -0
- package/src/modules/ai_assistant/migrations/Migration20260419132948.ts +16 -0
- package/src/modules/ai_assistant/migrations/Migration20260419134235.ts +17 -0
- package/src/modules/ai_assistant/setup.ts +53 -0
- package/src/modules/ai_assistant/workers/__tests__/ai-pending-action-cleanup.test.ts +333 -0
- package/src/modules/ai_assistant/workers/ai-pending-action-cleanup.ts +269 -0
package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentPromptOverrideRepository.test.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { AiAgentPromptOverrideRepository } from '../AiAgentPromptOverrideRepository'
|
|
2
|
+
import { AiAgentPromptOverride } from '../../entities'
|
|
3
|
+
|
|
4
|
+
type Row = {
|
|
5
|
+
id: string
|
|
6
|
+
tenantId: string
|
|
7
|
+
organizationId: string | null
|
|
8
|
+
agentId: string
|
|
9
|
+
version: number
|
|
10
|
+
sections: Record<string, string>
|
|
11
|
+
notes: string | null
|
|
12
|
+
createdByUserId: string | null
|
|
13
|
+
createdAt: Date
|
|
14
|
+
updatedAt: Date
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let idCounter = 0
|
|
18
|
+
|
|
19
|
+
function mockEm() {
|
|
20
|
+
const store: Row[] = []
|
|
21
|
+
|
|
22
|
+
const find = async (_entity: unknown, where: any, options?: any): Promise<Row[]> => {
|
|
23
|
+
let rows = store.filter((row) => {
|
|
24
|
+
if (where?.agentId && row.agentId !== where.agentId) return false
|
|
25
|
+
if (where?.tenantId && row.tenantId !== where.tenantId) return false
|
|
26
|
+
// organizationId supports null filter equivalence.
|
|
27
|
+
if (where && 'organizationId' in where) {
|
|
28
|
+
const expected = where.organizationId ?? null
|
|
29
|
+
if ((row.organizationId ?? null) !== expected) return false
|
|
30
|
+
}
|
|
31
|
+
return true
|
|
32
|
+
})
|
|
33
|
+
const orderBy = options?.orderBy
|
|
34
|
+
if (orderBy?.version === 'desc') {
|
|
35
|
+
rows = [...rows].sort((a, b) => b.version - a.version)
|
|
36
|
+
} else if (orderBy?.version === 'asc') {
|
|
37
|
+
rows = [...rows].sort((a, b) => a.version - b.version)
|
|
38
|
+
}
|
|
39
|
+
if (typeof options?.limit === 'number') rows = rows.slice(0, options.limit)
|
|
40
|
+
return rows
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const em: any = {
|
|
44
|
+
find,
|
|
45
|
+
findOne: async (_entity: unknown, where: any, options?: any) => {
|
|
46
|
+
const rows = await find(_entity, where, options)
|
|
47
|
+
return rows[0] ?? null
|
|
48
|
+
},
|
|
49
|
+
create: (_entity: unknown, data: any) => {
|
|
50
|
+
idCounter += 1
|
|
51
|
+
const row: Row = {
|
|
52
|
+
id: `row-${idCounter}`,
|
|
53
|
+
tenantId: data.tenantId,
|
|
54
|
+
organizationId: data.organizationId ?? null,
|
|
55
|
+
agentId: data.agentId,
|
|
56
|
+
version: data.version,
|
|
57
|
+
sections: data.sections,
|
|
58
|
+
notes: data.notes ?? null,
|
|
59
|
+
createdByUserId: data.createdByUserId ?? null,
|
|
60
|
+
createdAt: new Date(),
|
|
61
|
+
updatedAt: new Date(),
|
|
62
|
+
}
|
|
63
|
+
return row
|
|
64
|
+
},
|
|
65
|
+
persist: (row: Row) => {
|
|
66
|
+
em.__pendingPersist = row
|
|
67
|
+
return em
|
|
68
|
+
},
|
|
69
|
+
flush: async () => {
|
|
70
|
+
if (em.__pendingPersist) {
|
|
71
|
+
store.push(em.__pendingPersist as Row)
|
|
72
|
+
em.__pendingPersist = null
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
transactional: async (fn: (tx: any) => Promise<unknown>) => {
|
|
76
|
+
return fn(em)
|
|
77
|
+
},
|
|
78
|
+
__pendingPersist: null as Row | null,
|
|
79
|
+
__store: store,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return em
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('AiAgentPromptOverrideRepository', () => {
|
|
86
|
+
it('allocates monotonic versions per (tenant, org, agent)', async () => {
|
|
87
|
+
const em = mockEm()
|
|
88
|
+
const repo = new AiAgentPromptOverrideRepository(em)
|
|
89
|
+
const ctx = { tenantId: 't1', organizationId: null }
|
|
90
|
+
|
|
91
|
+
const first = await repo.save(
|
|
92
|
+
{ agentId: 'catalog.assistant', sections: { role: 'A' } },
|
|
93
|
+
ctx,
|
|
94
|
+
)
|
|
95
|
+
expect(first.version).toBe(1)
|
|
96
|
+
|
|
97
|
+
const second = await repo.save(
|
|
98
|
+
{ agentId: 'catalog.assistant', sections: { role: 'B' } },
|
|
99
|
+
ctx,
|
|
100
|
+
)
|
|
101
|
+
expect(second.version).toBe(2)
|
|
102
|
+
|
|
103
|
+
// Different agent under same tenant starts at 1 again.
|
|
104
|
+
const otherAgent = await repo.save(
|
|
105
|
+
{ agentId: 'customers.assistant', sections: { role: 'C' } },
|
|
106
|
+
ctx,
|
|
107
|
+
)
|
|
108
|
+
expect(otherAgent.version).toBe(1)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('scopes per tenant — getLatest for a different tenant returns null', async () => {
|
|
112
|
+
const em = mockEm()
|
|
113
|
+
const repo = new AiAgentPromptOverrideRepository(em)
|
|
114
|
+
|
|
115
|
+
await repo.save(
|
|
116
|
+
{ agentId: 'catalog.assistant', sections: { role: 'A' } },
|
|
117
|
+
{ tenantId: 't1', organizationId: null },
|
|
118
|
+
)
|
|
119
|
+
const latestA = await repo.getLatest('catalog.assistant', {
|
|
120
|
+
tenantId: 't1',
|
|
121
|
+
organizationId: null,
|
|
122
|
+
})
|
|
123
|
+
const latestB = await repo.getLatest('catalog.assistant', {
|
|
124
|
+
tenantId: 't2',
|
|
125
|
+
organizationId: null,
|
|
126
|
+
})
|
|
127
|
+
expect(latestA?.version).toBe(1)
|
|
128
|
+
expect(latestB).toBeNull()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('listVersions returns rows newest first and caps by limit', async () => {
|
|
132
|
+
const em = mockEm()
|
|
133
|
+
const repo = new AiAgentPromptOverrideRepository(em)
|
|
134
|
+
const ctx = { tenantId: 't1', organizationId: null }
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < 5; i += 1) {
|
|
137
|
+
await repo.save(
|
|
138
|
+
{ agentId: 'catalog.assistant', sections: { role: `v${i}` } },
|
|
139
|
+
ctx,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const versions = await repo.listVersions('catalog.assistant', ctx, 3)
|
|
144
|
+
expect(versions.map((v) => v.version)).toEqual([5, 4, 3])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('returns empty array when tenant/agent has no rows', async () => {
|
|
148
|
+
const em = mockEm()
|
|
149
|
+
const repo = new AiAgentPromptOverrideRepository(em)
|
|
150
|
+
const rows = await repo.listVersions('catalog.assistant', {
|
|
151
|
+
tenantId: 't1',
|
|
152
|
+
organizationId: null,
|
|
153
|
+
})
|
|
154
|
+
expect(rows).toEqual([])
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('drops empty/whitespace-only override values before persisting', async () => {
|
|
158
|
+
const em = mockEm()
|
|
159
|
+
const repo = new AiAgentPromptOverrideRepository(em)
|
|
160
|
+
const saved = await repo.save(
|
|
161
|
+
{
|
|
162
|
+
agentId: 'catalog.assistant',
|
|
163
|
+
sections: { role: 'kept', scope: ' ', data: '' },
|
|
164
|
+
},
|
|
165
|
+
{ tenantId: 't1', organizationId: null },
|
|
166
|
+
)
|
|
167
|
+
expect(Object.keys(saved.sections)).toEqual(['role'])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('throws when tenantId is missing on save', async () => {
|
|
171
|
+
const em = mockEm()
|
|
172
|
+
const repo = new AiAgentPromptOverrideRepository(em)
|
|
173
|
+
await expect(
|
|
174
|
+
repo.save(
|
|
175
|
+
{ agentId: 'catalog.assistant', sections: { role: 'x' } },
|
|
176
|
+
{ tenantId: '', organizationId: null } as any,
|
|
177
|
+
),
|
|
178
|
+
).rejects.toThrow(/tenantId/)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('returns an AiAgentPromptOverride-shaped payload', async () => {
|
|
182
|
+
const em = mockEm()
|
|
183
|
+
const repo = new AiAgentPromptOverrideRepository(em)
|
|
184
|
+
const saved = await repo.save(
|
|
185
|
+
{ agentId: 'catalog.assistant', sections: { role: 'x' }, notes: 'note' },
|
|
186
|
+
{ tenantId: 't1', organizationId: 'o1', userId: 'u1' },
|
|
187
|
+
)
|
|
188
|
+
expect(saved.agentId).toBe('catalog.assistant')
|
|
189
|
+
expect(saved.version).toBe(1)
|
|
190
|
+
expect(saved.organizationId).toBe('o1')
|
|
191
|
+
expect(saved.createdByUserId).toBe('u1')
|
|
192
|
+
expect(saved.notes).toBe('note')
|
|
193
|
+
// Entity class reference intact (mock returns a plain object but the
|
|
194
|
+
// real repo path calls tx.create(AiAgentPromptOverride, data)).
|
|
195
|
+
void AiAgentPromptOverride
|
|
196
|
+
})
|
|
197
|
+
})
|
package/src/modules/ai_assistant/data/repositories/__tests__/AiPendingActionRepository.test.ts
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { AiPendingActionRepository } from '../AiPendingActionRepository'
|
|
2
|
+
import { AiPendingAction } from '../../entities'
|
|
3
|
+
import {
|
|
4
|
+
AiPendingActionStateError,
|
|
5
|
+
type AiPendingActionStatus,
|
|
6
|
+
} from '../../../lib/pending-action-types'
|
|
7
|
+
|
|
8
|
+
type Row = {
|
|
9
|
+
id: string
|
|
10
|
+
tenantId: string
|
|
11
|
+
organizationId: string | null
|
|
12
|
+
agentId: string
|
|
13
|
+
toolName: string
|
|
14
|
+
conversationId: string | null
|
|
15
|
+
targetEntityType: string | null
|
|
16
|
+
targetRecordId: string | null
|
|
17
|
+
normalizedInput: Record<string, unknown>
|
|
18
|
+
fieldDiff: Array<{ field: string; before: unknown; after: unknown }>
|
|
19
|
+
records: Array<Record<string, unknown>> | null
|
|
20
|
+
failedRecords: Array<Record<string, unknown>> | null
|
|
21
|
+
sideEffectsSummary: string | null
|
|
22
|
+
recordVersion: string | null
|
|
23
|
+
attachmentIds: string[]
|
|
24
|
+
idempotencyKey: string
|
|
25
|
+
createdByUserId: string
|
|
26
|
+
status: AiPendingActionStatus
|
|
27
|
+
queueMode: 'inline' | 'stack'
|
|
28
|
+
executionResult: Record<string, unknown> | null
|
|
29
|
+
createdAt: Date
|
|
30
|
+
expiresAt: Date
|
|
31
|
+
resolvedAt: Date | null
|
|
32
|
+
resolvedByUserId: string | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let idCounter = 0
|
|
36
|
+
|
|
37
|
+
function rowMatchesWhere(row: Row, where: any): boolean {
|
|
38
|
+
if (!where) return true
|
|
39
|
+
if (where.id && row.id !== where.id) return false
|
|
40
|
+
if (where.tenantId && row.tenantId !== where.tenantId) return false
|
|
41
|
+
if ('organizationId' in where) {
|
|
42
|
+
const expected = where.organizationId ?? null
|
|
43
|
+
if ((row.organizationId ?? null) !== expected) return false
|
|
44
|
+
}
|
|
45
|
+
if (where.agentId && row.agentId !== where.agentId) return false
|
|
46
|
+
if (where.idempotencyKey && row.idempotencyKey !== where.idempotencyKey) {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
if (where.status && row.status !== where.status) return false
|
|
50
|
+
if (where.expiresAt && typeof where.expiresAt === 'object') {
|
|
51
|
+
if ('$lt' in where.expiresAt) {
|
|
52
|
+
if (!(row.expiresAt.getTime() < (where.expiresAt.$lt as Date).getTime())) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function applyOrder(rows: Row[], orderBy: any): Row[] {
|
|
61
|
+
if (!orderBy) return rows
|
|
62
|
+
if (orderBy.createdAt === 'desc') {
|
|
63
|
+
return [...rows].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
64
|
+
}
|
|
65
|
+
if (orderBy.createdAt === 'asc') {
|
|
66
|
+
return [...rows].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
|
|
67
|
+
}
|
|
68
|
+
if (orderBy.expiresAt === 'asc') {
|
|
69
|
+
return [...rows].sort((a, b) => a.expiresAt.getTime() - b.expiresAt.getTime())
|
|
70
|
+
}
|
|
71
|
+
if (orderBy.expiresAt === 'desc') {
|
|
72
|
+
return [...rows].sort((a, b) => b.expiresAt.getTime() - a.expiresAt.getTime())
|
|
73
|
+
}
|
|
74
|
+
return rows
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mockEm() {
|
|
78
|
+
const store: Row[] = []
|
|
79
|
+
|
|
80
|
+
const find = async (_entity: unknown, where: any, options?: any): Promise<Row[]> => {
|
|
81
|
+
let rows = store.filter((row) => rowMatchesWhere(row, where))
|
|
82
|
+
rows = applyOrder(rows, options?.orderBy)
|
|
83
|
+
if (typeof options?.limit === 'number') rows = rows.slice(0, options.limit)
|
|
84
|
+
return rows
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const em: any = {
|
|
88
|
+
find,
|
|
89
|
+
findOne: async (_entity: unknown, where: any, options?: any) => {
|
|
90
|
+
const rows = await find(_entity, where, options)
|
|
91
|
+
return rows[0] ?? null
|
|
92
|
+
},
|
|
93
|
+
create: (_entity: unknown, data: any) => {
|
|
94
|
+
idCounter += 1
|
|
95
|
+
const row: Row = {
|
|
96
|
+
id: `row-${idCounter}`,
|
|
97
|
+
tenantId: data.tenantId,
|
|
98
|
+
organizationId: data.organizationId ?? null,
|
|
99
|
+
agentId: data.agentId,
|
|
100
|
+
toolName: data.toolName,
|
|
101
|
+
conversationId: data.conversationId ?? null,
|
|
102
|
+
targetEntityType: data.targetEntityType ?? null,
|
|
103
|
+
targetRecordId: data.targetRecordId ?? null,
|
|
104
|
+
normalizedInput: data.normalizedInput ?? {},
|
|
105
|
+
fieldDiff: Array.isArray(data.fieldDiff) ? data.fieldDiff : [],
|
|
106
|
+
records: data.records ?? null,
|
|
107
|
+
failedRecords: data.failedRecords ?? null,
|
|
108
|
+
sideEffectsSummary: data.sideEffectsSummary ?? null,
|
|
109
|
+
recordVersion: data.recordVersion ?? null,
|
|
110
|
+
attachmentIds: Array.isArray(data.attachmentIds) ? data.attachmentIds : [],
|
|
111
|
+
idempotencyKey: data.idempotencyKey,
|
|
112
|
+
createdByUserId: data.createdByUserId,
|
|
113
|
+
status: data.status ?? 'pending',
|
|
114
|
+
queueMode: data.queueMode ?? 'inline',
|
|
115
|
+
executionResult: data.executionResult ?? null,
|
|
116
|
+
createdAt: data.createdAt instanceof Date ? data.createdAt : new Date(),
|
|
117
|
+
expiresAt: data.expiresAt instanceof Date ? data.expiresAt : new Date(),
|
|
118
|
+
resolvedAt: data.resolvedAt ?? null,
|
|
119
|
+
resolvedByUserId: data.resolvedByUserId ?? null,
|
|
120
|
+
}
|
|
121
|
+
return row
|
|
122
|
+
},
|
|
123
|
+
persist: (row: Row) => {
|
|
124
|
+
em.__pendingPersist = row
|
|
125
|
+
return em
|
|
126
|
+
},
|
|
127
|
+
remove: (row: Row) => {
|
|
128
|
+
em.__pendingRemove = row
|
|
129
|
+
return em
|
|
130
|
+
},
|
|
131
|
+
flush: async () => {
|
|
132
|
+
if (em.__pendingRemove) {
|
|
133
|
+
const row = em.__pendingRemove as Row
|
|
134
|
+
const idx = store.findIndex((candidate) => candidate.id === row.id)
|
|
135
|
+
if (idx >= 0) store.splice(idx, 1)
|
|
136
|
+
em.__pendingRemove = null
|
|
137
|
+
}
|
|
138
|
+
if (em.__pendingPersist) {
|
|
139
|
+
const row = em.__pendingPersist as Row
|
|
140
|
+
const idx = store.findIndex((candidate) => candidate.id === row.id)
|
|
141
|
+
if (idx >= 0) store[idx] = row
|
|
142
|
+
else store.push(row)
|
|
143
|
+
em.__pendingPersist = null
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
transactional: async (fn: (tx: any) => Promise<unknown>) => {
|
|
147
|
+
return fn(em)
|
|
148
|
+
},
|
|
149
|
+
__pendingPersist: null as Row | null,
|
|
150
|
+
__store: store,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return em
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const tenantAlpha = 't-alpha'
|
|
157
|
+
const tenantBeta = 't-beta'
|
|
158
|
+
|
|
159
|
+
function baseInput(overrides: Partial<any> = {}) {
|
|
160
|
+
return {
|
|
161
|
+
agentId: 'catalog.merchandising_assistant',
|
|
162
|
+
toolName: 'catalog.products.update',
|
|
163
|
+
idempotencyKey: overrides.idempotencyKey ?? 'idem-1',
|
|
164
|
+
createdByUserId: 'u-1',
|
|
165
|
+
normalizedInput: { productId: 'p-1', patch: { name: 'new' } },
|
|
166
|
+
fieldDiff: [{ field: 'name', before: 'old', after: 'new' }],
|
|
167
|
+
targetEntityType: 'catalog.product',
|
|
168
|
+
targetRecordId: 'p-1',
|
|
169
|
+
recordVersion: 'v-1',
|
|
170
|
+
...overrides,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
describe('AiPendingActionRepository', () => {
|
|
175
|
+
it('creates a row in status=pending with TTL-derived expiresAt and empty attachmentIds default', async () => {
|
|
176
|
+
const em = mockEm()
|
|
177
|
+
const repo = new AiPendingActionRepository(em)
|
|
178
|
+
const now = new Date('2026-04-18T12:00:00.000Z')
|
|
179
|
+
const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
|
|
180
|
+
|
|
181
|
+
const row = await repo.create(
|
|
182
|
+
baseInput({ now, ttlSeconds: 900 }),
|
|
183
|
+
ctx,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
expect(row.status).toBe('pending')
|
|
187
|
+
expect(row.tenantId).toBe(tenantAlpha)
|
|
188
|
+
expect(row.attachmentIds).toEqual([])
|
|
189
|
+
expect(row.expiresAt.getTime()).toBe(now.getTime() + 900 * 1000)
|
|
190
|
+
expect(row.queueMode).toBe('inline')
|
|
191
|
+
expect(row.executionResult).toBeNull()
|
|
192
|
+
expect(row.resolvedAt).toBeNull()
|
|
193
|
+
expect(row.resolvedByUserId).toBeNull()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('is idempotent: second create with same (tenant, org, idempotencyKey) returns the same row while pending', async () => {
|
|
197
|
+
const em = mockEm()
|
|
198
|
+
const repo = new AiPendingActionRepository(em)
|
|
199
|
+
const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
|
|
200
|
+
|
|
201
|
+
const first = await repo.create(baseInput({ idempotencyKey: 'idem-42' }), ctx)
|
|
202
|
+
const second = await repo.create(
|
|
203
|
+
baseInput({
|
|
204
|
+
idempotencyKey: 'idem-42',
|
|
205
|
+
normalizedInput: { productId: 'p-1', patch: { name: 'different-call' } },
|
|
206
|
+
}),
|
|
207
|
+
ctx,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
expect(second.id).toBe(first.id)
|
|
211
|
+
// the repo MUST NOT mutate the existing row from the second call
|
|
212
|
+
expect(second.normalizedInput).toEqual(first.normalizedInput)
|
|
213
|
+
expect(em.__store).toHaveLength(1)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('after a terminal status, same idempotencyKey mints a NEW row (new id)', async () => {
|
|
217
|
+
const em = mockEm()
|
|
218
|
+
const repo = new AiPendingActionRepository(em)
|
|
219
|
+
const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
|
|
220
|
+
|
|
221
|
+
const first = await repo.create(baseInput({ idempotencyKey: 'idem-9' }), ctx)
|
|
222
|
+
await repo.setStatus(first.id, 'cancelled', ctx, { resolvedByUserId: 'u-1' })
|
|
223
|
+
|
|
224
|
+
const second = await repo.create(baseInput({ idempotencyKey: 'idem-9' }), ctx)
|
|
225
|
+
expect(second.id).not.toBe(first.id)
|
|
226
|
+
expect(second.status).toBe('pending')
|
|
227
|
+
// The repo removes stale terminal rows (cancelled/failed/expired) before
|
|
228
|
+
// minting a fresh pending row so the unique-key constraint stays satisfied
|
|
229
|
+
// and the "Fix with AI" retry flow works. Store ends up with just the
|
|
230
|
+
// new pending row.
|
|
231
|
+
expect(em.__store).toHaveLength(1)
|
|
232
|
+
expect(em.__store[0].id).toBe(second.id)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('setStatus rejects illegal transitions (e.g. confirmed → pending) with AiPendingActionStateError', async () => {
|
|
236
|
+
const em = mockEm()
|
|
237
|
+
const repo = new AiPendingActionRepository(em)
|
|
238
|
+
const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
|
|
239
|
+
|
|
240
|
+
const row = await repo.create(baseInput({ idempotencyKey: 'idem-illegal' }), ctx)
|
|
241
|
+
await repo.setStatus(row.id, 'confirmed', ctx, { resolvedByUserId: 'u-1' })
|
|
242
|
+
|
|
243
|
+
await expect(
|
|
244
|
+
repo.setStatus(row.id, 'pending', ctx),
|
|
245
|
+
).rejects.toBeInstanceOf(AiPendingActionStateError)
|
|
246
|
+
await expect(
|
|
247
|
+
repo.setStatus(row.id, 'cancelled', ctx),
|
|
248
|
+
).rejects.toBeInstanceOf(AiPendingActionStateError)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('setStatus to expired sets resolvedAt and resolvedByUserId: null', async () => {
|
|
252
|
+
const em = mockEm()
|
|
253
|
+
const repo = new AiPendingActionRepository(em)
|
|
254
|
+
const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
|
|
255
|
+
|
|
256
|
+
const row = await repo.create(baseInput({ idempotencyKey: 'idem-expire' }), ctx)
|
|
257
|
+
const expiredAt = new Date('2026-04-18T13:00:00.000Z')
|
|
258
|
+
const expired = await repo.setStatus(row.id, 'expired', ctx, { now: expiredAt })
|
|
259
|
+
|
|
260
|
+
expect(expired.status).toBe('expired')
|
|
261
|
+
expect(expired.resolvedAt).toEqual(expiredAt)
|
|
262
|
+
expect(expired.resolvedByUserId).toBeNull()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('listExpired returns rows with status=pending and expiresAt < now, capped by limit, tenant-isolated', async () => {
|
|
266
|
+
const em = mockEm()
|
|
267
|
+
const repo = new AiPendingActionRepository(em)
|
|
268
|
+
const ctxAlpha = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
|
|
269
|
+
const ctxBeta = { tenantId: tenantBeta, organizationId: null, userId: 'u-2' }
|
|
270
|
+
|
|
271
|
+
const baseNow = new Date('2026-04-18T12:00:00.000Z')
|
|
272
|
+
for (let i = 0; i < 4; i += 1) {
|
|
273
|
+
await repo.create(
|
|
274
|
+
baseInput({
|
|
275
|
+
idempotencyKey: `alpha-${i}`,
|
|
276
|
+
now: new Date(baseNow.getTime() + i * 1000),
|
|
277
|
+
ttlSeconds: 60,
|
|
278
|
+
}),
|
|
279
|
+
ctxAlpha,
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
// a beta-tenant row that is also expired (MUST NOT appear in alpha's listExpired)
|
|
283
|
+
await repo.create(
|
|
284
|
+
baseInput({
|
|
285
|
+
idempotencyKey: 'beta-0',
|
|
286
|
+
now: baseNow,
|
|
287
|
+
ttlSeconds: 60,
|
|
288
|
+
}),
|
|
289
|
+
ctxBeta,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
// an alpha row that is still in the future
|
|
293
|
+
await repo.create(
|
|
294
|
+
baseInput({
|
|
295
|
+
idempotencyKey: 'alpha-future',
|
|
296
|
+
now: new Date(baseNow.getTime() + 3600 * 1000),
|
|
297
|
+
ttlSeconds: 3600,
|
|
298
|
+
}),
|
|
299
|
+
ctxAlpha,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
const cleanupNow = new Date(baseNow.getTime() + 120 * 1000)
|
|
303
|
+
const alphaExpired = await repo.listExpired(ctxAlpha, cleanupNow, 2)
|
|
304
|
+
expect(alphaExpired).toHaveLength(2)
|
|
305
|
+
for (const row of alphaExpired) {
|
|
306
|
+
expect(row.tenantId).toBe(tenantAlpha)
|
|
307
|
+
expect(row.status).toBe('pending')
|
|
308
|
+
expect(row.expiresAt.getTime()).toBeLessThan(cleanupNow.getTime())
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const betaExpired = await repo.listExpired(ctxBeta, cleanupNow, 10)
|
|
312
|
+
expect(betaExpired.map((r) => r.idempotencyKey)).toEqual(['beta-0'])
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('getById is tenant-scoped: another tenant returns null', async () => {
|
|
316
|
+
const em = mockEm()
|
|
317
|
+
const repo = new AiPendingActionRepository(em)
|
|
318
|
+
const ctxAlpha = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
|
|
319
|
+
|
|
320
|
+
const row = await repo.create(baseInput({ idempotencyKey: 'idem-iso' }), ctxAlpha)
|
|
321
|
+
const sameTenant = await repo.getById(row.id, ctxAlpha)
|
|
322
|
+
expect(sameTenant?.id).toBe(row.id)
|
|
323
|
+
|
|
324
|
+
const otherTenant = await repo.getById(row.id, {
|
|
325
|
+
tenantId: tenantBeta,
|
|
326
|
+
organizationId: null,
|
|
327
|
+
})
|
|
328
|
+
expect(otherTenant).toBeNull()
|
|
329
|
+
|
|
330
|
+
// sanity: the entity class is importable from both paths
|
|
331
|
+
void AiPendingAction
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('listPendingForAgent returns only pending rows for the requested agent and tenant', async () => {
|
|
335
|
+
const em = mockEm()
|
|
336
|
+
const repo = new AiPendingActionRepository(em)
|
|
337
|
+
const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
|
|
338
|
+
|
|
339
|
+
const first = await repo.create(
|
|
340
|
+
baseInput({ idempotencyKey: 'p-1', agentId: 'catalog.assistant' }),
|
|
341
|
+
ctx,
|
|
342
|
+
)
|
|
343
|
+
await repo.create(
|
|
344
|
+
baseInput({ idempotencyKey: 'p-2', agentId: 'catalog.assistant' }),
|
|
345
|
+
ctx,
|
|
346
|
+
)
|
|
347
|
+
await repo.create(
|
|
348
|
+
baseInput({ idempotencyKey: 'p-3', agentId: 'customers.assistant' }),
|
|
349
|
+
ctx,
|
|
350
|
+
)
|
|
351
|
+
await repo.setStatus(first.id, 'cancelled', ctx, { resolvedByUserId: 'u-1' })
|
|
352
|
+
|
|
353
|
+
const pending = await repo.listPendingForAgent('catalog.assistant', ctx)
|
|
354
|
+
expect(pending).toHaveLength(1)
|
|
355
|
+
expect(pending[0].idempotencyKey).toBe('p-2')
|
|
356
|
+
})
|
|
357
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createModuleEvents } from '@open-mercato/shared/modules/events'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI Assistant Module Events
|
|
5
|
+
*
|
|
6
|
+
* Typed declarations for the pending-action lifecycle events emitted by
|
|
7
|
+
* the Phase 3 WS-C mutation approval flow. The event IDs are FROZEN per
|
|
8
|
+
* `BACKWARD_COMPATIBILITY.md` §5 (contract surface #5) and MUST NOT be
|
|
9
|
+
* renamed; additive payload changes are allowed.
|
|
10
|
+
*
|
|
11
|
+
* - `ai.action.confirmed` — emitted by `executePendingActionConfirm`
|
|
12
|
+
* (Step 5.8) after the `pending → confirmed → executing → {confirmed|
|
|
13
|
+
* failed}` transition. The handler's outcome lives in
|
|
14
|
+
* `executionResult`; partial-stale rows carry the surviving stale
|
|
15
|
+
* records via `failedRecords`.
|
|
16
|
+
* - `ai.action.cancelled` — emitted by `executePendingActionCancel`
|
|
17
|
+
* (Step 5.9) after the atomic `pending → cancelled` transition.
|
|
18
|
+
* - `ai.action.expired` — emitted by the Step 5.9 expired short-circuit
|
|
19
|
+
* AND by the Step 5.12 cleanup worker when the TTL elapses. The
|
|
20
|
+
* worker is the actor in that path, so `resolvedByUserId` is NOT part
|
|
21
|
+
* of the payload.
|
|
22
|
+
*/
|
|
23
|
+
const events = [
|
|
24
|
+
{
|
|
25
|
+
id: 'ai.action.confirmed',
|
|
26
|
+
label: 'AI Pending Action Confirmed',
|
|
27
|
+
entity: 'ai_pending_action',
|
|
28
|
+
category: 'system' as const,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'ai.action.cancelled',
|
|
32
|
+
label: 'AI Pending Action Cancelled',
|
|
33
|
+
entity: 'ai_pending_action',
|
|
34
|
+
category: 'system' as const,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'ai.action.expired',
|
|
38
|
+
label: 'AI Pending Action Expired',
|
|
39
|
+
entity: 'ai_pending_action',
|
|
40
|
+
category: 'system' as const,
|
|
41
|
+
},
|
|
42
|
+
] as const
|
|
43
|
+
|
|
44
|
+
export const eventsConfig = createModuleEvents({
|
|
45
|
+
moduleId: 'ai_assistant',
|
|
46
|
+
events,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
/** Type-safe event emitter for the ai_assistant module. */
|
|
50
|
+
export const emitAiAssistantEvent = eventsConfig.emit
|
|
51
|
+
|
|
52
|
+
/** Event IDs declared by the ai_assistant module. */
|
|
53
|
+
export type AiAssistantEventId = (typeof events)[number]['id']
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Typed payload contracts for each ai_assistant event. Payloads are
|
|
57
|
+
* additive-only — extend existing fields rather than renaming/removing.
|
|
58
|
+
*/
|
|
59
|
+
export interface AiActionFailedRecordPayload {
|
|
60
|
+
recordId: string
|
|
61
|
+
error: { code: string; message: string }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AiActionExecutionResultPayload {
|
|
65
|
+
recordId?: string
|
|
66
|
+
commandName?: string
|
|
67
|
+
error?: { code: string; message: string }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AiActionConfirmedPayload {
|
|
71
|
+
pendingActionId: string
|
|
72
|
+
agentId: string
|
|
73
|
+
toolName: string
|
|
74
|
+
status: string
|
|
75
|
+
tenantId: string | null
|
|
76
|
+
organizationId: string | null
|
|
77
|
+
userId: string
|
|
78
|
+
resolvedByUserId: string
|
|
79
|
+
resolvedAt: string
|
|
80
|
+
executionResult: AiActionExecutionResultPayload | null
|
|
81
|
+
failedRecords?: AiActionFailedRecordPayload[] | null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface AiActionCancelledPayload {
|
|
85
|
+
pendingActionId: string
|
|
86
|
+
agentId: string
|
|
87
|
+
toolName: string
|
|
88
|
+
status: string
|
|
89
|
+
tenantId: string | null
|
|
90
|
+
organizationId: string | null
|
|
91
|
+
userId: string
|
|
92
|
+
resolvedByUserId: string
|
|
93
|
+
resolvedAt: string
|
|
94
|
+
executionResult: AiActionExecutionResultPayload | null
|
|
95
|
+
reason?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface AiActionExpiredPayload {
|
|
99
|
+
pendingActionId: string
|
|
100
|
+
agentId: string
|
|
101
|
+
toolName: string
|
|
102
|
+
status: string
|
|
103
|
+
tenantId: string | null
|
|
104
|
+
organizationId: string | null
|
|
105
|
+
userId: string | null
|
|
106
|
+
resolvedByUserId: null
|
|
107
|
+
resolvedAt: string
|
|
108
|
+
expiresAt?: string
|
|
109
|
+
expiredAt?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default eventsConfig
|