@open-mercato/ai-assistant 0.10.1-develop.7728.1.6b52eb2849 → 0.10.1-develop.7741.1.61a2652671

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/index.js +2 -0
  3. package/dist/index.js.map +2 -2
  4. package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-OWNER-008-pending-action-owner-and-tool-features.spec.js +186 -0
  5. package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-OWNER-008-pending-action-owner-and-tool-features.spec.js.map +7 -0
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-OWNER-009-approval-cards-ui.spec.js +149 -0
  7. package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-OWNER-009-approval-cards-ui.spec.js.map +7 -0
  8. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js +18 -0
  9. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js.map +2 -2
  10. package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js +21 -5
  11. package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js.map +2 -2
  12. package/dist/modules/ai_assistant/i18n/de.json +5 -0
  13. package/dist/modules/ai_assistant/i18n/en.json +5 -0
  14. package/dist/modules/ai_assistant/i18n/es.json +5 -0
  15. package/dist/modules/ai_assistant/i18n/ko.json +5 -0
  16. package/dist/modules/ai_assistant/i18n/pl.json +5 -0
  17. package/dist/modules/ai_assistant/lib/pending-action-executor.js +16 -0
  18. package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +2 -2
  19. package/dist/modules/ai_assistant/lib/pending-action-messages.js +12 -0
  20. package/dist/modules/ai_assistant/lib/pending-action-messages.js.map +7 -0
  21. package/dist/modules/ai_assistant/lib/pending-action-recheck.js +17 -0
  22. package/dist/modules/ai_assistant/lib/pending-action-recheck.js.map +2 -2
  23. package/dist/modules/ai_assistant/lib/prepare-mutation.js +4 -2
  24. package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +2 -2
  25. package/package.json +6 -6
  26. package/src/index.ts +1 -0
  27. package/src/modules/ai_assistant/__integration__/TC-AI-ACTIONS-OWNER-008-pending-action-owner-and-tool-features.spec.ts +228 -0
  28. package/src/modules/ai_assistant/__integration__/TC-AI-ACTIONS-OWNER-009-approval-cards-ui.spec.ts +181 -0
  29. package/src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts +25 -0
  30. package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/__tests__/route.test.ts +41 -0
  31. package/src/modules/ai_assistant/data/repositories/AiPendingActionRepository.ts +40 -15
  32. package/src/modules/ai_assistant/data/repositories/__tests__/AiPendingActionRepository.test.ts +105 -23
  33. package/src/modules/ai_assistant/i18n/de.json +5 -0
  34. package/src/modules/ai_assistant/i18n/en.json +5 -0
  35. package/src/modules/ai_assistant/i18n/es.json +5 -0
  36. package/src/modules/ai_assistant/i18n/ko.json +5 -0
  37. package/src/modules/ai_assistant/i18n/pl.json +5 -0
  38. package/src/modules/ai_assistant/lib/__tests__/pending-action-executor.test.ts +84 -0
  39. package/src/modules/ai_assistant/lib/__tests__/pending-action-recheck.test.ts +54 -0
  40. package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +44 -5
  41. package/src/modules/ai_assistant/lib/pending-action-executor.ts +19 -0
  42. package/src/modules/ai_assistant/lib/pending-action-messages.ts +15 -0
  43. package/src/modules/ai_assistant/lib/pending-action-recheck.ts +27 -0
  44. package/src/modules/ai_assistant/lib/prepare-mutation.ts +7 -0
@@ -1,2 +1,2 @@
1
- [build:ai-assistant] found 244 entry points
1
+ [build:ai-assistant] found 247 entry points
2
2
  [build:ai-assistant] built successfully
package/dist/index.js CHANGED
@@ -84,6 +84,7 @@ import {
84
84
  checkStatusAndExpiry,
85
85
  checkAgentAndFeatures,
86
86
  checkToolWhitelist,
87
+ checkToolFeatures,
87
88
  checkAttachmentScope,
88
89
  checkRecordVersion,
89
90
  PENDING_ACTION_RECHECK_CODES,
@@ -154,6 +155,7 @@ export {
154
155
  checkAttachmentScope,
155
156
  checkRecordVersion,
156
157
  checkStatusAndExpiry,
158
+ checkToolFeatures,
157
159
  checkToolWhitelist,
158
160
  composeSystemPrompt,
159
161
  computeMutationIdempotencyKey,
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["/**\n * @open-mercato/ai-assistant\n *\n * MCP (Model Context Protocol) server module for AI assistant integration.\n *\n * This module provides:\n * - MCP server with stdio transport for Claude Desktop integration\n * - Tool registry for modules to register AI-callable tools\n * - ACL-based permission filtering for tools\n * - Multi-tenant execution context\n *\n * @example\n * ```typescript\n * import { registerMcpTool } from '@open-mercato/ai-assistant/tools'\n * import { z } from 'zod'\n *\n * registerMcpTool({\n * name: 'customers.search',\n * description: 'Search for customers',\n * inputSchema: z.object({ query: z.string() }),\n * requiredFeatures: ['customers.people.view'],\n * handler: async (input, ctx) => {\n * // Implementation\n * }\n * }, { moduleId: 'customers' })\n * ```\n */\n\n// Re-export types\nexport * from './modules/ai_assistant/lib/types'\n\n// Focused-agent definition types + helper\nexport {\n defineAiAgent,\n defineAiAgentExtension,\n type AiAgentDefinition,\n type AiAgentExtension,\n type AiAgentSuggestion,\n type AiAgentExecutionMode,\n type AiAgentMutationPolicy,\n type AiAgentAcceptedMediaType,\n type AiAgentDataOperation,\n type AiAgentPageContextInput,\n type AiAgentStructuredOutput,\n type AiAgentDataCapabilities,\n} from './modules/ai_assistant/lib/ai-agent-definition'\n\n// Additive AI tool builder\nexport { defineAiTool } from './modules/ai_assistant/lib/ai-tool-definition'\n\n// Attachment-bridge contract types (spec Phase 0 \u00A78/\u00A710, implementation-ready for Phase 3 runtime)\nexport {\n type AttachmentSource,\n type AiResolvedAttachmentPart,\n type AiUiPart,\n type AiChatRequestContext,\n} from './modules/ai_assistant/lib/attachment-bridge-types'\n\n// Prompt-composition primitives (spec Phase 0 \u00A78, implementation-ready for Phase 3 prompt composer)\nexport {\n definePromptTemplate,\n type PromptSectionName,\n type PromptSection,\n type PromptTemplate,\n} from './modules/ai_assistant/lib/prompt-composition-types'\n\n// Tool registry\nexport {\n registerMcpTool,\n getToolRegistry,\n unregisterMcpTool,\n toolRegistry,\n} from './modules/ai_assistant/lib/tool-registry'\n\n// Tool executor\nexport { executeTool } from './modules/ai_assistant/lib/tool-executor'\n\n// MCP server (stdio)\nexport { createMcpServer, runMcpServer } from './modules/ai_assistant/lib/mcp-server'\n\n// MCP HTTP server\nexport { runMcpHttpServer, type McpHttpServerOptions } from './modules/ai_assistant/lib/http-server'\n\n// MCP auth\nexport {\n authenticateMcpRequest,\n extractApiKeyFromHeaders,\n type McpAuthResult,\n type McpAuthSuccess,\n type McpAuthFailure,\n} from './modules/ai_assistant/lib/auth'\n\n// Tool loader\nexport { loadAllModuleTools, indexToolsForSearch } from './modules/ai_assistant/lib/tool-loader'\n\n// AI Overrides \u2014 module-to-module + modules.ts + programmatic agent/tool replacement.\n// See `apps/docs/docs/framework/ai-assistant/overrides.mdx`.\nexport {\n applyAiAgentOverrides,\n applyAiAgentExtensions,\n applyAiToolOverrides,\n applyAiOverridesFromEnabledModules,\n resetProgrammaticOverridesForTests,\n type AiAgentOverride,\n type AiToolOverride,\n type AiAgentOverridesMap,\n type AiToolOverridesMap,\n type AiAgentOverrideConfigEntry,\n type AiAgentExtensionConfigEntry,\n type AiToolOverrideConfigEntry,\n type EnabledModuleAiOverrides,\n} from './modules/ai_assistant/lib/ai-overrides'\n\n// Agent registry (Phase 1 WS-A \u2014 read-side lookup API, no policy / dispatch)\nexport {\n loadAgentRegistry,\n getAgent,\n listAgents,\n listAgentsByModule,\n resetAgentRegistryForTests,\n} from './modules/ai_assistant/lib/agent-registry'\n\n// Agent runtime policy gate (Phase 1 WS-A \u2014 pure policy decisions, no HTTP or AI SDK wiring)\nexport {\n checkAgentPolicy,\n type AgentPolicyDenyCode,\n type AgentPolicyDecision,\n type AgentPolicyAuthContext,\n type AgentPolicyCheckInput,\n} from './modules/ai_assistant/lib/agent-policy'\n\n// AI SDK helpers (Phase 1 WS-B \u2014 chat-mode runtime + transport glue)\nexport {\n resolveAiAgentTools,\n AgentPolicyError,\n type ResolveAiAgentToolsInput,\n type ResolvedAgentTools,\n type AiUiPartQueue,\n} from './modules/ai_assistant/lib/agent-tools'\n\n// In-process API operation runner (Phase 1 of API-backed AI tool DRY refactor)\nexport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiApiOperationResponse,\n type AiApiOperationRunner,\n type AiApiOperationRunnerOptions,\n type AiApiHttpMethod,\n type AiToolExecutionContext,\n} from './modules/ai_assistant/lib/ai-api-operation-runner'\n\n// API-backed AI tool helper (Phase 2 of API-backed AI tool DRY refactor)\nexport {\n defineApiBackedAiTool,\n type ApiBackedAiToolConfig,\n} from './modules/ai_assistant/lib/api-backed-tool'\n\n// Mutation-preparation helper (Phase 3 WS-C \u2014 Step 5.6)\nexport {\n prepareMutation,\n computeMutationIdempotencyKey,\n AiMutationPreparationError,\n MUTATION_PREVIEW_CARD_COMPONENT,\n type PrepareMutationInput,\n type PrepareMutationContext,\n type PrepareMutationResult,\n} from './modules/ai_assistant/lib/prepare-mutation'\n\nexport {\n runAiAgentText,\n runAiAgentObject,\n composeSystemPrompt,\n type RunAiAgentTextInput,\n type RunAiAgentObjectInput,\n type RunAiAgentObjectOutputOverride,\n type RunAiAgentObjectResult,\n type RunAiAgentObjectGenerateResult,\n type RunAiAgentObjectStreamResult,\n type AgentRequestPageContext,\n} from './modules/ai_assistant/lib/agent-runtime'\n\nexport {\n createAiAgentTransport,\n type CreateAiAgentTransportInput,\n} from './modules/ai_assistant/lib/agent-transport'\n\n// Pending-action types / enums / state-machine guard (Phase 3 WS-C \u2014 Step 5.5)\nexport {\n AI_PENDING_ACTION_STATUSES,\n AI_PENDING_ACTION_QUEUE_MODES,\n AI_PENDING_ACTION_ALLOWED_TRANSITIONS,\n AI_PENDING_ACTION_TERMINAL_STATUSES,\n AI_PENDING_ACTION_DEFAULT_TTL_SECONDS,\n AI_PENDING_ACTION_TTL_ENV_VAR,\n AiPendingActionStateError,\n isAiPendingActionStatus,\n isAiPendingActionQueueMode,\n isTerminalAiPendingActionStatus,\n isAllowedAiPendingActionTransition,\n resolveAiPendingActionTtlSeconds,\n type AiPendingActionStatus,\n type AiPendingActionQueueMode,\n type AiPendingActionRecordDiff,\n type AiPendingActionFailedRecord,\n type AiPendingActionFieldDiff,\n type AiPendingActionExecutionResult,\n} from './modules/ai_assistant/lib/pending-action-types'\n\n// Client-facing pending-action serializer (Phase 3 WS-C \u2014 Step 5.7, reused by 5.8 / 5.9 / 5.10)\nexport {\n serializePendingActionForClient,\n type SerializedPendingAction,\n type SerializablePendingActionRow,\n} from './modules/ai_assistant/lib/pending-action-client'\n\n// Pending-action confirm re-check contract (Phase 3 WS-C \u2014 Step 5.8, reused by 5.9)\nexport {\n runPendingActionRechecks,\n checkStatusAndExpiry,\n checkAgentAndFeatures,\n checkToolWhitelist,\n checkAttachmentScope,\n checkRecordVersion,\n PENDING_ACTION_RECHECK_CODES,\n isPendingActionRecheckCode,\n type PendingActionRecheckCode,\n type PendingActionRecheckResult,\n type PendingActionRecheckInput,\n type PendingActionAuthContext,\n} from './modules/ai_assistant/lib/pending-action-recheck'\n\n// Pending-action confirm executor (Phase 3 WS-C \u2014 Step 5.8)\nexport {\n executePendingActionConfirm,\n PENDING_ACTION_CONFIRMED_EVENT_ID,\n type PendingActionExecuteInput,\n type PendingActionExecuteContext,\n type PendingActionExecuteResult,\n type PendingActionExecuteOk,\n type PendingActionExecuteFail,\n} from './modules/ai_assistant/lib/pending-action-executor'\n\n// Pending-action cancel executor (Phase 3 WS-C \u2014 Step 5.9)\nexport {\n executePendingActionCancel,\n PENDING_ACTION_CANCELLED_EVENT_ID,\n PENDING_ACTION_EXPIRED_EVENT_ID,\n type PendingActionCancelInput,\n type PendingActionCancelContext,\n type PendingActionCancelResult,\n type PendingActionCancelStatus,\n} from './modules/ai_assistant/lib/pending-action-cancel'\n\n// Shared AI model factory (Phase 3 WS-A \u2014 Step 5.1)\nexport {\n createModelFactory,\n AiModelFactoryError,\n type AiModelFactory,\n type AiModelFactoryInput,\n type AiModelResolution,\n type AiModelFactoryErrorCode,\n type AiModelInstance,\n type CreateModelFactoryDependencies,\n} from './modules/ai_assistant/lib/model-factory'\n\n// Attachment-to-model bridge (Phase 1 WS-C \u2014 Step 3.7)\nexport {\n resolveAttachmentParts,\n resolveAttachmentPartsForAgent,\n attachmentPartsToUiFileParts,\n summarizeAttachmentPartsForPrompt,\n type ResolveAttachmentPartsInput,\n type AttachmentSigner,\n} from './modules/ai_assistant/lib/attachment-parts'\n\n// OpenCode client\nexport {\n OpenCodeClient,\n createOpenCodeClient,\n type OpenCodeClientConfig,\n type OpenCodeSession,\n type OpenCodeMessage,\n type OpenCodeHealth,\n type OpenCodeMcpStatus,\n} from './modules/ai_assistant/lib/opencode-client'\n\n// OpenCode route handlers\nexport {\n handleOpenCodeMessage,\n handleOpenCodeHealth,\n handleOpenCodeMessageStreaming,\n handleOpenCodeAnswer,\n getPendingQuestions,\n getOwnedPendingQuestions,\n OpenCodeSessionOwnershipError,\n extractTextFromResponse,\n extractAllPartsFromResponse,\n extractMetadataFromResponse,\n type OpenCodeTestRequest,\n type OpenCodeTestResponse,\n type OpenCodeHealthResponse,\n type OpenCodeResponsePart,\n type OpenCodeResponseMetadata,\n type OpenCodeStreamEvent,\n type OpenCodeQuestion,\n type OpenCodeAuthContext,\n type OpenCodeAnswerOwnershipOptions,\n} from './modules/ai_assistant/lib/opencode-handlers'\n\n// Module metadata\nexport { metadata, features } from './modules/ai_assistant'\n"],
5
- "mappings": "AA6BA,cAAc;AAGd;AAAA,EACE;AAAA,EACA;AAAA,OAWK;AAGP,SAAS,oBAAoB;AAW7B;AAAA,EACE;AAAA,OAIK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,mBAAmB;AAG5B,SAAS,iBAAiB,oBAAoB;AAG9C,SAAS,wBAAmD;AAG5D;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAGP,SAAS,oBAAoB,2BAA2B;AAIxD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,OAKK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAGP;AAAA,EACE;AAAA,OAOK;AAGP;AAAA,EACE;AAAA,OAEK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAQK;AAEP;AAAA,EACE;AAAA,OAEK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAOK;AAGP;AAAA,EACE;AAAA,OAGK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,OAMK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,OAOK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,OAMK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAUK;AAGP,SAAS,UAAU,gBAAgB;",
4
+ "sourcesContent": ["/**\n * @open-mercato/ai-assistant\n *\n * MCP (Model Context Protocol) server module for AI assistant integration.\n *\n * This module provides:\n * - MCP server with stdio transport for Claude Desktop integration\n * - Tool registry for modules to register AI-callable tools\n * - ACL-based permission filtering for tools\n * - Multi-tenant execution context\n *\n * @example\n * ```typescript\n * import { registerMcpTool } from '@open-mercato/ai-assistant/tools'\n * import { z } from 'zod'\n *\n * registerMcpTool({\n * name: 'customers.search',\n * description: 'Search for customers',\n * inputSchema: z.object({ query: z.string() }),\n * requiredFeatures: ['customers.people.view'],\n * handler: async (input, ctx) => {\n * // Implementation\n * }\n * }, { moduleId: 'customers' })\n * ```\n */\n\n// Re-export types\nexport * from './modules/ai_assistant/lib/types'\n\n// Focused-agent definition types + helper\nexport {\n defineAiAgent,\n defineAiAgentExtension,\n type AiAgentDefinition,\n type AiAgentExtension,\n type AiAgentSuggestion,\n type AiAgentExecutionMode,\n type AiAgentMutationPolicy,\n type AiAgentAcceptedMediaType,\n type AiAgentDataOperation,\n type AiAgentPageContextInput,\n type AiAgentStructuredOutput,\n type AiAgentDataCapabilities,\n} from './modules/ai_assistant/lib/ai-agent-definition'\n\n// Additive AI tool builder\nexport { defineAiTool } from './modules/ai_assistant/lib/ai-tool-definition'\n\n// Attachment-bridge contract types (spec Phase 0 \u00A78/\u00A710, implementation-ready for Phase 3 runtime)\nexport {\n type AttachmentSource,\n type AiResolvedAttachmentPart,\n type AiUiPart,\n type AiChatRequestContext,\n} from './modules/ai_assistant/lib/attachment-bridge-types'\n\n// Prompt-composition primitives (spec Phase 0 \u00A78, implementation-ready for Phase 3 prompt composer)\nexport {\n definePromptTemplate,\n type PromptSectionName,\n type PromptSection,\n type PromptTemplate,\n} from './modules/ai_assistant/lib/prompt-composition-types'\n\n// Tool registry\nexport {\n registerMcpTool,\n getToolRegistry,\n unregisterMcpTool,\n toolRegistry,\n} from './modules/ai_assistant/lib/tool-registry'\n\n// Tool executor\nexport { executeTool } from './modules/ai_assistant/lib/tool-executor'\n\n// MCP server (stdio)\nexport { createMcpServer, runMcpServer } from './modules/ai_assistant/lib/mcp-server'\n\n// MCP HTTP server\nexport { runMcpHttpServer, type McpHttpServerOptions } from './modules/ai_assistant/lib/http-server'\n\n// MCP auth\nexport {\n authenticateMcpRequest,\n extractApiKeyFromHeaders,\n type McpAuthResult,\n type McpAuthSuccess,\n type McpAuthFailure,\n} from './modules/ai_assistant/lib/auth'\n\n// Tool loader\nexport { loadAllModuleTools, indexToolsForSearch } from './modules/ai_assistant/lib/tool-loader'\n\n// AI Overrides \u2014 module-to-module + modules.ts + programmatic agent/tool replacement.\n// See `apps/docs/docs/framework/ai-assistant/overrides.mdx`.\nexport {\n applyAiAgentOverrides,\n applyAiAgentExtensions,\n applyAiToolOverrides,\n applyAiOverridesFromEnabledModules,\n resetProgrammaticOverridesForTests,\n type AiAgentOverride,\n type AiToolOverride,\n type AiAgentOverridesMap,\n type AiToolOverridesMap,\n type AiAgentOverrideConfigEntry,\n type AiAgentExtensionConfigEntry,\n type AiToolOverrideConfigEntry,\n type EnabledModuleAiOverrides,\n} from './modules/ai_assistant/lib/ai-overrides'\n\n// Agent registry (Phase 1 WS-A \u2014 read-side lookup API, no policy / dispatch)\nexport {\n loadAgentRegistry,\n getAgent,\n listAgents,\n listAgentsByModule,\n resetAgentRegistryForTests,\n} from './modules/ai_assistant/lib/agent-registry'\n\n// Agent runtime policy gate (Phase 1 WS-A \u2014 pure policy decisions, no HTTP or AI SDK wiring)\nexport {\n checkAgentPolicy,\n type AgentPolicyDenyCode,\n type AgentPolicyDecision,\n type AgentPolicyAuthContext,\n type AgentPolicyCheckInput,\n} from './modules/ai_assistant/lib/agent-policy'\n\n// AI SDK helpers (Phase 1 WS-B \u2014 chat-mode runtime + transport glue)\nexport {\n resolveAiAgentTools,\n AgentPolicyError,\n type ResolveAiAgentToolsInput,\n type ResolvedAgentTools,\n type AiUiPartQueue,\n} from './modules/ai_assistant/lib/agent-tools'\n\n// In-process API operation runner (Phase 1 of API-backed AI tool DRY refactor)\nexport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiApiOperationResponse,\n type AiApiOperationRunner,\n type AiApiOperationRunnerOptions,\n type AiApiHttpMethod,\n type AiToolExecutionContext,\n} from './modules/ai_assistant/lib/ai-api-operation-runner'\n\n// API-backed AI tool helper (Phase 2 of API-backed AI tool DRY refactor)\nexport {\n defineApiBackedAiTool,\n type ApiBackedAiToolConfig,\n} from './modules/ai_assistant/lib/api-backed-tool'\n\n// Mutation-preparation helper (Phase 3 WS-C \u2014 Step 5.6)\nexport {\n prepareMutation,\n computeMutationIdempotencyKey,\n AiMutationPreparationError,\n MUTATION_PREVIEW_CARD_COMPONENT,\n type PrepareMutationInput,\n type PrepareMutationContext,\n type PrepareMutationResult,\n} from './modules/ai_assistant/lib/prepare-mutation'\n\nexport {\n runAiAgentText,\n runAiAgentObject,\n composeSystemPrompt,\n type RunAiAgentTextInput,\n type RunAiAgentObjectInput,\n type RunAiAgentObjectOutputOverride,\n type RunAiAgentObjectResult,\n type RunAiAgentObjectGenerateResult,\n type RunAiAgentObjectStreamResult,\n type AgentRequestPageContext,\n} from './modules/ai_assistant/lib/agent-runtime'\n\nexport {\n createAiAgentTransport,\n type CreateAiAgentTransportInput,\n} from './modules/ai_assistant/lib/agent-transport'\n\n// Pending-action types / enums / state-machine guard (Phase 3 WS-C \u2014 Step 5.5)\nexport {\n AI_PENDING_ACTION_STATUSES,\n AI_PENDING_ACTION_QUEUE_MODES,\n AI_PENDING_ACTION_ALLOWED_TRANSITIONS,\n AI_PENDING_ACTION_TERMINAL_STATUSES,\n AI_PENDING_ACTION_DEFAULT_TTL_SECONDS,\n AI_PENDING_ACTION_TTL_ENV_VAR,\n AiPendingActionStateError,\n isAiPendingActionStatus,\n isAiPendingActionQueueMode,\n isTerminalAiPendingActionStatus,\n isAllowedAiPendingActionTransition,\n resolveAiPendingActionTtlSeconds,\n type AiPendingActionStatus,\n type AiPendingActionQueueMode,\n type AiPendingActionRecordDiff,\n type AiPendingActionFailedRecord,\n type AiPendingActionFieldDiff,\n type AiPendingActionExecutionResult,\n} from './modules/ai_assistant/lib/pending-action-types'\n\n// Client-facing pending-action serializer (Phase 3 WS-C \u2014 Step 5.7, reused by 5.8 / 5.9 / 5.10)\nexport {\n serializePendingActionForClient,\n type SerializedPendingAction,\n type SerializablePendingActionRow,\n} from './modules/ai_assistant/lib/pending-action-client'\n\n// Pending-action confirm re-check contract (Phase 3 WS-C \u2014 Step 5.8, reused by 5.9)\nexport {\n runPendingActionRechecks,\n checkStatusAndExpiry,\n checkAgentAndFeatures,\n checkToolWhitelist,\n checkToolFeatures,\n checkAttachmentScope,\n checkRecordVersion,\n PENDING_ACTION_RECHECK_CODES,\n isPendingActionRecheckCode,\n type PendingActionRecheckCode,\n type PendingActionRecheckResult,\n type PendingActionRecheckInput,\n type PendingActionAuthContext,\n} from './modules/ai_assistant/lib/pending-action-recheck'\n\n// Pending-action confirm executor (Phase 3 WS-C \u2014 Step 5.8)\nexport {\n executePendingActionConfirm,\n PENDING_ACTION_CONFIRMED_EVENT_ID,\n type PendingActionExecuteInput,\n type PendingActionExecuteContext,\n type PendingActionExecuteResult,\n type PendingActionExecuteOk,\n type PendingActionExecuteFail,\n} from './modules/ai_assistant/lib/pending-action-executor'\n\n// Pending-action cancel executor (Phase 3 WS-C \u2014 Step 5.9)\nexport {\n executePendingActionCancel,\n PENDING_ACTION_CANCELLED_EVENT_ID,\n PENDING_ACTION_EXPIRED_EVENT_ID,\n type PendingActionCancelInput,\n type PendingActionCancelContext,\n type PendingActionCancelResult,\n type PendingActionCancelStatus,\n} from './modules/ai_assistant/lib/pending-action-cancel'\n\n// Shared AI model factory (Phase 3 WS-A \u2014 Step 5.1)\nexport {\n createModelFactory,\n AiModelFactoryError,\n type AiModelFactory,\n type AiModelFactoryInput,\n type AiModelResolution,\n type AiModelFactoryErrorCode,\n type AiModelInstance,\n type CreateModelFactoryDependencies,\n} from './modules/ai_assistant/lib/model-factory'\n\n// Attachment-to-model bridge (Phase 1 WS-C \u2014 Step 3.7)\nexport {\n resolveAttachmentParts,\n resolveAttachmentPartsForAgent,\n attachmentPartsToUiFileParts,\n summarizeAttachmentPartsForPrompt,\n type ResolveAttachmentPartsInput,\n type AttachmentSigner,\n} from './modules/ai_assistant/lib/attachment-parts'\n\n// OpenCode client\nexport {\n OpenCodeClient,\n createOpenCodeClient,\n type OpenCodeClientConfig,\n type OpenCodeSession,\n type OpenCodeMessage,\n type OpenCodeHealth,\n type OpenCodeMcpStatus,\n} from './modules/ai_assistant/lib/opencode-client'\n\n// OpenCode route handlers\nexport {\n handleOpenCodeMessage,\n handleOpenCodeHealth,\n handleOpenCodeMessageStreaming,\n handleOpenCodeAnswer,\n getPendingQuestions,\n getOwnedPendingQuestions,\n OpenCodeSessionOwnershipError,\n extractTextFromResponse,\n extractAllPartsFromResponse,\n extractMetadataFromResponse,\n type OpenCodeTestRequest,\n type OpenCodeTestResponse,\n type OpenCodeHealthResponse,\n type OpenCodeResponsePart,\n type OpenCodeResponseMetadata,\n type OpenCodeStreamEvent,\n type OpenCodeQuestion,\n type OpenCodeAuthContext,\n type OpenCodeAnswerOwnershipOptions,\n} from './modules/ai_assistant/lib/opencode-handlers'\n\n// Module metadata\nexport { metadata, features } from './modules/ai_assistant'\n"],
5
+ "mappings": "AA6BA,cAAc;AAGd;AAAA,EACE;AAAA,EACA;AAAA,OAWK;AAGP,SAAS,oBAAoB;AAW7B;AAAA,EACE;AAAA,OAIK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,mBAAmB;AAG5B,SAAS,iBAAiB,oBAAoB;AAG9C,SAAS,wBAAmD;AAG5D;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAGP,SAAS,oBAAoB,2BAA2B;AAIxD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,OAKK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAGP;AAAA,EACE;AAAA,OAOK;AAGP;AAAA,EACE;AAAA,OAEK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAQK;AAEP;AAAA,EACE;AAAA,OAEK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAOK;AAGP;AAAA,EACE;AAAA,OAGK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,OAMK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,OAOK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,OAMK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAUK;AAGP,SAAS,UAAU,gBAAgB;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,186 @@
1
+ import { test, expect } from "@playwright/test";
2
+ import { randomUUID } from "node:crypto";
3
+ import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
4
+ import {
5
+ createRoleFixture,
6
+ deleteRoleIfExists,
7
+ createUserFixture,
8
+ deleteUserIfExists,
9
+ setRoleAclFeatures
10
+ } from "@open-mercato/core/helpers/integration/authFixtures";
11
+ import { deleteUserAclInDb } from "@open-mercato/core/helpers/integration/dbFixtures";
12
+ import { createProductFixture, deleteCatalogProductIfExists } from "@open-mercato/core/helpers/integration/catalogFixtures";
13
+ import { getTokenScope, readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
14
+ import {
15
+ seedPendingActionInDb,
16
+ deletePendingActionInDb,
17
+ readPendingActionStateInDb,
18
+ readCatalogProductTitleInDb
19
+ } from "./helpers/aiAssistantFixtures.js";
20
+ const ACTIONS = "/api/ai_assistant/ai/actions";
21
+ const AGENT_ID = "catalog.merchandising_assistant";
22
+ const TOOL_NAME = "catalog.update_product";
23
+ const PASSWORD = "Secret123!";
24
+ async function createScopedUser(request, adminToken, organizationId, label, features, cleanups) {
25
+ const stamp = randomUUID().slice(0, 8);
26
+ const email = `it-owner-${label}-${stamp}@example.com`;
27
+ const roleId = await createRoleFixture(request, adminToken, { name: `IT Owner ${label} ${stamp}` });
28
+ cleanups.push(() => deleteRoleIfExists(request, adminToken, roleId));
29
+ await setRoleAclFeatures(request, adminToken, { roleId, features, organizations: null });
30
+ const userId = await createUserFixture(request, adminToken, {
31
+ email,
32
+ password: PASSWORD,
33
+ organizationId,
34
+ roles: [roleId]
35
+ });
36
+ cleanups.push(async () => {
37
+ await deleteUserAclInDb(userId).catch(() => void 0);
38
+ await deleteUserIfExists(request, adminToken, userId);
39
+ });
40
+ const token = await getAuthToken(request, email, PASSWORD);
41
+ return { userId, roleId, token };
42
+ }
43
+ test.describe("TC-AI-ACTIONS-OWNER-008: pending action owner scope and tool feature recheck", () => {
44
+ test("another user in the same organization cannot read, cancel or confirm a pending action", async ({ request }) => {
45
+ test.slow();
46
+ const adminToken = await getAuthToken(request, "admin");
47
+ const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);
48
+ const cleanups = [];
49
+ try {
50
+ const productId = await createProductFixture(request, adminToken, {
51
+ title: `IT owner product ${randomUUID().slice(0, 8)}`,
52
+ sku: `IT-OWN-${randomUUID().slice(0, 8)}`
53
+ });
54
+ cleanups.push(() => deleteCatalogProductIfExists(request, adminToken, productId));
55
+ const originalTitle = await readCatalogProductTitleInDb(productId);
56
+ const row = await seedPendingActionInDb({
57
+ tenantId,
58
+ organizationId: organizationId || null,
59
+ createdByUserId: adminId,
60
+ agentId: AGENT_ID,
61
+ toolName: TOOL_NAME,
62
+ normalizedInput: { productId, title: "Changed by another user" }
63
+ });
64
+ cleanups.push(() => deletePendingActionInDb(row.id));
65
+ const other = await createScopedUser(request, adminToken, organizationId, "other", [
66
+ "ai_assistant.view",
67
+ "catalog.products.view",
68
+ "catalog.products.manage"
69
+ ], cleanups);
70
+ const read = await apiRequest(request, "GET", `${ACTIONS}/${row.id}`, { token: other.token });
71
+ expect(read.status(), "non-owner GET is indistinguishable from an unknown id").toBe(404);
72
+ expect((await readJsonSafe(read))?.code).toBe("pending_action_not_found");
73
+ const cancel = await apiRequest(request, "POST", `${ACTIONS}/${row.id}/cancel`, {
74
+ token: other.token,
75
+ data: { reason: "not mine" }
76
+ });
77
+ expect(cancel.status(), "non-owner cancel is rejected").toBe(404);
78
+ expect((await readJsonSafe(cancel))?.code).toBe("pending_action_not_found");
79
+ const confirm = await apiRequest(request, "POST", `${ACTIONS}/${row.id}/confirm`, {
80
+ token: other.token,
81
+ data: {}
82
+ });
83
+ expect(confirm.status(), "non-owner confirm is rejected").toBe(404);
84
+ expect((await readJsonSafe(confirm))?.code).toBe("pending_action_not_found");
85
+ expect(await readPendingActionStateInDb(row.id)).toEqual({ status: "pending", resolvedByUserId: null });
86
+ expect(await readCatalogProductTitleInDb(productId)).toBe(originalTitle);
87
+ const ownerRead = await apiRequest(request, "GET", `${ACTIONS}/${row.id}`, { token: adminToken });
88
+ expect(ownerRead.status(), "the creator still reads their own action").toBe(200);
89
+ const ownerCancel = await apiRequest(request, "POST", `${ACTIONS}/${row.id}/cancel`, {
90
+ token: adminToken,
91
+ data: {}
92
+ });
93
+ expect(ownerCancel.status(), "the creator still cancels their own action").toBe(200);
94
+ expect(await readPendingActionStateInDb(row.id)).toEqual({ status: "cancelled", resolvedByUserId: adminId });
95
+ } finally {
96
+ for (const cleanup of cleanups.reverse()) {
97
+ await cleanup().catch(() => void 0);
98
+ }
99
+ }
100
+ });
101
+ test("confirm re-checks the tool features after they are revoked from the proposer", async ({ request }) => {
102
+ test.slow();
103
+ const adminToken = await getAuthToken(request, "admin");
104
+ const { tenantId, organizationId } = getTokenScope(adminToken);
105
+ const cleanups = [];
106
+ try {
107
+ const productId = await createProductFixture(request, adminToken, {
108
+ title: `IT feature product ${randomUUID().slice(0, 8)}`,
109
+ sku: `IT-FEAT-${randomUUID().slice(0, 8)}`
110
+ });
111
+ cleanups.push(() => deleteCatalogProductIfExists(request, adminToken, productId));
112
+ const originalTitle = await readCatalogProductTitleInDb(productId);
113
+ const viewer = await createScopedUser(request, adminToken, organizationId, "revoked", [
114
+ "ai_assistant.view",
115
+ "catalog.products.view",
116
+ "catalog.products.manage"
117
+ ], cleanups);
118
+ const viewerRow = await seedPendingActionInDb({
119
+ tenantId,
120
+ organizationId: organizationId || null,
121
+ createdByUserId: viewer.userId,
122
+ agentId: AGENT_ID,
123
+ toolName: TOOL_NAME,
124
+ normalizedInput: { productId, title: "Changed without manage" }
125
+ });
126
+ cleanups.push(() => deletePendingActionInDb(viewerRow.id));
127
+ await setRoleAclFeatures(request, adminToken, {
128
+ roleId: viewer.roleId,
129
+ features: ["ai_assistant.view", "catalog.products.view"],
130
+ organizations: null
131
+ });
132
+ const denied = await apiRequest(request, "POST", `${ACTIONS}/${viewerRow.id}/confirm`, {
133
+ token: viewer.token,
134
+ data: {}
135
+ });
136
+ expect(denied.status(), "owner whose tool feature was revoked cannot confirm").toBe(403);
137
+ expect((await readJsonSafe(denied))?.code).toBe("tool_features_denied");
138
+ const deniedPolish = await apiRequest(request, "POST", `${ACTIONS}/${viewerRow.id}/confirm`, {
139
+ token: viewer.token,
140
+ data: {},
141
+ headers: { "Accept-Language": "pl" }
142
+ });
143
+ expect(deniedPolish.status()).toBe(403);
144
+ expect(await readJsonSafe(deniedPolish), "the refusal is localized").toEqual({
145
+ code: "tool_features_denied",
146
+ error: "Brakuje Ci jednego z uprawnie\u0144 wymaganych przez narz\u0119dzie: catalog.products.manage"
147
+ });
148
+ expect(await readCatalogProductTitleInDb(productId)).toBe(originalTitle);
149
+ expect((await readPendingActionStateInDb(viewerRow.id))?.status).toBe("pending");
150
+ const viewerCancel = await apiRequest(request, "POST", `${ACTIONS}/${viewerRow.id}/cancel`, {
151
+ token: viewer.token,
152
+ data: {}
153
+ });
154
+ expect(viewerCancel.status(), "owner without the tool feature can still cancel").toBe(200);
155
+ const manager = await createScopedUser(request, adminToken, organizationId, "manager", [
156
+ "ai_assistant.view",
157
+ "catalog.products.view",
158
+ "catalog.products.manage"
159
+ ], cleanups);
160
+ const managerTitle = `Changed with manage ${randomUUID().slice(0, 8)}`;
161
+ const managerRow = await seedPendingActionInDb({
162
+ tenantId,
163
+ organizationId: organizationId || null,
164
+ createdByUserId: manager.userId,
165
+ agentId: AGENT_ID,
166
+ toolName: TOOL_NAME,
167
+ normalizedInput: { productId, title: managerTitle }
168
+ });
169
+ cleanups.push(() => deletePendingActionInDb(managerRow.id));
170
+ const confirmed = await apiRequest(request, "POST", `${ACTIONS}/${managerRow.id}/confirm`, {
171
+ token: manager.token,
172
+ data: {}
173
+ });
174
+ expect(confirmed.status(), "owner holding the tool feature confirms").toBe(200);
175
+ const confirmedBody = await readJsonSafe(confirmed);
176
+ expect(confirmedBody?.ok).toBe(true);
177
+ expect(confirmedBody?.pendingAction?.status).toBe("confirmed");
178
+ expect(await readCatalogProductTitleInDb(productId)).toBe(managerTitle);
179
+ } finally {
180
+ for (const cleanup of cleanups.reverse()) {
181
+ await cleanup().catch(() => void 0);
182
+ }
183
+ }
184
+ });
185
+ });
186
+ //# sourceMappingURL=TC-AI-ACTIONS-OWNER-008-pending-action-owner-and-tool-features.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-ACTIONS-OWNER-008-pending-action-owner-and-tool-features.spec.ts"],
4
+ "sourcesContent": ["import { test, expect, type APIRequestContext } from '@playwright/test';\nimport { randomUUID } from 'node:crypto';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport {\n createRoleFixture,\n deleteRoleIfExists,\n createUserFixture,\n deleteUserIfExists,\n setRoleAclFeatures,\n} from '@open-mercato/core/helpers/integration/authFixtures';\nimport { deleteUserAclInDb } from '@open-mercato/core/helpers/integration/dbFixtures';\nimport { createProductFixture, deleteCatalogProductIfExists } from '@open-mercato/core/helpers/integration/catalogFixtures';\nimport { getTokenScope, readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport {\n seedPendingActionInDb,\n deletePendingActionInDb,\n readPendingActionStateInDb,\n readCatalogProductTitleInDb,\n} from './helpers/aiAssistantFixtures';\n\n/**\n * TC-AI-ACTIONS-OWNER-008 \u2014 pending actions are resolvable only by their creator,\n * and confirm re-checks the tool's required features against the confirming user.\n * Source: GitHub issue #3806.\n *\n * Fixture: the shipped `catalog.merchandising_assistant` agent (requires\n * `catalog.products.view`, `confirm-required`) and its `catalog.update_product`\n * mutation tool (requires `catalog.products.manage`). Rows are seeded via SQL\n * because pending actions have no public create route.\n *\n * The action routes never consult conversations, so a same-organization user\n * who is not the creator stands in for a shared-conversation participant (the\n * participant learns the action id from the transcript; the route sees only\n * the id and the caller).\n */\n\nconst ACTIONS = '/api/ai_assistant/ai/actions';\nconst AGENT_ID = 'catalog.merchandising_assistant';\nconst TOOL_NAME = 'catalog.update_product';\nconst PASSWORD = 'Secret123!';\n\ntype Cleanup = () => Promise<void>;\n\nasync function createScopedUser(\n request: APIRequestContext,\n adminToken: string,\n organizationId: string,\n label: string,\n features: string[],\n cleanups: Cleanup[],\n): Promise<{ userId: string; roleId: string; token: string }> {\n const stamp = randomUUID().slice(0, 8);\n const email = `it-owner-${label}-${stamp}@example.com`;\n const roleId = await createRoleFixture(request, adminToken, { name: `IT Owner ${label} ${stamp}` });\n cleanups.push(() => deleteRoleIfExists(request, adminToken, roleId));\n await setRoleAclFeatures(request, adminToken, { roleId, features, organizations: null });\n const userId = await createUserFixture(request, adminToken, {\n email,\n password: PASSWORD,\n organizationId,\n roles: [roleId],\n });\n cleanups.push(async () => {\n await deleteUserAclInDb(userId).catch(() => undefined);\n await deleteUserIfExists(request, adminToken, userId);\n });\n const token = await getAuthToken(request, email, PASSWORD);\n return { userId, roleId, token };\n}\n\ntest.describe('TC-AI-ACTIONS-OWNER-008: pending action owner scope and tool feature recheck', () => {\n test('another user in the same organization cannot read, cancel or confirm a pending action', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);\n const cleanups: Cleanup[] = [];\n try {\n const productId = await createProductFixture(request, adminToken, {\n title: `IT owner product ${randomUUID().slice(0, 8)}`,\n sku: `IT-OWN-${randomUUID().slice(0, 8)}`,\n });\n cleanups.push(() => deleteCatalogProductIfExists(request, adminToken, productId));\n const originalTitle = await readCatalogProductTitleInDb(productId);\n\n const row = await seedPendingActionInDb({\n tenantId,\n organizationId: organizationId || null,\n createdByUserId: adminId,\n agentId: AGENT_ID,\n toolName: TOOL_NAME,\n normalizedInput: { productId, title: 'Changed by another user' },\n });\n cleanups.push(() => deletePendingActionInDb(row.id));\n\n const other = await createScopedUser(request, adminToken, organizationId, 'other', [\n 'ai_assistant.view',\n 'catalog.products.view',\n 'catalog.products.manage',\n ], cleanups);\n\n const read = await apiRequest(request, 'GET', `${ACTIONS}/${row.id}`, { token: other.token });\n expect(read.status(), 'non-owner GET is indistinguishable from an unknown id').toBe(404);\n expect((await readJsonSafe<{ code?: string }>(read))?.code).toBe('pending_action_not_found');\n\n const cancel = await apiRequest(request, 'POST', `${ACTIONS}/${row.id}/cancel`, {\n token: other.token,\n data: { reason: 'not mine' },\n });\n expect(cancel.status(), 'non-owner cancel is rejected').toBe(404);\n expect((await readJsonSafe<{ code?: string }>(cancel))?.code).toBe('pending_action_not_found');\n\n const confirm = await apiRequest(request, 'POST', `${ACTIONS}/${row.id}/confirm`, {\n token: other.token,\n data: {},\n });\n expect(confirm.status(), 'non-owner confirm is rejected').toBe(404);\n expect((await readJsonSafe<{ code?: string }>(confirm))?.code).toBe('pending_action_not_found');\n\n expect(await readPendingActionStateInDb(row.id)).toEqual({ status: 'pending', resolvedByUserId: null });\n expect(await readCatalogProductTitleInDb(productId)).toBe(originalTitle);\n\n const ownerRead = await apiRequest(request, 'GET', `${ACTIONS}/${row.id}`, { token: adminToken });\n expect(ownerRead.status(), 'the creator still reads their own action').toBe(200);\n const ownerCancel = await apiRequest(request, 'POST', `${ACTIONS}/${row.id}/cancel`, {\n token: adminToken,\n data: {},\n });\n expect(ownerCancel.status(), 'the creator still cancels their own action').toBe(200);\n expect(await readPendingActionStateInDb(row.id)).toEqual({ status: 'cancelled', resolvedByUserId: adminId });\n } finally {\n for (const cleanup of cleanups.reverse()) {\n await cleanup().catch(() => undefined);\n }\n }\n });\n\n test('confirm re-checks the tool features after they are revoked from the proposer', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId, organizationId } = getTokenScope(adminToken);\n const cleanups: Cleanup[] = [];\n try {\n const productId = await createProductFixture(request, adminToken, {\n title: `IT feature product ${randomUUID().slice(0, 8)}`,\n sku: `IT-FEAT-${randomUUID().slice(0, 8)}`,\n });\n cleanups.push(() => deleteCatalogProductIfExists(request, adminToken, productId));\n const originalTitle = await readCatalogProductTitleInDb(productId);\n\n const viewer = await createScopedUser(request, adminToken, organizationId, 'revoked', [\n 'ai_assistant.view',\n 'catalog.products.view',\n 'catalog.products.manage',\n ], cleanups);\n const viewerRow = await seedPendingActionInDb({\n tenantId,\n organizationId: organizationId || null,\n createdByUserId: viewer.userId,\n agentId: AGENT_ID,\n toolName: TOOL_NAME,\n normalizedInput: { productId, title: 'Changed without manage' },\n });\n cleanups.push(() => deletePendingActionInDb(viewerRow.id));\n\n await setRoleAclFeatures(request, adminToken, {\n roleId: viewer.roleId,\n features: ['ai_assistant.view', 'catalog.products.view'],\n organizations: null,\n });\n\n const denied = await apiRequest(request, 'POST', `${ACTIONS}/${viewerRow.id}/confirm`, {\n token: viewer.token,\n data: {},\n });\n expect(denied.status(), 'owner whose tool feature was revoked cannot confirm').toBe(403);\n expect((await readJsonSafe<{ code?: string }>(denied))?.code).toBe('tool_features_denied');\n\n const deniedPolish = await apiRequest(request, 'POST', `${ACTIONS}/${viewerRow.id}/confirm`, {\n token: viewer.token,\n data: {},\n headers: { 'Accept-Language': 'pl' },\n });\n expect(deniedPolish.status()).toBe(403);\n expect(await readJsonSafe<{ code?: string; error?: string }>(deniedPolish), 'the refusal is localized').toEqual({\n code: 'tool_features_denied',\n error: 'Brakuje Ci jednego z uprawnie\u0144 wymaganych przez narz\u0119dzie: catalog.products.manage',\n });\n expect(await readCatalogProductTitleInDb(productId)).toBe(originalTitle);\n expect((await readPendingActionStateInDb(viewerRow.id))?.status).toBe('pending');\n\n const viewerCancel = await apiRequest(request, 'POST', `${ACTIONS}/${viewerRow.id}/cancel`, {\n token: viewer.token,\n data: {},\n });\n expect(viewerCancel.status(), 'owner without the tool feature can still cancel').toBe(200);\n\n const manager = await createScopedUser(request, adminToken, organizationId, 'manager', [\n 'ai_assistant.view',\n 'catalog.products.view',\n 'catalog.products.manage',\n ], cleanups);\n const managerTitle = `Changed with manage ${randomUUID().slice(0, 8)}`;\n const managerRow = await seedPendingActionInDb({\n tenantId,\n organizationId: organizationId || null,\n createdByUserId: manager.userId,\n agentId: AGENT_ID,\n toolName: TOOL_NAME,\n normalizedInput: { productId, title: managerTitle },\n });\n cleanups.push(() => deletePendingActionInDb(managerRow.id));\n\n const confirmed = await apiRequest(request, 'POST', `${ACTIONS}/${managerRow.id}/confirm`, {\n token: manager.token,\n data: {},\n });\n expect(confirmed.status(), 'owner holding the tool feature confirms').toBe(200);\n const confirmedBody = await readJsonSafe<{ ok?: boolean; pendingAction?: { status?: string } }>(confirmed);\n expect(confirmedBody?.ok).toBe(true);\n expect(confirmedBody?.pendingAction?.status).toBe('confirmed');\n expect(await readCatalogProductTitleInDb(productId)).toBe(managerTitle);\n } finally {\n for (const cleanup of cleanups.reverse()) {\n await cleanup().catch(() => undefined);\n }\n }\n });\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,149 @@
1
+ import { test, expect } from "@playwright/test";
2
+ import { randomUUID } from "node:crypto";
3
+ import { getAuthToken } from "@open-mercato/core/helpers/integration/api";
4
+ import {
5
+ createRoleFixture,
6
+ deleteRoleIfExists,
7
+ createUserFixture,
8
+ deleteUserIfExists,
9
+ setRoleAclFeatures
10
+ } from "@open-mercato/core/helpers/integration/authFixtures";
11
+ import { deleteUserAclInDb } from "@open-mercato/core/helpers/integration/dbFixtures";
12
+ import { createProductFixture, deleteCatalogProductIfExists } from "@open-mercato/core/helpers/integration/catalogFixtures";
13
+ import { getTokenScope } from "@open-mercato/core/helpers/integration/generalFixtures";
14
+ import {
15
+ seedPendingActionInDb,
16
+ deletePendingActionInDb,
17
+ readPendingActionStateInDb,
18
+ readCatalogProductTitleInDb
19
+ } from "./helpers/aiAssistantFixtures.js";
20
+ const BASE_URL = process.env.BASE_URL?.trim() || "http://localhost:3000";
21
+ const PLAYGROUND = "/backend/config/ai-assistant/playground";
22
+ const AGENT_ID = "catalog.merchandising_assistant";
23
+ const TOOL_NAME = "catalog.update_product";
24
+ const PASSWORD = "Secret123!";
25
+ async function createLimitedUser(request, adminToken, organizationId, cleanups) {
26
+ const stamp = randomUUID().slice(0, 8);
27
+ const email = `it-owner-ui-${stamp}@example.com`;
28
+ const roleId = await createRoleFixture(request, adminToken, { name: `IT Owner UI ${stamp}` });
29
+ cleanups.push(() => deleteRoleIfExists(request, adminToken, roleId));
30
+ await setRoleAclFeatures(request, adminToken, {
31
+ roleId,
32
+ features: ["ai_assistant.view", "ai_assistant.settings.manage", "catalog.products.view"],
33
+ organizations: null
34
+ });
35
+ const userId = await createUserFixture(request, adminToken, {
36
+ email,
37
+ password: PASSWORD,
38
+ organizationId,
39
+ roles: [roleId]
40
+ });
41
+ cleanups.push(async () => {
42
+ await deleteUserAclInDb(userId).catch(() => void 0);
43
+ await deleteUserIfExists(request, adminToken, userId);
44
+ });
45
+ return { userId, token: await getAuthToken(request, email, PASSWORD) };
46
+ }
47
+ async function loginWithToken(page, token) {
48
+ const scope = getTokenScope(token);
49
+ await page.context().addCookies([
50
+ { name: "auth_token", value: token, url: BASE_URL, sameSite: "Lax" },
51
+ { name: "om_selected_tenant", value: scope.tenantId, url: BASE_URL, sameSite: "Lax" },
52
+ { name: "om_selected_org", value: scope.organizationId, url: BASE_URL, sameSite: "Lax" },
53
+ { name: "om_demo_notice_ack", value: "ack", url: BASE_URL },
54
+ { name: "om_cookie_notice_ack", value: "ack", url: BASE_URL },
55
+ { name: "om_feedback_suppress", value: "1", url: BASE_URL }
56
+ ]);
57
+ await page.route("**/api/ai_assistant/ai/agents", async (route) => {
58
+ await route.fulfill({
59
+ status: 200,
60
+ contentType: "application/json",
61
+ body: JSON.stringify({
62
+ agents: [
63
+ {
64
+ id: AGENT_ID,
65
+ moduleId: "catalog",
66
+ label: "Merchandising assistant",
67
+ description: "Proposes catalog product changes.",
68
+ executionMode: "chat",
69
+ mutationPolicy: "confirm-required",
70
+ allowedTools: [TOOL_NAME],
71
+ requiredFeatures: ["catalog.products.view"],
72
+ acceptedMediaTypes: [],
73
+ hasOutputSchema: false
74
+ }
75
+ ],
76
+ total: 1
77
+ })
78
+ });
79
+ });
80
+ }
81
+ async function openCard(page, uiPart, pendingActionId) {
82
+ await page.goto(`${PLAYGROUND}?uiPart=${uiPart}&pendingActionId=${pendingActionId}`, {
83
+ waitUntil: "domcontentloaded"
84
+ });
85
+ await expect(page.locator("[data-ai-playground]")).toBeVisible({ timeout: 6e4 });
86
+ }
87
+ test.describe("TC-AI-ACTIONS-OWNER-009: approval cards honour owner scope and the tool feature re-check", () => {
88
+ test("a non-owner sees an unavailable card and a refused confirm shows the permission alert", async ({ page, request }) => {
89
+ test.slow();
90
+ const adminToken = await getAuthToken(request, "admin");
91
+ const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);
92
+ const cleanups = [];
93
+ try {
94
+ const productId = await createProductFixture(request, adminToken, {
95
+ title: `IT owner UI product ${randomUUID().slice(0, 8)}`,
96
+ sku: `IT-OWN-UI-${randomUUID().slice(0, 8)}`
97
+ });
98
+ cleanups.push(() => deleteCatalogProductIfExists(request, adminToken, productId));
99
+ const originalTitle = await readCatalogProductTitleInDb(productId);
100
+ const limited = await createLimitedUser(request, adminToken, organizationId, cleanups);
101
+ const adminRow = await seedPendingActionInDb({
102
+ tenantId,
103
+ organizationId: organizationId || null,
104
+ createdByUserId: adminId,
105
+ agentId: AGENT_ID,
106
+ toolName: TOOL_NAME,
107
+ normalizedInput: { productId, title: "Proposed by admin" }
108
+ });
109
+ cleanups.push(() => deletePendingActionInDb(adminRow.id));
110
+ const limitedRow = await seedPendingActionInDb({
111
+ tenantId,
112
+ organizationId: organizationId || null,
113
+ createdByUserId: limited.userId,
114
+ agentId: AGENT_ID,
115
+ toolName: TOOL_NAME,
116
+ normalizedInput: { productId, title: "Proposed without manage" }
117
+ });
118
+ cleanups.push(() => deletePendingActionInDb(limitedRow.id));
119
+ await loginWithToken(page, limited.token);
120
+ await openCard(page, "mutation-preview-card", adminRow.id);
121
+ await expect(page.locator("[data-ai-mutation-preview-unavailable]")).toBeVisible({ timeout: 15e3 });
122
+ await expect(page.locator("[data-ai-mutation-preview-confirm]")).toHaveCount(0);
123
+ await expect(page.locator("[data-ai-mutation-preview-cancel]")).toHaveCount(0);
124
+ await expect(page.locator("[data-ai-mutation-preview-body]")).toHaveCount(0);
125
+ await openCard(page, "confirmation-card", adminRow.id);
126
+ const confirmationCard = page.locator("[data-ai-confirmation-card]").first();
127
+ await expect(confirmationCard).toHaveAttribute("data-ai-confirmation-status", "unavailable", { timeout: 15e3 });
128
+ await expect(page.locator("[data-ai-confirmation-cancel]")).toHaveCount(0);
129
+ await openCard(page, "mutation-preview-card", limitedRow.id);
130
+ const confirmButton = page.locator("[data-ai-mutation-preview-confirm]");
131
+ await expect(confirmButton).toBeVisible({ timeout: 15e3 });
132
+ const confirmResponse = page.waitForResponse(
133
+ (response) => response.url().includes(`/api/ai_assistant/ai/actions/${limitedRow.id}/confirm`) && response.request().method() === "POST"
134
+ );
135
+ await confirmButton.click();
136
+ expect((await confirmResponse).status()).toBe(403);
137
+ await expect(page.locator('[data-ai-confirmation-error="tool_features_denied"]')).toBeVisible({ timeout: 15e3 });
138
+ await expect(page.locator("[data-ai-mutation-preview-cancel]")).toBeVisible();
139
+ expect((await readPendingActionStateInDb(limitedRow.id))?.status).toBe("pending");
140
+ expect((await readPendingActionStateInDb(adminRow.id))?.status).toBe("pending");
141
+ expect(await readCatalogProductTitleInDb(productId)).toBe(originalTitle);
142
+ } finally {
143
+ for (const cleanup of cleanups.reverse()) {
144
+ await cleanup().catch(() => void 0);
145
+ }
146
+ }
147
+ });
148
+ });
149
+ //# sourceMappingURL=TC-AI-ACTIONS-OWNER-009-approval-cards-ui.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-ACTIONS-OWNER-009-approval-cards-ui.spec.ts"],
4
+ "sourcesContent": ["import { test, expect, type APIRequestContext, type Page } from '@playwright/test';\nimport { randomUUID } from 'node:crypto';\nimport { getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport {\n createRoleFixture,\n deleteRoleIfExists,\n createUserFixture,\n deleteUserIfExists,\n setRoleAclFeatures,\n} from '@open-mercato/core/helpers/integration/authFixtures';\nimport { deleteUserAclInDb } from '@open-mercato/core/helpers/integration/dbFixtures';\nimport { createProductFixture, deleteCatalogProductIfExists } from '@open-mercato/core/helpers/integration/catalogFixtures';\nimport { getTokenScope } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport {\n seedPendingActionInDb,\n deletePendingActionInDb,\n readPendingActionStateInDb,\n readCatalogProductTitleInDb,\n} from './helpers/aiAssistantFixtures';\n\n/**\n * TC-AI-ACTIONS-OWNER-009 \u2014 the approval cards in the browser honour the\n * owner scope and the tool-feature re-check from TC-AI-ACTIONS-OWNER-008.\n * Source: GitHub issue #3806 (UI QA follow-up on PR #7329).\n *\n * Pending actions have no public create route and no AI provider is needed:\n * rows are seeded via SQL and mounted through the playground's\n * `?uiPart=<card>&pendingActionId=<id>` debug seed. Everything after mount\n * (GET, confirm) goes through the real routes. The agents list is stubbed so\n * the playground deterministically opens a chat lane.\n */\n\nconst BASE_URL = process.env.BASE_URL?.trim() || 'http://localhost:3000';\nconst PLAYGROUND = '/backend/config/ai-assistant/playground';\nconst AGENT_ID = 'catalog.merchandising_assistant';\nconst TOOL_NAME = 'catalog.update_product';\nconst PASSWORD = 'Secret123!';\n\ntype Cleanup = () => Promise<void>;\n\nasync function createLimitedUser(\n request: APIRequestContext,\n adminToken: string,\n organizationId: string,\n cleanups: Cleanup[],\n): Promise<{ userId: string; token: string }> {\n const stamp = randomUUID().slice(0, 8);\n const email = `it-owner-ui-${stamp}@example.com`;\n const roleId = await createRoleFixture(request, adminToken, { name: `IT Owner UI ${stamp}` });\n cleanups.push(() => deleteRoleIfExists(request, adminToken, roleId));\n await setRoleAclFeatures(request, adminToken, {\n roleId,\n features: ['ai_assistant.view', 'ai_assistant.settings.manage', 'catalog.products.view'],\n organizations: null,\n });\n const userId = await createUserFixture(request, adminToken, {\n email,\n password: PASSWORD,\n organizationId,\n roles: [roleId],\n });\n cleanups.push(async () => {\n await deleteUserAclInDb(userId).catch(() => undefined);\n await deleteUserIfExists(request, adminToken, userId);\n });\n return { userId, token: await getAuthToken(request, email, PASSWORD) };\n}\n\nasync function loginWithToken(page: Page, token: string): Promise<void> {\n const scope = getTokenScope(token);\n await page.context().addCookies([\n { name: 'auth_token', value: token, url: BASE_URL, sameSite: 'Lax' },\n { name: 'om_selected_tenant', value: scope.tenantId, url: BASE_URL, sameSite: 'Lax' },\n { name: 'om_selected_org', value: scope.organizationId, url: BASE_URL, sameSite: 'Lax' },\n { name: 'om_demo_notice_ack', value: 'ack', url: BASE_URL },\n { name: 'om_cookie_notice_ack', value: 'ack', url: BASE_URL },\n { name: 'om_feedback_suppress', value: '1', url: BASE_URL },\n ]);\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agents: [\n {\n id: AGENT_ID,\n moduleId: 'catalog',\n label: 'Merchandising assistant',\n description: 'Proposes catalog product changes.',\n executionMode: 'chat',\n mutationPolicy: 'confirm-required',\n allowedTools: [TOOL_NAME],\n requiredFeatures: ['catalog.products.view'],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n ],\n total: 1,\n }),\n });\n });\n}\n\nasync function openCard(page: Page, uiPart: string, pendingActionId: string): Promise<void> {\n await page.goto(`${PLAYGROUND}?uiPart=${uiPart}&pendingActionId=${pendingActionId}`, {\n waitUntil: 'domcontentloaded',\n });\n await expect(page.locator('[data-ai-playground]')).toBeVisible({ timeout: 60_000 });\n}\n\ntest.describe('TC-AI-ACTIONS-OWNER-009: approval cards honour owner scope and the tool feature re-check', () => {\n test('a non-owner sees an unavailable card and a refused confirm shows the permission alert', async ({ page, request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);\n const cleanups: Cleanup[] = [];\n try {\n const productId = await createProductFixture(request, adminToken, {\n title: `IT owner UI product ${randomUUID().slice(0, 8)}`,\n sku: `IT-OWN-UI-${randomUUID().slice(0, 8)}`,\n });\n cleanups.push(() => deleteCatalogProductIfExists(request, adminToken, productId));\n const originalTitle = await readCatalogProductTitleInDb(productId);\n\n const limited = await createLimitedUser(request, adminToken, organizationId, cleanups);\n\n const adminRow = await seedPendingActionInDb({\n tenantId,\n organizationId: organizationId || null,\n createdByUserId: adminId,\n agentId: AGENT_ID,\n toolName: TOOL_NAME,\n normalizedInput: { productId, title: 'Proposed by admin' },\n });\n cleanups.push(() => deletePendingActionInDb(adminRow.id));\n const limitedRow = await seedPendingActionInDb({\n tenantId,\n organizationId: organizationId || null,\n createdByUserId: limited.userId,\n agentId: AGENT_ID,\n toolName: TOOL_NAME,\n normalizedInput: { productId, title: 'Proposed without manage' },\n });\n cleanups.push(() => deletePendingActionInDb(limitedRow.id));\n\n await loginWithToken(page, limited.token);\n\n await openCard(page, 'mutation-preview-card', adminRow.id);\n await expect(page.locator('[data-ai-mutation-preview-unavailable]')).toBeVisible({ timeout: 15_000 });\n await expect(page.locator('[data-ai-mutation-preview-confirm]')).toHaveCount(0);\n await expect(page.locator('[data-ai-mutation-preview-cancel]')).toHaveCount(0);\n await expect(page.locator('[data-ai-mutation-preview-body]')).toHaveCount(0);\n\n await openCard(page, 'confirmation-card', adminRow.id);\n const confirmationCard = page.locator('[data-ai-confirmation-card]').first();\n await expect(confirmationCard).toHaveAttribute('data-ai-confirmation-status', 'unavailable', { timeout: 15_000 });\n await expect(page.locator('[data-ai-confirmation-cancel]')).toHaveCount(0);\n\n await openCard(page, 'mutation-preview-card', limitedRow.id);\n const confirmButton = page.locator('[data-ai-mutation-preview-confirm]');\n await expect(confirmButton).toBeVisible({ timeout: 15_000 });\n const confirmResponse = page.waitForResponse(\n (response) =>\n response.url().includes(`/api/ai_assistant/ai/actions/${limitedRow.id}/confirm`) &&\n response.request().method() === 'POST',\n );\n await confirmButton.click();\n expect((await confirmResponse).status()).toBe(403);\n await expect(page.locator('[data-ai-confirmation-error=\"tool_features_denied\"]')).toBeVisible({ timeout: 15_000 });\n await expect(page.locator('[data-ai-mutation-preview-cancel]')).toBeVisible();\n\n expect((await readPendingActionStateInDb(limitedRow.id))?.status).toBe('pending');\n expect((await readPendingActionStateInDb(adminRow.id))?.status).toBe('pending');\n expect(await readCatalogProductTitleInDb(productId)).toBe(originalTitle);\n } finally {\n for (const cleanup of cleanups.reverse()) {\n await cleanup().catch(() => undefined);\n }\n }\n });\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -71,6 +71,22 @@ async function seedPendingActionInDb(input) {
71
71
  return { id: result.rows[0].id, idempotencyKey };
72
72
  });
73
73
  }
74
+ async function readPendingActionStateInDb(id) {
75
+ return withClient(async (client) => {
76
+ const result = await client.query(
77
+ "select status, resolved_by_user_id from ai_pending_actions where id = $1",
78
+ [id]
79
+ );
80
+ const row = result.rows[0];
81
+ return row ? { status: row.status, resolvedByUserId: row.resolved_by_user_id } : null;
82
+ });
83
+ }
84
+ async function readCatalogProductTitleInDb(productId) {
85
+ return withClient(async (client) => {
86
+ const result = await client.query("select title from catalog_products where id = $1", [productId]);
87
+ return result.rows[0]?.title ?? null;
88
+ });
89
+ }
74
90
  async function deletePendingActionInDb(id) {
75
91
  if (!id) return;
76
92
  await withClient(async (client) => {
@@ -151,6 +167,8 @@ export {
151
167
  deleteModerationFlagsInDb,
152
168
  deletePendingActionInDb,
153
169
  deleteTenantAllowlistInDb,
170
+ readCatalogProductTitleInDb,
171
+ readPendingActionStateInDb,
154
172
  seedModerationFlagInDb,
155
173
  seedPendingActionInDb
156
174
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts"],
4
- "sourcesContent": ["import { readFileSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\nimport path from 'node:path';\nimport { Client } from 'pg';\n\n/**\n * Direct-Postgres fixtures for AI Assistant integration specs.\n *\n * Mirrors the sanctioned pattern in\n * `@open-mercato/core/helpers/integration/dbFixtures` (raw SQL against\n * `DATABASE_URL`) for the surfaces that have NO public create route:\n * - `ai_pending_actions`: a pending mutation approval is only ever born from\n * the internal `prepareMutation` path during a real LLM agent turn. Seeding\n * the row directly keeps confirm/cancel/GET coverage deterministic and\n * provider-free.\n * - prompt overrides: the `prompt-override` route exposes no DELETE, so\n * versioned rows must be swept via SQL after a test.\n *\n * The app server and this helper MUST share one `DATABASE_URL`, so specs using\n * these helpers only run under the coherent app+DB harness\n * (`yarn test:integration` / `:ephemeral`), never against an arbitrary dev\n * server whose `DATABASE_URL` differs from `apps/mercato/.env`.\n */\n\nfunction resolveAppRoot(): string {\n const fromEnv = process.env.OM_TEST_APP_ROOT?.trim();\n return fromEnv ? path.resolve(fromEnv) : path.resolve(process.cwd(), 'apps/mercato');\n}\n\nfunction readEnvValue(key: string): string | undefined {\n if (process.env[key]) return process.env[key];\n const candidatePaths = [\n path.resolve(resolveAppRoot(), '.env'),\n path.resolve(process.cwd(), 'apps/mercato/.env'),\n path.resolve(process.cwd(), '.env'),\n ];\n for (const envPath of candidatePaths) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));\n if (match?.[1]) return match[1].trim();\n } catch {\n continue;\n }\n }\n return undefined;\n}\n\nfunction resolveDatabaseUrl(): string {\n const url = readEnvValue('DATABASE_URL');\n if (!url) throw new Error('[internal] DATABASE_URL is not configured for AI Assistant integration DB fixtures');\n return url;\n}\n\nasync function withClient<T>(run: (client: Client) => Promise<T>): Promise<T> {\n const client = new Client({ connectionString: resolveDatabaseUrl() });\n await client.connect();\n try {\n return await run(client);\n } finally {\n await client.end();\n }\n}\n\nexport type SeededPendingActionStatus =\n | 'pending'\n | 'confirmed'\n | 'cancelled'\n | 'expired'\n | 'executing'\n | 'failed';\n\nexport interface SeedPendingActionInput {\n tenantId: string;\n organizationId?: string | null;\n createdByUserId: string;\n status?: SeededPendingActionStatus;\n agentId?: string;\n toolName?: string;\n /** Minutes from now until expiry. Negative => the row is already expired. */\n expiresInMinutes?: number;\n normalizedInput?: Record<string, unknown>;\n executionResult?: Record<string, unknown> | null;\n idempotencyKey?: string;\n}\n\nexport interface SeededPendingAction {\n id: string;\n idempotencyKey: string;\n}\n\n/**\n * Inserts an `ai_pending_actions` row directly. Returns the new id so the test\n * can act on it and clean it up. Defaults produce an actionable `pending` row\n * with a future TTL; override `status`/`expiresInMinutes`/`executionResult` to\n * cover the idempotency short-circuit and 409 error branches.\n */\nexport async function seedPendingActionInDb(input: SeedPendingActionInput): Promise<SeededPendingAction> {\n const idempotencyKey = input.idempotencyKey ?? `it-pending-${randomUUID()}`;\n const status = input.status ?? 'pending';\n const expiresInMinutes = input.expiresInMinutes ?? 60;\n const normalizedInput = JSON.stringify(input.normalizedInput ?? {});\n const executionResult =\n input.executionResult === undefined || input.executionResult === null\n ? null\n : JSON.stringify(input.executionResult);\n return withClient(async (client) => {\n const result = await client.query<{ id: string }>(\n `insert into ai_pending_actions\n (id, tenant_id, organization_id, agent_id, tool_name, normalized_input,\n field_diff, attachment_ids, idempotency_key, created_by_user_id, status,\n queue_mode, execution_result, created_at, expires_at)\n values\n (gen_random_uuid(), $1, $2, $3, $4, $5::jsonb,\n '[]'::jsonb, '[]'::jsonb, $6, $7, $8,\n 'inline', $9::jsonb, now(), now() + make_interval(mins => $10::int))\n returning id`,\n [\n input.tenantId,\n input.organizationId ?? null,\n input.agentId ?? 'it_agent.pending_fixture',\n input.toolName ?? 'it_tool.noop',\n normalizedInput,\n idempotencyKey,\n input.createdByUserId,\n status,\n executionResult,\n expiresInMinutes,\n ],\n );\n return { id: result.rows[0].id, idempotencyKey };\n });\n}\n\n/** Hard-deletes a seeded pending action row (best-effort test cleanup). */\nexport async function deletePendingActionInDb(id: string | null): Promise<void> {\n if (!id) return;\n await withClient(async (client) => {\n await client.query('delete from ai_pending_actions where id = $1', [id]);\n });\n}\n\n/**\n * Sweeps every per-agent override row for a tenant across the three override\n * tables. Required for `prompt-override` cleanup (no DELETE route) and used as a\n * belt-and-braces teardown for mutation-policy / loop overrides.\n */\nexport async function deleteAgentOverridesInDb(input: { tenantId: string; agentId: string }): Promise<void> {\n if (!input.tenantId || !input.agentId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_agent_prompt_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n await client.query('delete from ai_agent_mutation_policy_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n await client.query('delete from ai_agent_runtime_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n });\n}\n\n/** Hard-deletes any tenant model-allowlist rows (best-effort cleanup). */\nexport async function deleteTenantAllowlistInDb(tenantId: string | null): Promise<void> {\n if (!tenantId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_tenant_model_allowlists where tenant_id = $1', [tenantId]);\n });\n}\n\nexport interface SeedModerationFlagInput {\n tenantId: string;\n organizationId?: string | null;\n agentId?: string;\n userId?: string;\n providerId?: string;\n modelId?: string;\n /** Defaults to a single flagged `hate` category. */\n categories?: Record<string, { flagged: boolean; score: number }>;\n}\n\n/**\n * Inserts an `ai_moderation_flags` audit row directly. A real flag is only ever\n * born from the gate during a flagged LLM turn (needs a live moderation call),\n * so seeding the row keeps audit-listing + tenant-isolation coverage\n * deterministic and provider-free. Returns the new id for assertions/cleanup.\n */\nexport async function seedModerationFlagInDb(input: SeedModerationFlagInput): Promise<string> {\n const categories = JSON.stringify(input.categories ?? { hate: { flagged: true, score: 0.97 } });\n return withClient(async (client) => {\n const result = await client.query<{ id: string }>(\n `insert into ai_moderation_flags\n (id, tenant_id, organization_id, agent_id, user_id, provider_id, model_id, categories, created_at)\n values\n (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7::jsonb, now())\n returning id`,\n [\n input.tenantId,\n input.organizationId ?? null,\n input.agentId ?? 'it_agent.moderation_fixture',\n input.userId ?? `it-user-${randomUUID()}`,\n input.providerId ?? 'openai',\n input.modelId ?? 'gpt-5-mini',\n categories,\n ],\n );\n return result.rows[0].id;\n });\n}\n\n/** Hard-deletes every moderation-flag row for a tenant (best-effort cleanup). */\nexport async function deleteModerationFlagsInDb(tenantId: string | null): Promise<void> {\n if (!tenantId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_moderation_flags where tenant_id = $1', [tenantId]);\n });\n}\n\n/**\n * Hard-deletes a conversation and its participant + message rows by the\n * client-facing `conversation_id`. The DELETE route only soft-deletes, so this\n * gives specs a true teardown for the rows they created.\n */\nexport async function deleteConversationCascadeInDb(input: {\n tenantId: string;\n conversationId: string;\n}): Promise<void> {\n if (!input.tenantId || !input.conversationId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_chat_messages where tenant_id = $1 and conversation_id = $2', [\n input.tenantId,\n input.conversationId,\n ]);\n await client.query(\n 'delete from ai_chat_conversation_participants where tenant_id = $1 and conversation_id = $2',\n [input.tenantId, input.conversationId],\n );\n await client.query('delete from ai_chat_conversations where tenant_id = $1 and conversation_id = $2', [\n input.tenantId,\n input.conversationId,\n ]);\n });\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { readFileSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\nimport path from 'node:path';\nimport { Client } from 'pg';\n\n/**\n * Direct-Postgres fixtures for AI Assistant integration specs.\n *\n * Mirrors the sanctioned pattern in\n * `@open-mercato/core/helpers/integration/dbFixtures` (raw SQL against\n * `DATABASE_URL`) for the surfaces that have NO public create route:\n * - `ai_pending_actions`: a pending mutation approval is only ever born from\n * the internal `prepareMutation` path during a real LLM agent turn. Seeding\n * the row directly keeps confirm/cancel/GET coverage deterministic and\n * provider-free.\n * - prompt overrides: the `prompt-override` route exposes no DELETE, so\n * versioned rows must be swept via SQL after a test.\n *\n * The app server and this helper MUST share one `DATABASE_URL`, so specs using\n * these helpers only run under the coherent app+DB harness\n * (`yarn test:integration` / `:ephemeral`), never against an arbitrary dev\n * server whose `DATABASE_URL` differs from `apps/mercato/.env`.\n */\n\nfunction resolveAppRoot(): string {\n const fromEnv = process.env.OM_TEST_APP_ROOT?.trim();\n return fromEnv ? path.resolve(fromEnv) : path.resolve(process.cwd(), 'apps/mercato');\n}\n\nfunction readEnvValue(key: string): string | undefined {\n if (process.env[key]) return process.env[key];\n const candidatePaths = [\n path.resolve(resolveAppRoot(), '.env'),\n path.resolve(process.cwd(), 'apps/mercato/.env'),\n path.resolve(process.cwd(), '.env'),\n ];\n for (const envPath of candidatePaths) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));\n if (match?.[1]) return match[1].trim();\n } catch {\n continue;\n }\n }\n return undefined;\n}\n\nfunction resolveDatabaseUrl(): string {\n const url = readEnvValue('DATABASE_URL');\n if (!url) throw new Error('[internal] DATABASE_URL is not configured for AI Assistant integration DB fixtures');\n return url;\n}\n\nasync function withClient<T>(run: (client: Client) => Promise<T>): Promise<T> {\n const client = new Client({ connectionString: resolveDatabaseUrl() });\n await client.connect();\n try {\n return await run(client);\n } finally {\n await client.end();\n }\n}\n\nexport type SeededPendingActionStatus =\n | 'pending'\n | 'confirmed'\n | 'cancelled'\n | 'expired'\n | 'executing'\n | 'failed';\n\nexport interface SeedPendingActionInput {\n tenantId: string;\n organizationId?: string | null;\n createdByUserId: string;\n status?: SeededPendingActionStatus;\n agentId?: string;\n toolName?: string;\n /** Minutes from now until expiry. Negative => the row is already expired. */\n expiresInMinutes?: number;\n normalizedInput?: Record<string, unknown>;\n executionResult?: Record<string, unknown> | null;\n idempotencyKey?: string;\n}\n\nexport interface SeededPendingAction {\n id: string;\n idempotencyKey: string;\n}\n\n/**\n * Inserts an `ai_pending_actions` row directly. Returns the new id so the test\n * can act on it and clean it up. Defaults produce an actionable `pending` row\n * with a future TTL; override `status`/`expiresInMinutes`/`executionResult` to\n * cover the idempotency short-circuit and 409 error branches.\n */\nexport async function seedPendingActionInDb(input: SeedPendingActionInput): Promise<SeededPendingAction> {\n const idempotencyKey = input.idempotencyKey ?? `it-pending-${randomUUID()}`;\n const status = input.status ?? 'pending';\n const expiresInMinutes = input.expiresInMinutes ?? 60;\n const normalizedInput = JSON.stringify(input.normalizedInput ?? {});\n const executionResult =\n input.executionResult === undefined || input.executionResult === null\n ? null\n : JSON.stringify(input.executionResult);\n return withClient(async (client) => {\n const result = await client.query<{ id: string }>(\n `insert into ai_pending_actions\n (id, tenant_id, organization_id, agent_id, tool_name, normalized_input,\n field_diff, attachment_ids, idempotency_key, created_by_user_id, status,\n queue_mode, execution_result, created_at, expires_at)\n values\n (gen_random_uuid(), $1, $2, $3, $4, $5::jsonb,\n '[]'::jsonb, '[]'::jsonb, $6, $7, $8,\n 'inline', $9::jsonb, now(), now() + make_interval(mins => $10::int))\n returning id`,\n [\n input.tenantId,\n input.organizationId ?? null,\n input.agentId ?? 'it_agent.pending_fixture',\n input.toolName ?? 'it_tool.noop',\n normalizedInput,\n idempotencyKey,\n input.createdByUserId,\n status,\n executionResult,\n expiresInMinutes,\n ],\n );\n return { id: result.rows[0].id, idempotencyKey };\n });\n}\n\nexport interface PendingActionDbState {\n status: string;\n resolvedByUserId: string | null;\n}\n\n/** Reads a pending action's lifecycle columns straight from Postgres (bypasses the owner-scoped routes). */\nexport async function readPendingActionStateInDb(id: string): Promise<PendingActionDbState | null> {\n return withClient(async (client) => {\n const result = await client.query<{ status: string; resolved_by_user_id: string | null }>(\n 'select status, resolved_by_user_id from ai_pending_actions where id = $1',\n [id],\n );\n const row = result.rows[0];\n return row ? { status: row.status, resolvedByUserId: row.resolved_by_user_id } : null;\n });\n}\n\n/** Reads a catalog product title straight from Postgres so assertions never hit a cached CRUD response. */\nexport async function readCatalogProductTitleInDb(productId: string): Promise<string | null> {\n return withClient(async (client) => {\n const result = await client.query<{ title: string }>('select title from catalog_products where id = $1', [productId]);\n return result.rows[0]?.title ?? null;\n });\n}\n\n/** Hard-deletes a seeded pending action row (best-effort test cleanup). */\nexport async function deletePendingActionInDb(id: string | null): Promise<void> {\n if (!id) return;\n await withClient(async (client) => {\n await client.query('delete from ai_pending_actions where id = $1', [id]);\n });\n}\n\n/**\n * Sweeps every per-agent override row for a tenant across the three override\n * tables. Required for `prompt-override` cleanup (no DELETE route) and used as a\n * belt-and-braces teardown for mutation-policy / loop overrides.\n */\nexport async function deleteAgentOverridesInDb(input: { tenantId: string; agentId: string }): Promise<void> {\n if (!input.tenantId || !input.agentId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_agent_prompt_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n await client.query('delete from ai_agent_mutation_policy_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n await client.query('delete from ai_agent_runtime_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n });\n}\n\n/** Hard-deletes any tenant model-allowlist rows (best-effort cleanup). */\nexport async function deleteTenantAllowlistInDb(tenantId: string | null): Promise<void> {\n if (!tenantId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_tenant_model_allowlists where tenant_id = $1', [tenantId]);\n });\n}\n\nexport interface SeedModerationFlagInput {\n tenantId: string;\n organizationId?: string | null;\n agentId?: string;\n userId?: string;\n providerId?: string;\n modelId?: string;\n /** Defaults to a single flagged `hate` category. */\n categories?: Record<string, { flagged: boolean; score: number }>;\n}\n\n/**\n * Inserts an `ai_moderation_flags` audit row directly. A real flag is only ever\n * born from the gate during a flagged LLM turn (needs a live moderation call),\n * so seeding the row keeps audit-listing + tenant-isolation coverage\n * deterministic and provider-free. Returns the new id for assertions/cleanup.\n */\nexport async function seedModerationFlagInDb(input: SeedModerationFlagInput): Promise<string> {\n const categories = JSON.stringify(input.categories ?? { hate: { flagged: true, score: 0.97 } });\n return withClient(async (client) => {\n const result = await client.query<{ id: string }>(\n `insert into ai_moderation_flags\n (id, tenant_id, organization_id, agent_id, user_id, provider_id, model_id, categories, created_at)\n values\n (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7::jsonb, now())\n returning id`,\n [\n input.tenantId,\n input.organizationId ?? null,\n input.agentId ?? 'it_agent.moderation_fixture',\n input.userId ?? `it-user-${randomUUID()}`,\n input.providerId ?? 'openai',\n input.modelId ?? 'gpt-5-mini',\n categories,\n ],\n );\n return result.rows[0].id;\n });\n}\n\n/** Hard-deletes every moderation-flag row for a tenant (best-effort cleanup). */\nexport async function deleteModerationFlagsInDb(tenantId: string | null): Promise<void> {\n if (!tenantId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_moderation_flags where tenant_id = $1', [tenantId]);\n });\n}\n\n/**\n * Hard-deletes a conversation and its participant + message rows by the\n * client-facing `conversation_id`. The DELETE route only soft-deletes, so this\n * gives specs a true teardown for the rows they created.\n */\nexport async function deleteConversationCascadeInDb(input: {\n tenantId: string;\n conversationId: string;\n}): Promise<void> {\n if (!input.tenantId || !input.conversationId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_chat_messages where tenant_id = $1 and conversation_id = $2', [\n input.tenantId,\n input.conversationId,\n ]);\n await client.query(\n 'delete from ai_chat_conversation_participants where tenant_id = $1 and conversation_id = $2',\n [input.tenantId, input.conversationId],\n );\n await client.query('delete from ai_chat_conversations where tenant_id = $1 and conversation_id = $2', [\n input.tenantId,\n input.conversationId,\n ]);\n });\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }