@open-mercato/ai-assistant 0.6.5-develop.4964.1.ae0edca575 → 0.6.5-develop.5033.1.c970204a3f

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.
@@ -259,21 +259,17 @@ test.describe("TC-AI-AGENT-LOOP-001\u2013006: agentic loop controls", () => {
259
259
  test("agents API payload carries executionEngine: tool-loop-agent on the catalog entry", async ({ page }) => {
260
260
  test.setTimeout(6e4);
261
261
  await login(page, "superadmin");
262
- let capturedAgentsPayload = null;
263
262
  await page.route("**/api/ai_assistant/ai/agents", async (route) => {
264
- capturedAgentsPayload = agentsPayload;
265
263
  await route.fulfill({
266
264
  status: 200,
267
265
  contentType: "application/json",
268
266
  body: JSON.stringify(agentsPayload)
269
267
  });
270
268
  });
271
- const agentsResponsePromise = page.waitForResponse(
272
- (response) => response.url().includes("/api/ai_assistant/ai/agents") && response.request().method() === "GET"
273
- );
274
- await page.goto(playgroundPath, { waitUntil: "domcontentloaded" });
275
- await agentsResponsePromise;
276
- expect(capturedAgentsPayload).not.toBeNull();
269
+ const capturedAgentsPayload = await page.evaluate(async () => {
270
+ const response = await fetch("/api/ai_assistant/ai/agents");
271
+ return await response.json();
272
+ });
277
273
  const toolLoopEntry = capturedAgentsPayload.agents.find(
278
274
  (a) => a.id === "catalog.tool_loop_assistant"
279
275
  );
@@ -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 // 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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",
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 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 const capturedAgentsPayload = await page.evaluate(async () => {\n const response = await fetch('/api/ai_assistant/ai/agents');\n return (await response.json()) as typeof agentsPayload;\n });\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 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
  }
@@ -57,6 +57,7 @@ const STEP_EVENT = {
57
57
  updatedAt: "2026-05-01T10:00:00.000Z"
58
58
  };
59
59
  test.describe("TC-AI-TOKEN-USAGE-001\u2013005: token usage stats page", () => {
60
+ test.describe.configure({ timeout: 12e4 });
60
61
  test("TC-AI-TOKEN-USAGE-001: usage page renders summary tiles for superadmin", async ({ page }) => {
61
62
  await login(page, "superadmin");
62
63
  await page.route("**/api/ai_assistant/usage/daily**", async (route) => {
@@ -73,7 +74,7 @@ test.describe("TC-AI-TOKEN-USAGE-001\u2013005: token usage stats page", () => {
73
74
  body: JSON.stringify(EMPTY_SESSIONS_PAYLOAD)
74
75
  });
75
76
  });
76
- await page.goto(USAGE_PAGE, { waitUntil: "domcontentloaded" });
77
+ await page.goto(USAGE_PAGE, { waitUntil: "commit" });
77
78
  const summaryTile = page.locator("p.font-semibold.text-xl", { hasText: /^(1,000|500)$/ }).first();
78
79
  await expect(summaryTile).toBeVisible({ timeout: 15e3 });
79
80
  });
@@ -101,7 +102,7 @@ test.describe("TC-AI-TOKEN-USAGE-001\u2013005: token usage stats page", () => {
101
102
  (response) => response.url().includes("/api/ai_assistant/usage/sessions") && response.status() === 200,
102
103
  { timeout: 15e3 }
103
104
  );
104
- await page.goto(USAGE_PAGE, { waitUntil: "domcontentloaded" });
105
+ await page.goto(USAGE_PAGE, { waitUntil: "commit" });
105
106
  await Promise.all([initialDailyRequest, initialSessionsRequest]);
106
107
  const fromInput = page.locator("#usage-from");
107
108
  const toInput = page.locator("#usage-to");
@@ -138,7 +139,7 @@ test.describe("TC-AI-TOKEN-USAGE-001\u2013005: token usage stats page", () => {
138
139
  body: JSON.stringify({ sessions: [SESSION_ROW], total: 1, limit: 50, offset: 0 })
139
140
  });
140
141
  });
141
- await page.goto(USAGE_PAGE, { waitUntil: "domcontentloaded" });
142
+ await page.goto(USAGE_PAGE, { waitUntil: "commit" });
142
143
  const sessionCell = page.getByText("00000000").first();
143
144
  await expect(sessionCell).toBeVisible({ timeout: 15e3 });
144
145
  });
@@ -169,7 +170,7 @@ test.describe("TC-AI-TOKEN-USAGE-001\u2013005: token usage stats page", () => {
169
170
  body: JSON.stringify({ sessions: [SESSION_ROW], total: 1, limit: 50, offset: 0 })
170
171
  });
171
172
  });
172
- await page.goto(USAGE_PAGE, { waitUntil: "domcontentloaded" });
173
+ await page.goto(USAGE_PAGE, { waitUntil: "commit" });
173
174
  const sessionCell = page.getByText("00000000").first();
174
175
  await expect(sessionCell).toBeVisible({ timeout: 15e3 });
175
176
  await sessionCell.click();
@@ -182,7 +183,7 @@ test.describe("TC-AI-TOKEN-USAGE-001\u2013005: token usage stats page", () => {
182
183
  const context = await browser.newContext();
183
184
  const page = await context.newPage();
184
185
  try {
185
- await page.goto(USAGE_PAGE, { waitUntil: "domcontentloaded" });
186
+ await page.goto(USAGE_PAGE, { waitUntil: "commit" });
186
187
  await page.waitForURL(/\/login/, { timeout: 15e3 });
187
188
  expect(page.url()).toMatch(/\/login/);
188
189
  } finally {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.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-TOKEN-USAGE-001 through TC-AI-TOKEN-USAGE-005\n *\n * Integration coverage for Phase 6 (Token Usage Tracking & Stats Page) of\n * spec `2026-04-28-ai-agents-agentic-loop-controls`.\n *\n * TC-AI-TOKEN-USAGE-001 \u2014 Usage page loads and renders summary tiles (ACL gate).\n * TC-AI-TOKEN-USAGE-002 \u2014 Date filter apply re-fetches with updated params.\n * TC-AI-TOKEN-USAGE-003 \u2014 Sessions list renders when API returns session rows.\n * TC-AI-TOKEN-USAGE-004 \u2014 Clicking a session row opens the detail dialog.\n * TC-AI-TOKEN-USAGE-005 \u2014 Unauthenticated visit to usage page redirects to login.\n *\n * All API calls are intercepted via page.route() stubs \u2014 no real DB needed.\n */\n\nconst USAGE_PAGE = '/backend/config/ai-assistant/usage';\n\nconst EMPTY_DAILY_PAYLOAD = { rows: [], total: 0 };\nconst EMPTY_SESSIONS_PAYLOAD = { sessions: [], total: 0, limit: 50, offset: 0 };\n\nconst DAILY_ROW = {\n id: 'row-1',\n tenantId: 'tenant-1',\n organizationId: null,\n day: '2026-05-01',\n agentId: 'catalog.assistant',\n modelId: 'claude-haiku-4-5',\n providerId: 'anthropic',\n inputTokens: '1000',\n outputTokens: '500',\n cachedInputTokens: '0',\n reasoningTokens: '0',\n stepCount: '5',\n turnCount: '3',\n sessionCount: '2',\n createdAt: '2026-05-01T12:00:00.000Z',\n updatedAt: '2026-05-01T12:00:00.000Z',\n};\n\nconst SESSION_ROW = {\n sessionId: '00000000-0000-0000-0000-000000000001',\n agentId: 'catalog.assistant',\n moduleId: 'catalog',\n userId: 'user-1',\n startedAt: '2026-05-01T10:00:00.000Z',\n lastEventAt: '2026-05-01T10:05:00.000Z',\n stepCount: 5,\n turnCount: 3,\n inputTokens: 1000,\n outputTokens: 500,\n cachedInputTokens: 0,\n reasoningTokens: 0,\n};\n\nconst STEP_EVENT = {\n id: 'evt-1',\n tenantId: 'tenant-1',\n organizationId: null,\n userId: 'user-1',\n agentId: 'catalog.assistant',\n moduleId: 'catalog',\n sessionId: '00000000-0000-0000-0000-000000000001',\n turnId: '00000000-0000-0000-0000-000000000002',\n stepIndex: 0,\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n inputTokens: 1000,\n outputTokens: 500,\n cachedInputTokens: null,\n reasoningTokens: null,\n finishReason: 'stop',\n loopAbortReason: null,\n createdAt: '2026-05-01T10:00:00.000Z',\n updatedAt: '2026-05-01T10:00:00.000Z',\n};\n\ntest.describe('TC-AI-TOKEN-USAGE-001\u2013005: token usage stats page', () => {\n test('TC-AI-TOKEN-USAGE-001: usage page renders summary tiles for superadmin', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/usage/daily**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ rows: [DAILY_ROW], total: 1 }),\n });\n });\n\n await page.route('**/api/ai_assistant/usage/sessions**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(EMPTY_SESSIONS_PAYLOAD),\n });\n });\n\n await page.goto(USAGE_PAGE, { waitUntil: 'domcontentloaded' });\n\n const summaryTile = page.locator('p.font-semibold.text-xl', { hasText: /^(1,000|500)$/ }).first();\n await expect(summaryTile).toBeVisible({ timeout: 15_000 });\n });\n\n test('TC-AI-TOKEN-USAGE-002: apply filter triggers re-fetch with new date params', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/usage/daily**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(EMPTY_DAILY_PAYLOAD),\n });\n });\n\n await page.route('**/api/ai_assistant/usage/sessions**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(EMPTY_SESSIONS_PAYLOAD),\n });\n });\n\n const initialDailyRequest = page.waitForResponse(\n (response) => response.url().includes('/api/ai_assistant/usage/daily') && response.status() === 200,\n { timeout: 15_000 },\n );\n const initialSessionsRequest = page.waitForResponse(\n (response) => response.url().includes('/api/ai_assistant/usage/sessions') && response.status() === 200,\n { timeout: 15_000 },\n );\n await page.goto(USAGE_PAGE, { waitUntil: 'domcontentloaded' });\n await Promise.all([initialDailyRequest, initialSessionsRequest]);\n\n const fromInput = page.locator('#usage-from');\n const toInput = page.locator('#usage-to');\n const applyButton = page.getByRole('button', { name: /apply/i });\n\n await expect(fromInput).toBeVisible({ timeout: 10_000 });\n\n await fromInput.fill('2026-04-01');\n await toInput.fill('2026-04-30');\n await expect(fromInput).toHaveValue('2026-04-01');\n await expect(toInput).toHaveValue('2026-04-30');\n const updatedDailyRequest = page.waitForResponse(\n (response) =>\n response.url().includes('/api/ai_assistant/usage/daily') &&\n response.url().includes('from=2026-04-01') &&\n response.url().includes('to=2026-04-30') &&\n response.status() === 200,\n { timeout: 10_000 },\n );\n await applyButton.click();\n await updatedDailyRequest;\n });\n\n test('TC-AI-TOKEN-USAGE-003: sessions list renders rows when API returns sessions', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/usage/daily**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(EMPTY_DAILY_PAYLOAD),\n });\n });\n\n await page.route('**/api/ai_assistant/usage/sessions**', async (route, request) => {\n if (request.url().includes('/sessions/')) {\n await route.continue();\n return;\n }\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: [SESSION_ROW], total: 1, limit: 50, offset: 0 }),\n });\n });\n\n await page.goto(USAGE_PAGE, { waitUntil: 'domcontentloaded' });\n\n const sessionCell = page.getByText('00000000').first();\n await expect(sessionCell).toBeVisible({ timeout: 15_000 });\n });\n\n test('TC-AI-TOKEN-USAGE-004: clicking a session row opens the detail dialog', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/usage/daily**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(EMPTY_DAILY_PAYLOAD),\n });\n });\n\n await page.route('**/api/ai_assistant/usage/sessions/00000000-0000-0000-0000-000000000001', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ events: [STEP_EVENT], total: 1, sessionId: SESSION_ROW.sessionId }),\n });\n });\n\n await page.route('**/api/ai_assistant/usage/sessions**', async (route, request) => {\n if (request.url().includes('/00000000-0000-0000-0000-000000000001')) {\n // Fall back to the previously registered (more specific) handler.\n await route.fallback();\n return;\n }\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: [SESSION_ROW], total: 1, limit: 50, offset: 0 }),\n });\n });\n\n await page.goto(USAGE_PAGE, { waitUntil: 'domcontentloaded' });\n\n const sessionCell = page.getByText('00000000').first();\n await expect(sessionCell).toBeVisible({ timeout: 15_000 });\n await sessionCell.click();\n\n const dialogTitle = page.getByRole('dialog');\n await expect(dialogTitle).toBeVisible({ timeout: 10_000 });\n\n const modelCell = page.getByText('claude-haiku-4-5').first();\n await expect(modelCell).toBeVisible({ timeout: 5_000 });\n });\n\n test('TC-AI-TOKEN-USAGE-005: unauthenticated visit redirects to login', async ({ browser }) => {\n const context = await browser.newContext();\n const page = await context.newPage();\n try {\n await page.goto(USAGE_PAGE, { waitUntil: 'domcontentloaded' });\n await page.waitForURL(/\\/login/, { timeout: 15_000 });\n expect(page.url()).toMatch(/\\/login/);\n } finally {\n await context.close();\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-TOKEN-USAGE-001 through TC-AI-TOKEN-USAGE-005\n *\n * Integration coverage for Phase 6 (Token Usage Tracking & Stats Page) of\n * spec `2026-04-28-ai-agents-agentic-loop-controls`.\n *\n * TC-AI-TOKEN-USAGE-001 \u2014 Usage page loads and renders summary tiles (ACL gate).\n * TC-AI-TOKEN-USAGE-002 \u2014 Date filter apply re-fetches with updated params.\n * TC-AI-TOKEN-USAGE-003 \u2014 Sessions list renders when API returns session rows.\n * TC-AI-TOKEN-USAGE-004 \u2014 Clicking a session row opens the detail dialog.\n * TC-AI-TOKEN-USAGE-005 \u2014 Unauthenticated visit to usage page redirects to login.\n *\n * All API calls are intercepted via page.route() stubs \u2014 no real DB needed.\n */\n\nconst USAGE_PAGE = '/backend/config/ai-assistant/usage';\n\nconst EMPTY_DAILY_PAYLOAD = { rows: [], total: 0 };\nconst EMPTY_SESSIONS_PAYLOAD = { sessions: [], total: 0, limit: 50, offset: 0 };\n\nconst DAILY_ROW = {\n id: 'row-1',\n tenantId: 'tenant-1',\n organizationId: null,\n day: '2026-05-01',\n agentId: 'catalog.assistant',\n modelId: 'claude-haiku-4-5',\n providerId: 'anthropic',\n inputTokens: '1000',\n outputTokens: '500',\n cachedInputTokens: '0',\n reasoningTokens: '0',\n stepCount: '5',\n turnCount: '3',\n sessionCount: '2',\n createdAt: '2026-05-01T12:00:00.000Z',\n updatedAt: '2026-05-01T12:00:00.000Z',\n};\n\nconst SESSION_ROW = {\n sessionId: '00000000-0000-0000-0000-000000000001',\n agentId: 'catalog.assistant',\n moduleId: 'catalog',\n userId: 'user-1',\n startedAt: '2026-05-01T10:00:00.000Z',\n lastEventAt: '2026-05-01T10:05:00.000Z',\n stepCount: 5,\n turnCount: 3,\n inputTokens: 1000,\n outputTokens: 500,\n cachedInputTokens: 0,\n reasoningTokens: 0,\n};\n\nconst STEP_EVENT = {\n id: 'evt-1',\n tenantId: 'tenant-1',\n organizationId: null,\n userId: 'user-1',\n agentId: 'catalog.assistant',\n moduleId: 'catalog',\n sessionId: '00000000-0000-0000-0000-000000000001',\n turnId: '00000000-0000-0000-0000-000000000002',\n stepIndex: 0,\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n inputTokens: 1000,\n outputTokens: 500,\n cachedInputTokens: null,\n reasoningTokens: null,\n finishReason: 'stop',\n loopAbortReason: null,\n createdAt: '2026-05-01T10:00:00.000Z',\n updatedAt: '2026-05-01T10:00:00.000Z',\n};\n\ntest.describe('TC-AI-TOKEN-USAGE-001\u2013005: token usage stats page', () => {\n test.describe.configure({ timeout: 120_000 });\n\n test('TC-AI-TOKEN-USAGE-001: usage page renders summary tiles for superadmin', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/usage/daily**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ rows: [DAILY_ROW], total: 1 }),\n });\n });\n\n await page.route('**/api/ai_assistant/usage/sessions**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(EMPTY_SESSIONS_PAYLOAD),\n });\n });\n\n await page.goto(USAGE_PAGE, { waitUntil: 'commit' });\n\n const summaryTile = page.locator('p.font-semibold.text-xl', { hasText: /^(1,000|500)$/ }).first();\n await expect(summaryTile).toBeVisible({ timeout: 15_000 });\n });\n\n test('TC-AI-TOKEN-USAGE-002: apply filter triggers re-fetch with new date params', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/usage/daily**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(EMPTY_DAILY_PAYLOAD),\n });\n });\n\n await page.route('**/api/ai_assistant/usage/sessions**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(EMPTY_SESSIONS_PAYLOAD),\n });\n });\n\n const initialDailyRequest = page.waitForResponse(\n (response) => response.url().includes('/api/ai_assistant/usage/daily') && response.status() === 200,\n { timeout: 15_000 },\n );\n const initialSessionsRequest = page.waitForResponse(\n (response) => response.url().includes('/api/ai_assistant/usage/sessions') && response.status() === 200,\n { timeout: 15_000 },\n );\n await page.goto(USAGE_PAGE, { waitUntil: 'commit' });\n await Promise.all([initialDailyRequest, initialSessionsRequest]);\n\n const fromInput = page.locator('#usage-from');\n const toInput = page.locator('#usage-to');\n const applyButton = page.getByRole('button', { name: /apply/i });\n\n await expect(fromInput).toBeVisible({ timeout: 10_000 });\n\n await fromInput.fill('2026-04-01');\n await toInput.fill('2026-04-30');\n await expect(fromInput).toHaveValue('2026-04-01');\n await expect(toInput).toHaveValue('2026-04-30');\n const updatedDailyRequest = page.waitForResponse(\n (response) =>\n response.url().includes('/api/ai_assistant/usage/daily') &&\n response.url().includes('from=2026-04-01') &&\n response.url().includes('to=2026-04-30') &&\n response.status() === 200,\n { timeout: 10_000 },\n );\n await applyButton.click();\n await updatedDailyRequest;\n });\n\n test('TC-AI-TOKEN-USAGE-003: sessions list renders rows when API returns sessions', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/usage/daily**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(EMPTY_DAILY_PAYLOAD),\n });\n });\n\n await page.route('**/api/ai_assistant/usage/sessions**', async (route, request) => {\n if (request.url().includes('/sessions/')) {\n await route.continue();\n return;\n }\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: [SESSION_ROW], total: 1, limit: 50, offset: 0 }),\n });\n });\n\n await page.goto(USAGE_PAGE, { waitUntil: 'commit' });\n\n const sessionCell = page.getByText('00000000').first();\n await expect(sessionCell).toBeVisible({ timeout: 15_000 });\n });\n\n test('TC-AI-TOKEN-USAGE-004: clicking a session row opens the detail dialog', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/usage/daily**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(EMPTY_DAILY_PAYLOAD),\n });\n });\n\n await page.route('**/api/ai_assistant/usage/sessions/00000000-0000-0000-0000-000000000001', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ events: [STEP_EVENT], total: 1, sessionId: SESSION_ROW.sessionId }),\n });\n });\n\n await page.route('**/api/ai_assistant/usage/sessions**', async (route, request) => {\n if (request.url().includes('/00000000-0000-0000-0000-000000000001')) {\n // Fall back to the previously registered (more specific) handler.\n await route.fallback();\n return;\n }\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: [SESSION_ROW], total: 1, limit: 50, offset: 0 }),\n });\n });\n\n await page.goto(USAGE_PAGE, { waitUntil: 'commit' });\n\n const sessionCell = page.getByText('00000000').first();\n await expect(sessionCell).toBeVisible({ timeout: 15_000 });\n await sessionCell.click();\n\n const dialogTitle = page.getByRole('dialog');\n await expect(dialogTitle).toBeVisible({ timeout: 10_000 });\n\n const modelCell = page.getByText('claude-haiku-4-5').first();\n await expect(modelCell).toBeVisible({ timeout: 5_000 });\n });\n\n test('TC-AI-TOKEN-USAGE-005: unauthenticated visit redirects to login', async ({ browser }) => {\n const context = await browser.newContext();\n const page = await context.newPage();\n try {\n await page.goto(USAGE_PAGE, { waitUntil: 'commit' });\n await page.waitForURL(/\\/login/, { timeout: 15_000 });\n expect(page.url()).toMatch(/\\/login/);\n } finally {\n await context.close();\n }\n });\n});\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -373,18 +373,8 @@ function buildEntitySchemas(graph) {
373
373
  };
374
374
  });
375
375
  }
376
- function detectMutationInCode(code) {
377
- const methods = [];
378
- const pattern = /method:\s*['"](\w+)['"]/gi;
379
- let match;
380
- while ((match = pattern.exec(code)) !== null) {
381
- const method = match[1].toUpperCase();
382
- if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
383
- methods.push(method);
384
- }
385
- }
386
- return { hasMutation: methods.length > 0, methods };
387
- }
376
+ const CODE_MODE_MAX_API_CALLS = 50;
377
+ const CODE_MODE_MAX_MUTATION_CALLS = 20;
388
378
  async function loadCodeModeTools() {
389
379
  const commonTypes = await generateCommonTypes();
390
380
  registerSearchTool();
@@ -488,17 +478,20 @@ RULES: For FIND/LIST \u2192 GET only (1 call). For UPDATE \u2192 PUT to collecti
488
478
  };
489
479
  }
490
480
  }
491
- const mutationInfo = detectMutationInCode(input.code);
492
- const maxApiCalls = mutationInfo.hasMutation ? 20 : 50;
493
- if (mutationInfo.hasMutation) {
494
- console.error(`[AI Usage] execute: MUTATION DETECTED (${mutationInfo.methods.join(",")}) \u2014 capping API calls to ${maxApiCalls}`);
495
- }
481
+ const maxApiCalls = CODE_MODE_MAX_API_CALLS;
496
482
  let apiCallCount = 0;
497
- const apiRequestFn = createApiRequestFn(ctx, () => {
483
+ let mutationCallCount = 0;
484
+ const apiRequestFn = createApiRequestFn(ctx, (normalizedMethod) => {
498
485
  apiCallCount++;
499
486
  if (apiCallCount > maxApiCalls) {
500
487
  throw new Error(`API call limit exceeded (max ${maxApiCalls})`);
501
488
  }
489
+ if (isUnsafeHttpMethod(normalizedMethod)) {
490
+ mutationCallCount++;
491
+ if (mutationCallCount > CODE_MODE_MAX_MUTATION_CALLS) {
492
+ throw new Error(`Mutation API call limit exceeded (max ${CODE_MODE_MAX_MUTATION_CALLS})`);
493
+ }
494
+ }
502
495
  });
503
496
  const context = {
504
497
  tenantId: ctx.tenantId,
@@ -539,10 +532,10 @@ RULES: For FIND/LIST \u2192 GET only (1 call). For UPDATE \u2192 PUT to collecti
539
532
  function createApiRequestFn(ctx, onCall) {
540
533
  const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || "http://localhost:3000";
541
534
  return async (params) => {
542
- onCall();
543
535
  const { method, path, query, body } = params;
544
536
  const callStart = Date.now();
545
- const normalizedMethod = method.toUpperCase();
537
+ const normalizedMethod = String(method ?? "").toUpperCase();
538
+ onCall(normalizedMethod);
546
539
  const apiPath = normalizeApiRequestPath(path);
547
540
  const authorization = await authorizeCodeModeApiRequest(ctx, normalizedMethod, apiPath);
548
541
  if (!authorization.allowed) {
@@ -709,8 +702,12 @@ function tryParseJson(text) {
709
702
  }
710
703
  }
711
704
  export {
705
+ CODE_MODE_MAX_API_CALLS,
706
+ CODE_MODE_MAX_MUTATION_CALLS,
712
707
  CODE_MODE_REQUIRED_FEATURES,
713
708
  authorizeCodeModeApiRequest,
709
+ createApiRequestFn,
710
+ isUnsafeHttpMethod,
714
711
  loadCodeModeTools,
715
712
  matchApiEndpointPath
716
713
  };