@open-mercato/ai-assistant 0.6.4-develop.4239.1.4a264a5828 → 0.6.4-develop.4264.1.53368d85fe
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.
|
@@ -268,7 +268,11 @@ test.describe("TC-AI-AGENT-LOOP-001\u2013006: agentic loop controls", () => {
|
|
|
268
268
|
body: JSON.stringify(agentsPayload)
|
|
269
269
|
});
|
|
270
270
|
});
|
|
271
|
+
const agentsResponsePromise = page.waitForResponse(
|
|
272
|
+
(response) => response.url().includes("/api/ai_assistant/ai/agents") && response.request().method() === "GET"
|
|
273
|
+
);
|
|
271
274
|
await page.goto(playgroundPath, { waitUntil: "domcontentloaded" });
|
|
275
|
+
await agentsResponsePromise;
|
|
272
276
|
expect(capturedAgentsPayload).not.toBeNull();
|
|
273
277
|
const toolLoopEntry = capturedAgentsPayload.agents.find(
|
|
274
278
|
(a) => a.id === "catalog.tool_loop_assistant"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.ts"],
|
|
4
|
-
"sourcesContent": ["import { test, expect } from '@playwright/test';\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';\n\n/**\n * TC-AI-AGENT-LOOP-001 through TC-AI-AGENT-LOOP-006\n *\n * Integration coverage for Phase 3 (operator budgets + kill switch) and\n * Phase 4 (LoopTrace, loopBudget dispatcher param, allowRuntimeOverride rename)\n * of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n *\n * Coverage table (per spec \u00A7Test scenarios):\n *\n * TC-AI-AGENT-LOOP-001 \u2014 Kill-switch banner: when loop_disabled is active for an agent,\n * `<AiChat>` renders the LoopDisabledBanner component.\n *\n * TC-AI-AGENT-LOOP-002 \u2014 loopBudget dispatcher param: `?loopBudget=tight` resolves to\n * the pinned tight preset, is blocked when `allowRuntimeOverride: false`, and the\n * 'default' value is a no-op.\n *\n * TC-AI-AGENT-LOOP-003 \u2014 hasToolCall stopWhen (API contract): chat API returns a\n * stream with `loopAbortReason: 'has-tool-call'` when stopWhen fires.\n *\n * TC-AI-AGENT-LOOP-004 \u2014 loop_violates_mutation_policy: a `prepareStep` that smuggles\n * a raw mutation handler triggers a 409 response with code `loop_violates_mutation_policy`.\n *\n * TC-AI-AGENT-LOOP-005 \u2014 LoopTrace panel (playground): the playground renders a\n * LoopTrace panel with step-level detail when the debug panel is open.\n *\n * TC-AI-AGENT-LOOP-006 \u2014 Mutation gating survives engine swap: a mock response for\n * an agent that declares `executionEngine: 'tool-loop-agent'` confirms that the\n * `/api/ai_assistant/ai/agents` payload still carries the agent entry and\n * tool-loop agents are listed by the registry.\n *\n * All API calls are intercepted via page.route() stubs \u2014 no LLM is required.\n */\n\ntest.describe('TC-AI-AGENT-LOOP-001\u2013006: agentic loop controls', () => {\n const settingsPath = '/backend/config/ai-assistant/settings';\n const playgroundPath = '/backend/config/ai-assistant/playground';\n\n const agentsPayload = {\n agents: [\n {\n id: 'customers.account_assistant',\n moduleId: 'customers',\n label: 'Account Assistant',\n description: 'Customer account AI assistant.',\n executionMode: 'chat',\n mutationPolicy: 'confirm-required',\n readOnly: false,\n maxSteps: 10,\n allowedTools: ['customers.update_deal_stage'],\n tools: [\n {\n name: 'customers.update_deal_stage',\n displayName: 'Update deal stage',\n isMutation: true,\n registered: true,\n },\n ],\n requiredFeatures: ['customers.view'],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n {\n id: 'catalog.tool_loop_assistant',\n moduleId: 'catalog',\n label: 'Tool Loop Assistant',\n description: 'Catalog assistant using tool-loop-agent engine.',\n executionMode: 'chat',\n mutationPolicy: 'confirm-required',\n readOnly: false,\n maxSteps: 5,\n allowedTools: ['catalog.list_products'],\n tools: [\n {\n name: 'catalog.list_products',\n displayName: 'List products',\n isMutation: false,\n registered: true,\n },\n ],\n requiredFeatures: ['catalog.view'],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n executionEngine: 'tool-loop-agent',\n },\n ],\n total: 2,\n };\n\n const settingsPayload = {\n provider: { id: 'anthropic', name: 'Anthropic', defaultModel: 'claude-haiku-4-5' },\n availableProviders: [\n {\n id: 'anthropic',\n name: 'Anthropic',\n isConfigured: true,\n defaultModels: [{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' }],\n },\n ],\n mcpKeyConfigured: true,\n resolvedDefault: {\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n baseURL: null,\n source: 'provider_default',\n },\n tenantOverride: null,\n agents: [\n {\n agentId: 'customers.account_assistant',\n moduleId: 'customers',\n allowRuntimeOverride: true,\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n baseURL: null,\n source: 'provider_default',\n },\n ],\n };\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-001 \u2014 Kill-switch banner\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-001: kill-switch banner in settings Loop panel', () => {\n test('settings page renders Loop policy section for the configured agent', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }),\n });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ tools: [] }),\n });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n const settingsContainer = page.locator('[data-ai-assistant-settings]');\n await expect(settingsContainer).toBeVisible({ timeout: 30_000 });\n });\n\n test('LoopDisabledBanner export is present in ui package', async ({ request }) => {\n // Smoke test: the `loop-override` API route is mounted and reachable.\n // (Does not require auth - 401 is an acceptable response.)\n const response = await request.get(\n '/api/ai_assistant/ai/agents/customers.account_assistant/loop-override',\n );\n expect([200, 401, 403, 404]).toContain(response.status());\n });\n });\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-002 \u2014 loopBudget dispatcher param\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-002: loopBudget query-param on POST /api/ai_assistant/ai/chat', () => {\n test('endpoint is mounted and returns 401 for unauthenticated requests', async ({ request }) => {\n const response = await request.post(\n '/api/ai_assistant/ai/chat?agent=customers.account_assistant&loopBudget=tight',\n {\n data: { messages: [{ role: 'user', content: 'test' }] },\n headers: { 'content-type': 'application/json' },\n },\n );\n expect([200, 401, 403, 404, 409]).toContain(response.status());\n });\n\n test('playground renders and loopBudget picker area is accessible', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\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(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/ai/agents/*/models', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agentId: 'customers.account_assistant',\n allowRuntimeOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [],\n }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n const chatArea = page.locator('[data-ai-playground-chat]').first();\n await expect(chatArea).toBeVisible({ timeout: 30_000 });\n });\n });\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-003 \u2014 hasToolCall stopWhen (API contract)\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-003: loop-override route for stopWhen declaration', () => {\n test('loop-override GET route is mounted (returns 200, 401, or 404)', async ({ request }) => {\n const response = await request.get(\n '/api/ai_assistant/ai/agents/customers.account_assistant/loop-override',\n );\n expect([200, 401, 403, 404]).toContain(response.status());\n if (response.status() === 200) {\n const body = await response.json();\n expect(body).toBeDefined();\n }\n });\n });\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-004 \u2014 loop_violates_mutation_policy\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-004: loop_violates_mutation_policy (chat API)', () => {\n test('chat API endpoint is reachable and validates the request body', async ({ request }) => {\n const response = await request.post(\n '/api/ai_assistant/ai/chat?agent=customers.account_assistant',\n {\n data: {},\n headers: { 'content-type': 'application/json' },\n },\n );\n // 400 (validation), 401 (unauth), 403 (no features), 404 (unknown agent), 409 (policy)\n expect([400, 401, 403, 404, 409]).toContain(response.status());\n });\n });\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-005 \u2014 LoopTrace panel in playground\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-005: LoopTrace panel renders in playground debug view', () => {\n test('playground debug toggle is visible and the loop trace area is discoverable', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\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(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/ai/agents/*/models', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agentId: 'customers.account_assistant',\n allowRuntimeOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [],\n }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n const chatArea = page.locator('[data-ai-playground-chat]').first();\n await expect(chatArea).toBeVisible({ timeout: 30_000 });\n\n // The loop trace panel is rendered inside AiChat debug panel.\n // We verify the chat lane itself loaded \u2014 trace panels only appear\n // after a chat turn with emitLoopTrace enabled.\n const debugToggle = page.locator('[data-ai-chat-debug-toggle]').first();\n const anyDebugToggle = debugToggle.or(page.locator('[aria-label=\"Debug\"]').first());\n // It's OK if the toggle isn't found \u2014 the panel is not displayed until after a turn.\n await expect(anyDebugToggle.or(chatArea)).toBeVisible({ timeout: 10_000 });\n });\n\n test('loop-finish SSE event format: chat API emits text/event-stream', async ({ request }) => {\n // Verify the chat route streams SSE (Content-Type: text/event-stream) when authorized.\n // An unauthenticated call should return 401 JSON (not a stream).\n const response = await request.post(\n '/api/ai_assistant/ai/chat?agent=customers.account_assistant',\n {\n data: { messages: [{ role: 'user', content: 'hello' }] },\n headers: { 'content-type': 'application/json' },\n },\n );\n // 401 = no auth; 200 = would be a stream (OK in CI with a configured agent)\n // Any 4xx is acceptable in integration CI where LLM keys are absent.\n expect([200, 401, 403, 404, 409]).toContain(response.status());\n });\n });\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-006 \u2014 Mutation gating survives tool-loop-agent engine swap\n //\n // Proof contract: a mutation tool call routed through an agent that declares\n // `executionEngine: 'tool-loop-agent'` MUST land in `ai_pending_actions` with\n // status `pending`. The test stubs the AI dispatcher via page.route() so no\n // real LLM is required.\n //\n // What this test checks:\n // 1. The `/api/ai_assistant/ai/agents` registry lists the tool-loop-agent entry\n // with `executionEngine: 'tool-loop-agent'` in the payload.\n // 2. When the chat dispatcher is mocked to simulate a mutation tool call response\n // from a `tool-loop-agent`-engine agent, the `ai_pending_actions` POST endpoint\n // is called (mutation-approval gate intercepted the tool call).\n // 3. The chat response carries a `pendingActionId` in the tool result envelope \u2014\n // the same contract that `stream-text` engine agents fulfil (non-regression).\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-006: mutation gating survives tool-loop-agent engine swap', () => {\n test('agents API returns tool-loop-agent entry with executionEngine field', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\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(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/ai/agents/*/models', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agentId: 'catalog.tool_loop_assistant',\n allowRuntimeOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [],\n }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // The mock injects a `tool-loop-agent` entry \u2014 verify the page loads\n // with both agents present in the agent picker.\n const chatArea = page.locator('[data-ai-playground-chat]').first();\n await expect(chatArea).toBeVisible({ timeout: 30_000 });\n\n // Assert that the mocked agents payload contains the tool-loop-agent entry\n // so we confirm the playground received the executionEngine field correctly.\n const agentsRoute = await page.evaluate(() => {\n return true; // Page loaded \u2014 agents were served from mock\n });\n expect(agentsRoute).toBe(true);\n });\n\n test('agents API payload carries executionEngine: tool-loop-agent on the catalog entry', async ({ page }) => {\n test.setTimeout(60_000);\n await login(page, 'superadmin');\n\n let capturedAgentsPayload: typeof agentsPayload | null = null;\n\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n capturedAgentsPayload = agentsPayload;\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(agentsPayload),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // Verify that the mocked payload carrying executionEngine was served.\n // This asserts the agents API contract for Phase 5:\n // - tool-loop-agent entries include `executionEngine: 'tool-loop-agent'`\n // - stream-text entries either omit it or set `executionEngine: 'stream-text'`\n expect(capturedAgentsPayload).not.toBeNull();\n const toolLoopEntry = capturedAgentsPayload!.agents.find(\n (a: (typeof agentsPayload)['agents'][number]) => a.id === 'catalog.tool_loop_assistant',\n );\n expect(toolLoopEntry).toBeDefined();\n expect(toolLoopEntry?.executionEngine).toBe('tool-loop-agent');\n\n const streamTextEntry = capturedAgentsPayload!.agents.find(\n (a: (typeof agentsPayload)['agents'][number]) => a.id === 'customers.account_assistant',\n );\n expect(streamTextEntry).toBeDefined();\n // stream-text is the default \u2014 may be absent from the payload or explicitly 'stream-text'\n expect(\n streamTextEntry?.executionEngine === undefined ||\n streamTextEntry?.executionEngine === 'stream-text',\n ).toBe(true);\n });\n\n test('mutation tool call via tool-loop-agent agent routes through pending-actions gate', async ({ page }) => {\n // Proof that the mutation-approval contract holds when executionEngine === 'tool-loop-agent'.\n //\n // Strategy: mock the chat dispatcher to return a SSE stream that simulates\n // a mutation tool call result. The mock mirrors what `prepareMutation` injects\n // into the tool result envelope: `{ status: \"pending-confirmation\", pendingActionId: \"<id>\" }`.\n // We then assert that:\n // (a) the chat API was called for the tool-loop-agent-engine agent\n // (b) the mock response carries a pendingActionId in the body \u2014 same contract as stream-text\n //\n // We do NOT require a real LLM \u2014 the page.route() stub replays a pre-recorded\n // SSE fragment that a real prepareMutation call would have emitted.\n\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n const fakePendingActionId = 'pai_tc006_toolloopagent_test';\n\n // Mock the agents listing so catalog.tool_loop_assistant is available.\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(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/ai/agents/*/models', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agentId: 'catalog.tool_loop_assistant',\n allowRuntimeOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [],\n }),\n });\n });\n\n // Mock the chat dispatcher to return a SSE stream that simulates a mutation\n // tool call result where prepareMutation placed the action in ai_pending_actions.\n // This replays what the real dispatcher would emit when the tool-loop-agent\n // engine calls a mutation tool and prepareMutation intercepts it.\n let chatApiCallCount = 0;\n await page.route('**/api/ai_assistant/ai/chat**', async (route) => {\n chatApiCallCount += 1;\n // Simulate a response stream where the mutation tool returned a pending envelope.\n // The SSE data-message format mirrors what useAiChat / AI SDK clients parse.\n const mutationToolResultSse = [\n // Tool call step\n `0:\"Let me update that product for you.\"\\n`,\n // Tool result \u2014 mutation gated \u2014 carries pendingActionId per prepareMutation contract\n `9:{\"toolCallId\":\"tc_001\",\"toolName\":\"catalog.list_products\",\"args\":{},\"result\":{\"status\":\"pending-confirmation\",\"pendingActionId\":\"${fakePendingActionId}\",\"message\":\"Mutation approval required. Confirm the pending action to proceed.\"}}\\n`,\n // Final text step\n `0:\"The mutation has been submitted for approval. Pending action ID: ${fakePendingActionId}\"\\n`,\n `e:{\"finishReason\":\"stop\",\"usage\":{\"promptTokens\":10,\"completionTokens\":5}}\\n`,\n `d:{\"finishReason\":\"stop\"}\\n`,\n ].join('');\n\n await route.fulfill({\n status: 200,\n contentType: 'text/event-stream',\n headers: {\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n },\n body: mutationToolResultSse,\n });\n });\n\n // Mock the pending-actions endpoint so page.route can assert it was called.\n const pendingActionsRequests: string[] = [];\n await page.route('**/api/ai/actions**', async (route) => {\n pendingActionsRequests.push(route.request().url());\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ id: fakePendingActionId, status: 'pending' }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // The playground must load and show the chat area.\n const chatArea = page.locator('[data-ai-playground-chat]').first();\n await expect(chatArea).toBeVisible({ timeout: 30_000 });\n\n // Core assertion: the mock chat response carries the pending-action envelope.\n // This proves that if the real runtime had called prepareMutation (which it\n // must for any mutation tool call regardless of executionEngine), the response\n // would contain pendingActionId \u2014 same contract as stream-text.\n //\n // The chat SSE body we returned above contains pendingActionId which is what\n // the prepareMutation wrapper injects. The assertion below verifies the\n // integration test correctly models the expected contract shape.\n expect(fakePendingActionId).toMatch(/^pai_/);\n expect(fakePendingActionId.length).toBeGreaterThan(4);\n });\n\n test('agents API contract \u2014 GET /api/ai_assistant/ai/agents is mounted', async ({ request }) => {\n const response = await request.get('/api/ai_assistant/ai/agents');\n expect([200, 401, 403]).toContain(response.status());\n if (response.status() === 200) {\n const body = await response.json();\n expect(body).toHaveProperty('agents');\n expect(Array.isArray(body.agents)).toBe(true);\n }\n });\n });\n});\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { test, expect } from '@playwright/test';\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';\n\n/**\n * TC-AI-AGENT-LOOP-001 through TC-AI-AGENT-LOOP-006\n *\n * Integration coverage for Phase 3 (operator budgets + kill switch) and\n * Phase 4 (LoopTrace, loopBudget dispatcher param, allowRuntimeOverride rename)\n * of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n *\n * Coverage table (per spec \u00A7Test scenarios):\n *\n * TC-AI-AGENT-LOOP-001 \u2014 Kill-switch banner: when loop_disabled is active for an agent,\n * `<AiChat>` renders the LoopDisabledBanner component.\n *\n * TC-AI-AGENT-LOOP-002 \u2014 loopBudget dispatcher param: `?loopBudget=tight` resolves to\n * the pinned tight preset, is blocked when `allowRuntimeOverride: false`, and the\n * 'default' value is a no-op.\n *\n * TC-AI-AGENT-LOOP-003 \u2014 hasToolCall stopWhen (API contract): chat API returns a\n * stream with `loopAbortReason: 'has-tool-call'` when stopWhen fires.\n *\n * TC-AI-AGENT-LOOP-004 \u2014 loop_violates_mutation_policy: a `prepareStep` that smuggles\n * a raw mutation handler triggers a 409 response with code `loop_violates_mutation_policy`.\n *\n * TC-AI-AGENT-LOOP-005 \u2014 LoopTrace panel (playground): the playground renders a\n * LoopTrace panel with step-level detail when the debug panel is open.\n *\n * TC-AI-AGENT-LOOP-006 \u2014 Mutation gating survives engine swap: a mock response for\n * an agent that declares `executionEngine: 'tool-loop-agent'` confirms that the\n * `/api/ai_assistant/ai/agents` payload still carries the agent entry and\n * tool-loop agents are listed by the registry.\n *\n * All API calls are intercepted via page.route() stubs \u2014 no LLM is required.\n */\n\ntest.describe('TC-AI-AGENT-LOOP-001\u2013006: agentic loop controls', () => {\n const settingsPath = '/backend/config/ai-assistant/settings';\n const playgroundPath = '/backend/config/ai-assistant/playground';\n\n const agentsPayload = {\n agents: [\n {\n id: 'customers.account_assistant',\n moduleId: 'customers',\n label: 'Account Assistant',\n description: 'Customer account AI assistant.',\n executionMode: 'chat',\n mutationPolicy: 'confirm-required',\n readOnly: false,\n maxSteps: 10,\n allowedTools: ['customers.update_deal_stage'],\n tools: [\n {\n name: 'customers.update_deal_stage',\n displayName: 'Update deal stage',\n isMutation: true,\n registered: true,\n },\n ],\n requiredFeatures: ['customers.view'],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n {\n id: 'catalog.tool_loop_assistant',\n moduleId: 'catalog',\n label: 'Tool Loop Assistant',\n description: 'Catalog assistant using tool-loop-agent engine.',\n executionMode: 'chat',\n mutationPolicy: 'confirm-required',\n readOnly: false,\n maxSteps: 5,\n allowedTools: ['catalog.list_products'],\n tools: [\n {\n name: 'catalog.list_products',\n displayName: 'List products',\n isMutation: false,\n registered: true,\n },\n ],\n requiredFeatures: ['catalog.view'],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n executionEngine: 'tool-loop-agent',\n },\n ],\n total: 2,\n };\n\n const settingsPayload = {\n provider: { id: 'anthropic', name: 'Anthropic', defaultModel: 'claude-haiku-4-5' },\n availableProviders: [\n {\n id: 'anthropic',\n name: 'Anthropic',\n isConfigured: true,\n defaultModels: [{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' }],\n },\n ],\n mcpKeyConfigured: true,\n resolvedDefault: {\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n baseURL: null,\n source: 'provider_default',\n },\n tenantOverride: null,\n agents: [\n {\n agentId: 'customers.account_assistant',\n moduleId: 'customers',\n allowRuntimeOverride: true,\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n baseURL: null,\n source: 'provider_default',\n },\n ],\n };\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-001 \u2014 Kill-switch banner\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-001: kill-switch banner in settings Loop panel', () => {\n test('settings page renders Loop policy section for the configured agent', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }),\n });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ tools: [] }),\n });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n const settingsContainer = page.locator('[data-ai-assistant-settings]');\n await expect(settingsContainer).toBeVisible({ timeout: 30_000 });\n });\n\n test('LoopDisabledBanner export is present in ui package', async ({ request }) => {\n // Smoke test: the `loop-override` API route is mounted and reachable.\n // (Does not require auth - 401 is an acceptable response.)\n const response = await request.get(\n '/api/ai_assistant/ai/agents/customers.account_assistant/loop-override',\n );\n expect([200, 401, 403, 404]).toContain(response.status());\n });\n });\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-002 \u2014 loopBudget dispatcher param\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-002: loopBudget query-param on POST /api/ai_assistant/ai/chat', () => {\n test('endpoint is mounted and returns 401 for unauthenticated requests', async ({ request }) => {\n const response = await request.post(\n '/api/ai_assistant/ai/chat?agent=customers.account_assistant&loopBudget=tight',\n {\n data: { messages: [{ role: 'user', content: 'test' }] },\n headers: { 'content-type': 'application/json' },\n },\n );\n expect([200, 401, 403, 404, 409]).toContain(response.status());\n });\n\n test('playground renders and loopBudget picker area is accessible', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\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(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/ai/agents/*/models', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agentId: 'customers.account_assistant',\n allowRuntimeOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [],\n }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n const chatArea = page.locator('[data-ai-playground-chat]').first();\n await expect(chatArea).toBeVisible({ timeout: 30_000 });\n });\n });\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-003 \u2014 hasToolCall stopWhen (API contract)\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-003: loop-override route for stopWhen declaration', () => {\n test('loop-override GET route is mounted (returns 200, 401, or 404)', async ({ request }) => {\n const response = await request.get(\n '/api/ai_assistant/ai/agents/customers.account_assistant/loop-override',\n );\n expect([200, 401, 403, 404]).toContain(response.status());\n if (response.status() === 200) {\n const body = await response.json();\n expect(body).toBeDefined();\n }\n });\n });\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-004 \u2014 loop_violates_mutation_policy\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-004: loop_violates_mutation_policy (chat API)', () => {\n test('chat API endpoint is reachable and validates the request body', async ({ request }) => {\n const response = await request.post(\n '/api/ai_assistant/ai/chat?agent=customers.account_assistant',\n {\n data: {},\n headers: { 'content-type': 'application/json' },\n },\n );\n // 400 (validation), 401 (unauth), 403 (no features), 404 (unknown agent), 409 (policy)\n expect([400, 401, 403, 404, 409]).toContain(response.status());\n });\n });\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-005 \u2014 LoopTrace panel in playground\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-005: LoopTrace panel renders in playground debug view', () => {\n test('playground debug toggle is visible and the loop trace area is discoverable', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\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(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/ai/agents/*/models', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agentId: 'customers.account_assistant',\n allowRuntimeOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [],\n }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n const chatArea = page.locator('[data-ai-playground-chat]').first();\n await expect(chatArea).toBeVisible({ timeout: 30_000 });\n\n // The loop trace panel is rendered inside AiChat debug panel.\n // We verify the chat lane itself loaded \u2014 trace panels only appear\n // after a chat turn with emitLoopTrace enabled.\n const debugToggle = page.locator('[data-ai-chat-debug-toggle]').first();\n const anyDebugToggle = debugToggle.or(page.locator('[aria-label=\"Debug\"]').first());\n // It's OK if the toggle isn't found \u2014 the panel is not displayed until after a turn.\n await expect(anyDebugToggle.or(chatArea)).toBeVisible({ timeout: 10_000 });\n });\n\n test('loop-finish SSE event format: chat API emits text/event-stream', async ({ request }) => {\n // Verify the chat route streams SSE (Content-Type: text/event-stream) when authorized.\n // An unauthenticated call should return 401 JSON (not a stream).\n const response = await request.post(\n '/api/ai_assistant/ai/chat?agent=customers.account_assistant',\n {\n data: { messages: [{ role: 'user', content: 'hello' }] },\n headers: { 'content-type': 'application/json' },\n },\n );\n // 401 = no auth; 200 = would be a stream (OK in CI with a configured agent)\n // Any 4xx is acceptable in integration CI where LLM keys are absent.\n expect([200, 401, 403, 404, 409]).toContain(response.status());\n });\n });\n\n // ---------------------------------------------------------------------------\n // TC-AI-AGENT-LOOP-006 \u2014 Mutation gating survives tool-loop-agent engine swap\n //\n // Proof contract: a mutation tool call routed through an agent that declares\n // `executionEngine: 'tool-loop-agent'` MUST land in `ai_pending_actions` with\n // status `pending`. The test stubs the AI dispatcher via page.route() so no\n // real LLM is required.\n //\n // What this test checks:\n // 1. The `/api/ai_assistant/ai/agents` registry lists the tool-loop-agent entry\n // with `executionEngine: 'tool-loop-agent'` in the payload.\n // 2. When the chat dispatcher is mocked to simulate a mutation tool call response\n // from a `tool-loop-agent`-engine agent, the `ai_pending_actions` POST endpoint\n // is called (mutation-approval gate intercepted the tool call).\n // 3. The chat response carries a `pendingActionId` in the tool result envelope \u2014\n // the same contract that `stream-text` engine agents fulfil (non-regression).\n // ---------------------------------------------------------------------------\n test.describe('TC-AI-AGENT-LOOP-006: mutation gating survives tool-loop-agent engine swap', () => {\n test('agents API returns tool-loop-agent entry with executionEngine field', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\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(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/ai/agents/*/models', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agentId: 'catalog.tool_loop_assistant',\n allowRuntimeOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [],\n }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // The mock injects a `tool-loop-agent` entry \u2014 verify the page loads\n // with both agents present in the agent picker.\n const chatArea = page.locator('[data-ai-playground-chat]').first();\n await expect(chatArea).toBeVisible({ timeout: 30_000 });\n\n // Assert that the mocked agents payload contains the tool-loop-agent entry\n // so we confirm the playground received the executionEngine field correctly.\n const agentsRoute = await page.evaluate(() => {\n return true; // Page loaded \u2014 agents were served from mock\n });\n expect(agentsRoute).toBe(true);\n });\n\n test('agents API payload carries executionEngine: tool-loop-agent on the catalog entry', async ({ page }) => {\n test.setTimeout(60_000);\n await login(page, 'superadmin');\n\n let capturedAgentsPayload: typeof agentsPayload | null = null;\n\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n capturedAgentsPayload = agentsPayload;\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(agentsPayload),\n });\n });\n\n // The agents request fires from a post-hydration `useQuery`, not from\n // navigation, so the route handler that sets `capturedAgentsPayload` runs\n // after `goto` resolves. Await the response deterministically instead of\n // asserting the captured payload immediately (which races hydration).\n const agentsResponsePromise = page.waitForResponse(\n (response) =>\n response.url().includes('/api/ai_assistant/ai/agents') &&\n response.request().method() === 'GET',\n );\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n await agentsResponsePromise;\n\n // Verify that the mocked payload carrying executionEngine was served.\n // This asserts the agents API contract for Phase 5:\n // - tool-loop-agent entries include `executionEngine: 'tool-loop-agent'`\n // - stream-text entries either omit it or set `executionEngine: 'stream-text'`\n expect(capturedAgentsPayload).not.toBeNull();\n const toolLoopEntry = capturedAgentsPayload!.agents.find(\n (a: (typeof agentsPayload)['agents'][number]) => a.id === 'catalog.tool_loop_assistant',\n );\n expect(toolLoopEntry).toBeDefined();\n expect(toolLoopEntry?.executionEngine).toBe('tool-loop-agent');\n\n const streamTextEntry = capturedAgentsPayload!.agents.find(\n (a: (typeof agentsPayload)['agents'][number]) => a.id === 'customers.account_assistant',\n );\n expect(streamTextEntry).toBeDefined();\n // stream-text is the default \u2014 may be absent from the payload or explicitly 'stream-text'\n expect(\n streamTextEntry?.executionEngine === undefined ||\n streamTextEntry?.executionEngine === 'stream-text',\n ).toBe(true);\n });\n\n test('mutation tool call via tool-loop-agent agent routes through pending-actions gate', async ({ page }) => {\n // Proof that the mutation-approval contract holds when executionEngine === 'tool-loop-agent'.\n //\n // Strategy: mock the chat dispatcher to return a SSE stream that simulates\n // a mutation tool call result. The mock mirrors what `prepareMutation` injects\n // into the tool result envelope: `{ status: \"pending-confirmation\", pendingActionId: \"<id>\" }`.\n // We then assert that:\n // (a) the chat API was called for the tool-loop-agent-engine agent\n // (b) the mock response carries a pendingActionId in the body \u2014 same contract as stream-text\n //\n // We do NOT require a real LLM \u2014 the page.route() stub replays a pre-recorded\n // SSE fragment that a real prepareMutation call would have emitted.\n\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n const fakePendingActionId = 'pai_tc006_toolloopagent_test';\n\n // Mock the agents listing so catalog.tool_loop_assistant is available.\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(agentsPayload),\n });\n });\n\n await page.route('**/api/ai_assistant/ai/agents/*/models', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n agentId: 'catalog.tool_loop_assistant',\n allowRuntimeOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [],\n }),\n });\n });\n\n // Mock the chat dispatcher to return a SSE stream that simulates a mutation\n // tool call result where prepareMutation placed the action in ai_pending_actions.\n // This replays what the real dispatcher would emit when the tool-loop-agent\n // engine calls a mutation tool and prepareMutation intercepts it.\n let chatApiCallCount = 0;\n await page.route('**/api/ai_assistant/ai/chat**', async (route) => {\n chatApiCallCount += 1;\n // Simulate a response stream where the mutation tool returned a pending envelope.\n // The SSE data-message format mirrors what useAiChat / AI SDK clients parse.\n const mutationToolResultSse = [\n // Tool call step\n `0:\"Let me update that product for you.\"\\n`,\n // Tool result \u2014 mutation gated \u2014 carries pendingActionId per prepareMutation contract\n `9:{\"toolCallId\":\"tc_001\",\"toolName\":\"catalog.list_products\",\"args\":{},\"result\":{\"status\":\"pending-confirmation\",\"pendingActionId\":\"${fakePendingActionId}\",\"message\":\"Mutation approval required. Confirm the pending action to proceed.\"}}\\n`,\n // Final text step\n `0:\"The mutation has been submitted for approval. Pending action ID: ${fakePendingActionId}\"\\n`,\n `e:{\"finishReason\":\"stop\",\"usage\":{\"promptTokens\":10,\"completionTokens\":5}}\\n`,\n `d:{\"finishReason\":\"stop\"}\\n`,\n ].join('');\n\n await route.fulfill({\n status: 200,\n contentType: 'text/event-stream',\n headers: {\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n },\n body: mutationToolResultSse,\n });\n });\n\n // Mock the pending-actions endpoint so page.route can assert it was called.\n const pendingActionsRequests: string[] = [];\n await page.route('**/api/ai/actions**', async (route) => {\n pendingActionsRequests.push(route.request().url());\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ id: fakePendingActionId, status: 'pending' }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // The playground must load and show the chat area.\n const chatArea = page.locator('[data-ai-playground-chat]').first();\n await expect(chatArea).toBeVisible({ timeout: 30_000 });\n\n // Core assertion: the mock chat response carries the pending-action envelope.\n // This proves that if the real runtime had called prepareMutation (which it\n // must for any mutation tool call regardless of executionEngine), the response\n // would contain pendingActionId \u2014 same contract as stream-text.\n //\n // The chat SSE body we returned above contains pendingActionId which is what\n // the prepareMutation wrapper injects. The assertion below verifies the\n // integration test correctly models the expected contract shape.\n expect(fakePendingActionId).toMatch(/^pai_/);\n expect(fakePendingActionId.length).toBeGreaterThan(4);\n });\n\n test('agents API contract \u2014 GET /api/ai_assistant/ai/agents is mounted', async ({ request }) => {\n const response = await request.get('/api/ai_assistant/ai/agents');\n expect([200, 401, 403]).toContain(response.status());\n if (response.status() === 200) {\n const body = await response.json();\n expect(body).toHaveProperty('agents');\n expect(Array.isArray(body.agents)).toBe(true);\n }\n });\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ai-assistant",
|
|
3
|
-
"version": "0.6.4-develop.
|
|
3
|
+
"version": "0.6.4-develop.4264.1.53368d85fe",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -98,16 +98,16 @@
|
|
|
98
98
|
"zod-to-json-schema": "^3.25.2"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
101
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
102
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
101
|
+
"@open-mercato/shared": "0.6.4-develop.4264.1.53368d85fe",
|
|
102
|
+
"@open-mercato/ui": "0.6.4-develop.4264.1.53368d85fe",
|
|
103
103
|
"react": "^19.0.0",
|
|
104
104
|
"react-dom": "^19.0.0",
|
|
105
105
|
"zod": ">=3.23.0"
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
|
-
"@open-mercato/cli": "0.6.4-develop.
|
|
109
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
110
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
108
|
+
"@open-mercato/cli": "0.6.4-develop.4264.1.53368d85fe",
|
|
109
|
+
"@open-mercato/shared": "0.6.4-develop.4264.1.53368d85fe",
|
|
110
|
+
"@open-mercato/ui": "0.6.4-develop.4264.1.53368d85fe",
|
|
111
111
|
"@types/react": "^19.2.15",
|
|
112
112
|
"@types/react-dom": "^19.2.3",
|
|
113
113
|
"react": "19.2.6",
|
|
@@ -383,7 +383,18 @@ test.describe('TC-AI-AGENT-LOOP-001–006: agentic loop controls', () => {
|
|
|
383
383
|
});
|
|
384
384
|
});
|
|
385
385
|
|
|
386
|
+
// The agents request fires from a post-hydration `useQuery`, not from
|
|
387
|
+
// navigation, so the route handler that sets `capturedAgentsPayload` runs
|
|
388
|
+
// after `goto` resolves. Await the response deterministically instead of
|
|
389
|
+
// asserting the captured payload immediately (which races hydration).
|
|
390
|
+
const agentsResponsePromise = page.waitForResponse(
|
|
391
|
+
(response) =>
|
|
392
|
+
response.url().includes('/api/ai_assistant/ai/agents') &&
|
|
393
|
+
response.request().method() === 'GET',
|
|
394
|
+
);
|
|
395
|
+
|
|
386
396
|
await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });
|
|
397
|
+
await agentsResponsePromise;
|
|
387
398
|
|
|
388
399
|
// Verify that the mocked payload carrying executionEngine was served.
|
|
389
400
|
// This asserts the agents API contract for Phase 5:
|