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