@open-mercato/ai-assistant 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +30 -4
- package/dist/frontend/components/AiChatButton.js +3 -2
- package/dist/frontend/components/AiChatButton.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js +364 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +7 -7
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +182 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +316 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +8 -7
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +43 -20
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +4 -3
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/usage/daily/route.js +111 -0
- package/dist/modules/ai_assistant/api/usage/daily/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +108 -0
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/usage/sessions/route.js +153 -0
- package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +335 -38
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -7
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +44 -35
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js +282 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +12 -0
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +1 -1
- package/dist/modules/ai_assistant/data/entities.js +177 -1
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +104 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js +168 -0
- package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js.map +7 -0
- package/dist/modules/ai_assistant/events.js +8 -0
- package/dist/modules/ai_assistant/events.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +74 -1
- package/dist/modules/ai_assistant/i18n/en.json +74 -1
- package/dist/modules/ai_assistant/i18n/es.json +75 -2
- package/dist/modules/ai_assistant/i18n/pl.json +74 -1
- package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +588 -23
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +3 -3
- package/dist/modules/ai_assistant/lib/agent-tools.js +6 -1
- package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-factory.js +63 -22
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js +78 -0
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +7 -0
- package/dist/modules/ai_assistant/lib/usage-serialization.js +33 -0
- package/dist/modules/ai_assistant/lib/usage-serialization.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js +25 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js +88 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js.map +7 -0
- package/dist/modules/ai_assistant/setup.js +34 -0
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +114 -0
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +7 -0
- package/generated/entities/ai_token_usage_daily/index.ts +16 -0
- package/generated/entities/ai_token_usage_event/index.ts +19 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +47 -1
- package/package.json +15 -7
- package/src/frontend/components/AiChatButton.tsx +3 -2
- package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.ts +521 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +8 -8
- package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +231 -0
- package/src/modules/ai_assistant/__tests__/events.test.ts +4 -3
- package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +5 -5
- package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +109 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +388 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +5 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +8 -7
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +102 -5
- package/src/modules/ai_assistant/api/ai/chat/route.ts +55 -18
- package/src/modules/ai_assistant/api/settings/route.ts +5 -3
- package/src/modules/ai_assistant/api/usage/daily/__tests__/route.test.ts +159 -0
- package/src/modules/ai_assistant/api/usage/daily/route.ts +126 -0
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/__tests__/route.test.ts +143 -0
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +130 -0
- package/src/modules/ai_assistant/api/usage/sessions/__tests__/route.test.ts +123 -0
- package/src/modules/ai_assistant/api/usage/sessions/route.ts +184 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +372 -16
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +1 -4
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +26 -9
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.tsx +469 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +18 -0
- package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +1 -1
- package/src/modules/ai_assistant/data/entities.ts +237 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +135 -3
- package/src/modules/ai_assistant/data/repositories/AiTokenUsageRepository.ts +213 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +223 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiTokenUsageRepository.test.ts +58 -0
- package/src/modules/ai_assistant/events.ts +8 -0
- package/src/modules/ai_assistant/i18n/de.json +74 -1
- package/src/modules/ai_assistant/i18n/en.json +74 -1
- package/src/modules/ai_assistant/i18n/es.json +75 -2
- package/src/modules/ai_assistant/i18n/pl.json +74 -1
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +439 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase1.test.ts +243 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase2.test.ts +388 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase3.test.ts +359 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +2 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +2 -1
- package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +12 -13
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +77 -14
- package/src/modules/ai_assistant/lib/agent-policy.ts +9 -0
- package/src/modules/ai_assistant/lib/agent-runtime.ts +1148 -43
- package/src/modules/ai_assistant/lib/agent-tools.ts +5 -1
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +289 -2
- package/src/modules/ai_assistant/lib/model-factory.ts +128 -43
- package/src/modules/ai_assistant/lib/token-usage-recorder.ts +122 -0
- package/src/modules/ai_assistant/lib/usage-serialization.ts +29 -0
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +791 -0
- package/src/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.ts +25 -0
- package/src/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.ts +89 -0
- package/src/modules/ai_assistant/setup.ts +49 -0
- package/src/modules/ai_assistant/workers/__tests__/ai-token-usage-prune.test.ts +144 -0
- package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +188 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.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-RUNTIME-OVERRIDES-006: Phase 4b \u2014 runtime model overrides (ModelPicker,\n * editable settings form, playground resolution panel).\n *\n * Coverage:\n * - /backend/config/ai-assistant/settings page loads with override form\n * - GlobalOverrideForm: provider + model selects, save, clear\n * - PerAgentOverrideList: table rows, source column, Clear override button\n * - /backend/config/ai-assistant/playground: ModelResolutionPanel renders\n * - ModelPicker renders in the playground's <AiChat> composer when the\n * agent allows runtime model override\n * - ModelPicker is absent when allowRuntimeModelOverride === false\n *\n * All API calls that would hit a real LLM or require a configured provider\n * are intercepted via page.route() stubs.\n */\ntest.describe('TC-AI-RUNTIME-OVERRIDES-006: runtime model overrides', () => {\n const settingsPath = '/backend/config/ai-assistant/settings';\n const playgroundPath = '/backend/config/ai-assistant/playground';\n\n // ---------------------------------------------------------------------------\n // Shared stubs\n // ---------------------------------------------------------------------------\n const settingsPayload = {\n provider: {\n id: 'anthropic',\n name: 'Anthropic',\n model: 'claude-haiku-4-5',\n defaultModel: 'claude-haiku-4-5',\n envKey: 'ANTHROPIC_API_KEY',\n configured: true,\n defaultModels: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },\n { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },\n ],\n },\n availableProviders: [\n {\n id: 'anthropic',\n name: 'Anthropic',\n model: 'claude-haiku-4-5',\n defaultModel: 'claude-haiku-4-5',\n envKey: 'ANTHROPIC_API_KEY',\n configured: true,\n defaultModels: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },\n { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },\n ],\n },\n {\n id: 'openai',\n name: 'OpenAI',\n model: 'gpt-5-mini',\n defaultModel: 'gpt-5-mini',\n envKey: 'OPENAI_API_KEY',\n configured: false,\n defaultModels: [{ id: 'gpt-5-mini', name: 'GPT-5 Mini' }],\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: 'catalog.merchandising_assistant',\n moduleId: 'catalog',\n allowRuntimeModelOverride: true,\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n baseURL: null,\n source: 'provider_default',\n },\n {\n agentId: 'customers.account_assistant',\n moduleId: 'customers',\n allowRuntimeModelOverride: false,\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseURL: null,\n source: 'tenant_override',\n },\n ],\n };\n\n const agentsPayload = {\n agents: [\n {\n id: 'catalog.merchandising_assistant',\n moduleId: 'catalog',\n label: 'Merchandising Assistant',\n description: 'Catalog merchandising tool.',\n systemPrompt: 'You are a merchandising assistant.',\n executionMode: 'chat',\n mutationPolicy: 'confirm-required',\n readOnly: false,\n maxSteps: 10,\n allowedTools: ['catalog.list_products'],\n tools: [{ name: 'catalog.list_products', displayName: 'List products', isMutation: false, registered: true }],\n requiredFeatures: ['catalog.view'],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n ],\n total: 1,\n };\n\n // ---------------------------------------------------------------------------\n // Settings page\n // ---------------------------------------------------------------------------\n test.describe('Settings page (/backend/config/ai-assistant/settings)', () => {\n test('renders override form and per-agent resolution table', 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 // The main settings container should be visible\n const settingsContainer = page.locator('[data-ai-assistant-settings]');\n await expect(settingsContainer).toBeVisible({ timeout: 30_000 });\n\n // Global override form\n const overrideForm = page.locator('[data-ai-settings-override-form]');\n await expect(overrideForm).toBeVisible({ timeout: 15_000 });\n\n // Per-agent override table\n const agentOverridesTable = page.locator('[data-ai-settings-agent-overrides]');\n await expect(agentOverridesTable).toBeVisible({ timeout: 15_000 });\n\n // Both registered agents appear as rows\n const catalogRow = page.locator('[data-ai-settings-agent-row=\"catalog.merchandising_assistant\"]');\n await expect(catalogRow).toBeVisible();\n\n const customersRow = page.locator('[data-ai-settings-agent-row=\"customers.account_assistant\"]');\n await expect(customersRow).toBeVisible();\n });\n\n test('shows Clear override button only for agents with non-default source', 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({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n const agentOverridesTable = page.locator('[data-ai-settings-agent-overrides]');\n await expect(agentOverridesTable).toBeVisible({ timeout: 30_000 });\n\n // customers.account_assistant has source='tenant_override' \u2192 should have Clear button\n const customersClear = page.locator('[data-ai-settings-clear-agent-override=\"customers.account_assistant\"]');\n await expect(customersClear).toBeVisible();\n\n // catalog.merchandising_assistant has source='provider_default' \u2192 no Clear button\n const catalogClear = page.locator('[data-ai-settings-clear-agent-override=\"catalog.merchandising_assistant\"]');\n await expect(catalogClear).not.toBeVisible();\n });\n\n test('save override calls PUT /api/ai_assistant/settings', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n let putCalls = 0;\n await page.route('**/api/ai_assistant/settings', async (route) => {\n if (route.request().method() === 'PUT') {\n putCalls += 1;\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n id: 'row-1',\n tenantId: 'tenant-1',\n organizationId: 'org-1',\n agentId: null,\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseUrl: null,\n updatedAt: new Date().toISOString(),\n }),\n });\n return;\n }\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({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n const overrideForm = page.locator('[data-ai-settings-override-form]');\n await expect(overrideForm).toBeVisible({ timeout: 30_000 });\n\n // Select provider\n const providerSelect = page.locator('[data-ai-settings-provider-select]');\n await providerSelect.click();\n const anthropicOption = page.getByRole('option', { name: 'Anthropic' });\n await anthropicOption.click();\n\n // Select model\n const modelSelect = page.locator('[data-ai-settings-model-select]');\n await modelSelect.click();\n const sonnetOption = page.getByRole('option', { name: 'Claude Sonnet 4.5' });\n await sonnetOption.click();\n\n // Save\n const saveButton = page.locator('[data-ai-settings-save-override]');\n await saveButton.click();\n\n await page.waitForTimeout(500);\n expect(putCalls).toBeGreaterThanOrEqual(1);\n });\n\n test('clear override calls DELETE /api/ai_assistant/settings', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n const settingsWithOverride = {\n ...settingsPayload,\n tenantOverride: {\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseURL: null,\n agentId: null,\n updatedAt: new Date().toISOString(),\n },\n };\n\n let deleteCalls = 0;\n await page.route('**/api/ai_assistant/settings', async (route) => {\n if (route.request().method() === 'DELETE') {\n deleteCalls += 1;\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ cleared: true }),\n });\n return;\n }\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsWithOverride),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n // The \"Clear override\" button for the active override\n const clearButton = page.locator('[data-ai-settings-clear-override]');\n await expect(clearButton).toBeVisible({ timeout: 30_000 });\n await clearButton.click();\n\n await page.waitForTimeout(500);\n expect(deleteCalls).toBeGreaterThanOrEqual(1);\n });\n });\n\n // ---------------------------------------------------------------------------\n // Playground page \u2014 ModelResolutionPanel\n // ---------------------------------------------------------------------------\n test.describe('Playground page (/backend/config/ai-assistant/playground)', () => {\n test('renders ModelResolutionPanel with provider/model/source for the selected agent', async ({\n page,\n }) => {\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/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.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // Wait for agent list to load\n const agentSection = page.locator('[data-ai-playground-agent=\"catalog.merchandising_assistant\"]');\n await expect(agentSection).toBeVisible({ timeout: 30_000 });\n\n // The resolution panel should show provider info\n const resolutionPanel = page.locator('[data-ai-playground-resolution-panel=\"catalog.merchandising_assistant\"]');\n await expect(resolutionPanel).toBeVisible({ timeout: 15_000 });\n\n // Provider field should be present\n const providerField = page.locator('[data-ai-playground-resolution-provider]');\n await expect(providerField).toBeVisible();\n\n // Model field should be present\n const modelField = page.locator('[data-ai-playground-resolution-model]');\n await expect(modelField).toBeVisible();\n\n // Source field should be present\n const sourceField = page.locator('[data-ai-playground-resolution-source]');\n await expect(sourceField).toBeVisible();\n });\n\n test('ModelPicker is present in AiChat composer when allowRuntimeModelOverride is true', async ({\n page,\n }) => {\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/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n // Stub the models endpoint\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.merchandising_assistant',\n allowRuntimeModelOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [\n {\n id: 'anthropic',\n name: 'Anthropic',\n isDefault: true,\n models: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', isDefault: true },\n ],\n },\n ],\n }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // Wait for the chat area to be visible under the selected agent\n const chatContainer = page.locator('[data-ai-playground-chat=\"catalog.merchandising_assistant\"]');\n await expect(chatContainer).toBeVisible({ timeout: 30_000 });\n\n // The ModelPicker trigger should be visible inside the chat container\n const modelPickerTrigger = chatContainer.locator('[data-ai-model-picker-trigger]');\n // Use a soft assertion \u2014 the picker requires the models endpoint to resolve;\n // if the CI environment skips the endpoint, we verify the playground itself loaded.\n const pickerVisible = await modelPickerTrigger.isVisible().catch(() => false);\n if (pickerVisible) {\n await expect(modelPickerTrigger).toBeVisible();\n } else {\n // At minimum the chat area must be visible\n await expect(chatContainer).toBeVisible();\n }\n });\n });\n\n // ---------------------------------------------------------------------------\n // API contract tests (no browser needed)\n // ---------------------------------------------------------------------------\n test.describe('API contract \u2014 GET /api/ai_assistant/settings', () => {\n test('unauthenticated request returns 401 or redirect', async ({ request }) => {\n const response = await request.get('/api/ai_assistant/settings');\n expect([200, 401, 302, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 PUT /api/ai_assistant/settings', () => {\n test('unauthenticated PUT returns 401', async ({ request }) => {\n const response = await request.put('/api/ai_assistant/settings', {\n data: { providerId: 'anthropic', modelId: 'claude-haiku-4-5' },\n headers: { 'content-type': 'application/json' },\n });\n expect([400, 401, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 DELETE /api/ai_assistant/settings', () => {\n test('unauthenticated DELETE returns 401', async ({ request }) => {\n const response = await request.delete('/api/ai_assistant/settings', {\n data: {},\n headers: { 'content-type': 'application/json' },\n });\n expect([400, 401, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 GET /api/ai_assistant/ai/agents/:agentId/models', () => {\n test('route is mounted and returns 401 or JSON payload', async ({ request }) => {\n const response = await request.get('/api/ai_assistant/ai/agents/catalog.merchandising_assistant/models');\n expect([200, 401, 403]).toContain(response.status());\n if (response.status() === 200) {\n const body = await response.json();\n expect(body).toHaveProperty('agentId');\n expect(body).toHaveProperty('allowRuntimeModelOverride');\n expect(body).toHaveProperty('providers');\n }\n });\n });\n});\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,MAAM,cAAc;AAC7B,SAAS,aAAa;AAkBtB,KAAK,SAAS,wDAAwD,MAAM;AAC1E,QAAM,eAAe;AACrB,QAAM,iBAAiB;AAKvB,QAAM,kBAAkB;AAAA,IACtB,UAAU;AAAA,MACR,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,OAAO;AAAA,MACP,cAAc;AAAA,MACd,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,eAAe;AAAA,QACb,EAAE,IAAI,oBAAoB,MAAM,mBAAmB;AAAA,QACnD,EAAE,IAAI,qBAAqB,MAAM,oBAAoB;AAAA,MACvD;AAAA,IACF;AAAA,IACA,oBAAoB;AAAA,MAClB;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,OAAO;AAAA,QACP,cAAc;AAAA,QACd,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,eAAe;AAAA,UACb,EAAE,IAAI,oBAAoB,MAAM,mBAAmB;AAAA,UACnD,EAAE,IAAI,qBAAqB,MAAM,oBAAoB;AAAA,QACvD;AAAA,MACF;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,OAAO;AAAA,QACP,cAAc;AAAA,QACd,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,eAAe,CAAC,EAAE,IAAI,cAAc,MAAM,aAAa,CAAC;AAAA,MAC1D;AAAA,IACF;AAAA,IACA,kBAAkB;AAAA,IAClB,iBAAiB;AAAA,MACf,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,SAAS;AAAA,MACT,QAAQ;AAAA,IACV;AAAA,IACA,gBAAgB;AAAA,IAChB,QAAQ;AAAA,MACN;AAAA,QACE,SAAS;AAAA,QACT,UAAU;AAAA,QACV,
|
|
4
|
+
"sourcesContent": ["import { test, expect } from '@playwright/test';\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';\n\n/**\n * TC-AI-RUNTIME-OVERRIDES-006: Phase 4b \u2014 runtime model overrides (ModelPicker,\n * editable settings form, playground resolution panel).\n *\n * Coverage:\n * - /backend/config/ai-assistant/settings page loads with override form\n * - GlobalOverrideForm: provider + model selects, save, clear\n * - PerAgentOverrideList: table rows, source column, Clear override button\n * - /backend/config/ai-assistant/playground: ModelResolutionPanel renders\n * - ModelPicker renders in the playground's <AiChat> composer when the\n * agent allows runtime model override\n * - ModelPicker is absent when allowRuntimeOverride === false\n *\n * All API calls that would hit a real LLM or require a configured provider\n * are intercepted via page.route() stubs.\n */\ntest.describe('TC-AI-RUNTIME-OVERRIDES-006: runtime model overrides', () => {\n const settingsPath = '/backend/config/ai-assistant/settings';\n const playgroundPath = '/backend/config/ai-assistant/playground';\n\n // ---------------------------------------------------------------------------\n // Shared stubs\n // ---------------------------------------------------------------------------\n const settingsPayload = {\n provider: {\n id: 'anthropic',\n name: 'Anthropic',\n model: 'claude-haiku-4-5',\n defaultModel: 'claude-haiku-4-5',\n envKey: 'ANTHROPIC_API_KEY',\n configured: true,\n defaultModels: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },\n { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },\n ],\n },\n availableProviders: [\n {\n id: 'anthropic',\n name: 'Anthropic',\n model: 'claude-haiku-4-5',\n defaultModel: 'claude-haiku-4-5',\n envKey: 'ANTHROPIC_API_KEY',\n configured: true,\n defaultModels: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },\n { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },\n ],\n },\n {\n id: 'openai',\n name: 'OpenAI',\n model: 'gpt-5-mini',\n defaultModel: 'gpt-5-mini',\n envKey: 'OPENAI_API_KEY',\n configured: false,\n defaultModels: [{ id: 'gpt-5-mini', name: 'GPT-5 Mini' }],\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: 'catalog.merchandising_assistant',\n moduleId: 'catalog',\n allowRuntimeOverride: true,\n providerId: 'anthropic',\n modelId: 'claude-haiku-4-5',\n baseURL: null,\n source: 'provider_default',\n },\n {\n agentId: 'customers.account_assistant',\n moduleId: 'customers',\n allowRuntimeOverride: false,\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseURL: null,\n source: 'tenant_override',\n },\n ],\n };\n\n const agentsPayload = {\n agents: [\n {\n id: 'catalog.merchandising_assistant',\n moduleId: 'catalog',\n label: 'Merchandising Assistant',\n description: 'Catalog merchandising tool.',\n systemPrompt: 'You are a merchandising assistant.',\n executionMode: 'chat',\n mutationPolicy: 'confirm-required',\n readOnly: false,\n maxSteps: 10,\n allowedTools: ['catalog.list_products'],\n tools: [{ name: 'catalog.list_products', displayName: 'List products', isMutation: false, registered: true }],\n requiredFeatures: ['catalog.view'],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n ],\n total: 1,\n };\n\n // ---------------------------------------------------------------------------\n // Settings page\n // ---------------------------------------------------------------------------\n test.describe('Settings page (/backend/config/ai-assistant/settings)', () => {\n test('renders override form and per-agent resolution table', 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 // The main settings container should be visible\n const settingsContainer = page.locator('[data-ai-assistant-settings]');\n await expect(settingsContainer).toBeVisible({ timeout: 30_000 });\n\n // Global override form\n const overrideForm = page.locator('[data-ai-settings-override-form]');\n await expect(overrideForm).toBeVisible({ timeout: 15_000 });\n\n // Per-agent override table\n const agentOverridesTable = page.locator('[data-ai-settings-agent-overrides]');\n await expect(agentOverridesTable).toBeVisible({ timeout: 15_000 });\n\n // Both registered agents appear as rows\n const catalogRow = page.locator('[data-ai-settings-agent-row=\"catalog.merchandising_assistant\"]');\n await expect(catalogRow).toBeVisible();\n\n const customersRow = page.locator('[data-ai-settings-agent-row=\"customers.account_assistant\"]');\n await expect(customersRow).toBeVisible();\n });\n\n test('shows Clear override button only for agents with non-default source', 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({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n const agentOverridesTable = page.locator('[data-ai-settings-agent-overrides]');\n await expect(agentOverridesTable).toBeVisible({ timeout: 30_000 });\n\n // customers.account_assistant has source='tenant_override' \u2192 should have Clear button\n const customersClear = page.locator('[data-ai-settings-clear-agent-override=\"customers.account_assistant\"]');\n await expect(customersClear).toBeVisible();\n\n // catalog.merchandising_assistant has source='provider_default' \u2192 no Clear button\n const catalogClear = page.locator('[data-ai-settings-clear-agent-override=\"catalog.merchandising_assistant\"]');\n await expect(catalogClear).not.toBeVisible();\n });\n\n test('save override calls PUT /api/ai_assistant/settings', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n let putCalls = 0;\n await page.route('**/api/ai_assistant/settings', async (route) => {\n if (route.request().method() === 'PUT') {\n putCalls += 1;\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n id: 'row-1',\n tenantId: 'tenant-1',\n organizationId: 'org-1',\n agentId: null,\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseUrl: null,\n updatedAt: new Date().toISOString(),\n }),\n });\n return;\n }\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({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n const overrideForm = page.locator('[data-ai-settings-override-form]');\n await expect(overrideForm).toBeVisible({ timeout: 30_000 });\n\n // Select provider\n const providerSelect = page.locator('[data-ai-settings-provider-select]');\n await providerSelect.click();\n const anthropicOption = page.getByRole('option', { name: 'Anthropic' });\n await anthropicOption.click();\n\n // Select model\n const modelSelect = page.locator('[data-ai-settings-model-select]');\n await modelSelect.click();\n const sonnetOption = page.getByRole('option', { name: 'Claude Sonnet 4.5' });\n await sonnetOption.click();\n\n // Save\n const saveButton = page.locator('[data-ai-settings-save-override]');\n await saveButton.click();\n\n await page.waitForTimeout(500);\n expect(putCalls).toBeGreaterThanOrEqual(1);\n });\n\n test('clear override calls DELETE /api/ai_assistant/settings', async ({ page }) => {\n test.setTimeout(120_000);\n await login(page, 'superadmin');\n\n const settingsWithOverride = {\n ...settingsPayload,\n tenantOverride: {\n providerId: 'anthropic',\n modelId: 'claude-sonnet-4-5',\n baseURL: null,\n agentId: null,\n updatedAt: new Date().toISOString(),\n },\n };\n\n let deleteCalls = 0;\n await page.route('**/api/ai_assistant/settings', async (route) => {\n if (route.request().method() === 'DELETE') {\n deleteCalls += 1;\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ cleared: true }),\n });\n return;\n }\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsWithOverride),\n });\n });\n\n await page.route('**/api/ai_assistant/health', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });\n });\n\n await page.route('**/api/ai_assistant/tools', async (route) => {\n await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });\n });\n\n await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });\n\n // The \"Clear override\" button for the active override\n const clearButton = page.locator('[data-ai-settings-clear-override]');\n await expect(clearButton).toBeVisible({ timeout: 30_000 });\n await clearButton.click();\n\n await page.waitForTimeout(500);\n expect(deleteCalls).toBeGreaterThanOrEqual(1);\n });\n });\n\n // ---------------------------------------------------------------------------\n // Playground page \u2014 ModelResolutionPanel\n // ---------------------------------------------------------------------------\n test.describe('Playground page (/backend/config/ai-assistant/playground)', () => {\n test('renders ModelResolutionPanel with provider/model/source for the selected agent', async ({\n page,\n }) => {\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/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.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // Wait for agent list to load\n const agentSection = page.locator('[data-ai-playground-chat=\"catalog.merchandising_assistant\"]');\n await expect(agentSection).toBeVisible({ timeout: 30_000 });\n\n // The resolution panel should show provider info\n const resolutionPanel = page.locator('[data-ai-playground-model-resolution=\"catalog.merchandising_assistant\"]');\n await expect(resolutionPanel).toBeVisible({ timeout: 15_000 });\n\n // Provider field should be present\n const providerField = page.locator('[data-ai-playground-resolution-provider]');\n await expect(providerField).toBeVisible();\n\n // Model field should be present\n const modelField = page.locator('[data-ai-playground-resolution-model]');\n await expect(modelField).toBeVisible();\n\n // Source field should be present\n const sourceField = page.locator('[data-ai-playground-resolution-source]');\n await expect(sourceField).toBeVisible();\n });\n\n test('ModelPicker is present in AiChat composer when allowRuntimeOverride is true', async ({\n page,\n }) => {\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/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(settingsPayload),\n });\n });\n\n // Stub the models endpoint\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.merchandising_assistant',\n allowRuntimeOverride: true,\n defaultProviderId: 'anthropic',\n defaultModelId: 'claude-haiku-4-5',\n providers: [\n {\n id: 'anthropic',\n name: 'Anthropic',\n isDefault: true,\n models: [\n { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', isDefault: true },\n ],\n },\n ],\n }),\n });\n });\n\n await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });\n\n // Wait for the chat area to be visible under the selected agent\n const chatContainer = page.locator('[data-ai-playground-chat=\"catalog.merchandising_assistant\"]');\n await expect(chatContainer).toBeVisible({ timeout: 30_000 });\n\n // The ModelPicker trigger should be visible inside the chat container\n const modelPickerTrigger = chatContainer.locator('[data-ai-model-picker-trigger]');\n // Use a soft assertion \u2014 the picker requires the models endpoint to resolve;\n // if the CI environment skips the endpoint, we verify the playground itself loaded.\n const pickerVisible = await modelPickerTrigger.isVisible().catch(() => false);\n if (pickerVisible) {\n await expect(modelPickerTrigger).toBeVisible();\n } else {\n // At minimum the chat area must be visible\n await expect(chatContainer).toBeVisible();\n }\n });\n });\n\n // ---------------------------------------------------------------------------\n // API contract tests (no browser needed)\n // ---------------------------------------------------------------------------\n test.describe('API contract \u2014 GET /api/ai_assistant/settings', () => {\n test('unauthenticated request returns 401 or redirect', async ({ request }) => {\n const response = await request.get('/api/ai_assistant/settings');\n expect([200, 401, 302, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 PUT /api/ai_assistant/settings', () => {\n test('unauthenticated PUT returns 401', async ({ request }) => {\n const response = await request.put('/api/ai_assistant/settings', {\n data: { providerId: 'anthropic', modelId: 'claude-haiku-4-5' },\n headers: { 'content-type': 'application/json' },\n });\n expect([400, 401, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 DELETE /api/ai_assistant/settings', () => {\n test('unauthenticated DELETE returns 401', async ({ request }) => {\n const response = await request.delete('/api/ai_assistant/settings', {\n data: {},\n headers: { 'content-type': 'application/json' },\n });\n expect([400, 401, 403]).toContain(response.status());\n });\n });\n\n test.describe('API contract \u2014 GET /api/ai_assistant/ai/agents/:agentId/models', () => {\n test('route is mounted and returns 401 or JSON payload', async ({ request }) => {\n const response = await request.get('/api/ai_assistant/ai/agents/catalog.merchandising_assistant/models');\n expect([200, 401, 403]).toContain(response.status());\n if (response.status() === 200) {\n const body = await response.json();\n expect(body).toHaveProperty('agentId');\n expect(body).toHaveProperty('allowRuntimeOverride');\n expect(body).toHaveProperty('providers');\n }\n });\n });\n});\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,MAAM,cAAc;AAC7B,SAAS,aAAa;AAkBtB,KAAK,SAAS,wDAAwD,MAAM;AAC1E,QAAM,eAAe;AACrB,QAAM,iBAAiB;AAKvB,QAAM,kBAAkB;AAAA,IACtB,UAAU;AAAA,MACR,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,OAAO;AAAA,MACP,cAAc;AAAA,MACd,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,eAAe;AAAA,QACb,EAAE,IAAI,oBAAoB,MAAM,mBAAmB;AAAA,QACnD,EAAE,IAAI,qBAAqB,MAAM,oBAAoB;AAAA,MACvD;AAAA,IACF;AAAA,IACA,oBAAoB;AAAA,MAClB;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,OAAO;AAAA,QACP,cAAc;AAAA,QACd,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,eAAe;AAAA,UACb,EAAE,IAAI,oBAAoB,MAAM,mBAAmB;AAAA,UACnD,EAAE,IAAI,qBAAqB,MAAM,oBAAoB;AAAA,QACvD;AAAA,MACF;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,OAAO;AAAA,QACP,cAAc;AAAA,QACd,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,eAAe,CAAC,EAAE,IAAI,cAAc,MAAM,aAAa,CAAC;AAAA,MAC1D;AAAA,IACF;AAAA,IACA,kBAAkB;AAAA,IAClB,iBAAiB;AAAA,MACf,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,SAAS;AAAA,MACT,QAAQ;AAAA,IACV;AAAA,IACA,gBAAgB;AAAA,IAChB,QAAQ;AAAA,MACN;AAAA,QACE,SAAS;AAAA,QACT,UAAU;AAAA,QACV,sBAAsB;AAAA,QACtB,YAAY;AAAA,QACZ,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,MACV;AAAA,MACA;AAAA,QACE,SAAS;AAAA,QACT,UAAU;AAAA,QACV,sBAAsB;AAAA,QACtB,YAAY;AAAA,QACZ,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAEA,QAAM,gBAAgB;AAAA,IACpB,QAAQ;AAAA,MACN;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,OAAO;AAAA,QACP,aAAa;AAAA,QACb,cAAc;AAAA,QACd,eAAe;AAAA,QACf,gBAAgB;AAAA,QAChB,UAAU;AAAA,QACV,UAAU;AAAA,QACV,cAAc,CAAC,uBAAuB;AAAA,QACtC,OAAO,CAAC,EAAE,MAAM,yBAAyB,aAAa,iBAAiB,YAAY,OAAO,YAAY,KAAK,CAAC;AAAA,QAC5G,kBAAkB,CAAC,cAAc;AAAA,QACjC,oBAAoB,CAAC;AAAA,QACrB,iBAAiB;AAAA,MACnB;AAAA,IACF;AAAA,IACA,OAAO;AAAA,EACT;AAKA,OAAK,SAAS,yDAAyD,MAAM;AAC3E,SAAK,wDAAwD,OAAO,EAAE,KAAK,MAAM;AAC/E,WAAK,WAAW,IAAO;AACvB,YAAM,MAAM,MAAM,YAAY;AAE9B,YAAM,KAAK,MAAM,gCAAgC,OAAO,UAAU;AAChE,cAAM,MAAM,QAAQ;AAAA,UAClB,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,MAAM,KAAK,UAAU,eAAe;AAAA,QACtC,CAAC;AAAA,MACH,CAAC;AAED,YAAM,KAAK,MAAM,8BAA8B,OAAO,UAAU;AAC9D,cAAM,MAAM,QAAQ;AAAA,UAClB,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,MAAM,KAAK,UAAU,EAAE,QAAQ,MAAM,KAAK,oBAAoB,QAAQ,wBAAwB,CAAC;AAAA,QACjG,CAAC;AAAA,MACH,CAAC;AAED,YAAM,KAAK,MAAM,6BAA6B,OAAO,UAAU;AAC7D,cAAM,MAAM,QAAQ;AAAA,UAClB,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC;AAAA,QACpC,CAAC;AAAA,MACH,CAAC;AAED,YAAM,KAAK,KAAK,cAAc,EAAE,WAAW,mBAAmB,CAAC;AAG/D,YAAM,oBAAoB,KAAK,QAAQ,8BAA8B;AACrE,YAAM,OAAO,iBAAiB,EAAE,YAAY,EAAE,SAAS,IAAO,CAAC;AAG/D,YAAM,eAAe,KAAK,QAAQ,kCAAkC;AACpE,YAAM,OAAO,YAAY,EAAE,YAAY,EAAE,SAAS,KAAO,CAAC;AAG1D,YAAM,sBAAsB,KAAK,QAAQ,oCAAoC;AAC7E,YAAM,OAAO,mBAAmB,EAAE,YAAY,EAAE,SAAS,KAAO,CAAC;AAGjE,YAAM,aAAa,KAAK,QAAQ,gEAAgE;AAChG,YAAM,OAAO,UAAU,EAAE,YAAY;AAErC,YAAM,eAAe,KAAK,QAAQ,4DAA4D;AAC9F,YAAM,OAAO,YAAY,EAAE,YAAY;AAAA,IACzC,CAAC;AAED,SAAK,uEAAuE,OAAO,EAAE,KAAK,MAAM;AAC9F,WAAK,WAAW,IAAO;AACvB,YAAM,MAAM,MAAM,YAAY;AAE9B,YAAM,KAAK,MAAM,gCAAgC,OAAO,UAAU;AAChE,cAAM,MAAM,QAAQ;AAAA,UAClB,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,MAAM,KAAK,UAAU,eAAe;AAAA,QACtC,CAAC;AAAA,MACH,CAAC;AAED,YAAM,KAAK,MAAM,8BAA8B,OAAO,UAAU;AAC9D,cAAM,MAAM,QAAQ,EAAE,QAAQ,KAAK,aAAa,oBAAoB,MAAM,KAAK,UAAU,EAAE,QAAQ,MAAM,KAAK,oBAAoB,QAAQ,wBAAwB,CAAC,EAAE,CAAC;AAAA,MACxK,CAAC;AAED,YAAM,KAAK,MAAM,6BAA6B,OAAO,UAAU;AAC7D,cAAM,MAAM,QAAQ,EAAE,QAAQ,KAAK,aAAa,oBAAoB,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;AAAA,MAC3G,CAAC;AAED,YAAM,KAAK,KAAK,cAAc,EAAE,WAAW,mBAAmB,CAAC;AAE/D,YAAM,sBAAsB,KAAK,QAAQ,oCAAoC;AAC7E,YAAM,OAAO,mBAAmB,EAAE,YAAY,EAAE,SAAS,IAAO,CAAC;AAGjE,YAAM,iBAAiB,KAAK,QAAQ,uEAAuE;AAC3G,YAAM,OAAO,cAAc,EAAE,YAAY;AAGzC,YAAM,eAAe,KAAK,QAAQ,2EAA2E;AAC7G,YAAM,OAAO,YAAY,EAAE,IAAI,YAAY;AAAA,IAC7C,CAAC;AAED,SAAK,sDAAsD,OAAO,EAAE,KAAK,MAAM;AAC7E,WAAK,WAAW,IAAO;AACvB,YAAM,MAAM,MAAM,YAAY;AAE9B,UAAI,WAAW;AACf,YAAM,KAAK,MAAM,gCAAgC,OAAO,UAAU;AAChE,YAAI,MAAM,QAAQ,EAAE,OAAO,MAAM,OAAO;AACtC,sBAAY;AACZ,gBAAM,MAAM,QAAQ;AAAA,YAClB,QAAQ;AAAA,YACR,aAAa;AAAA,YACb,MAAM,KAAK,UAAU;AAAA,cACnB,IAAI;AAAA,cACJ,UAAU;AAAA,cACV,gBAAgB;AAAA,cAChB,SAAS;AAAA,cACT,YAAY;AAAA,cACZ,SAAS;AAAA,cACT,SAAS;AAAA,cACT,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,YACpC,CAAC;AAAA,UACH,CAAC;AACD;AAAA,QACF;AACA,cAAM,MAAM,QAAQ;AAAA,UAClB,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,MAAM,KAAK,UAAU,eAAe;AAAA,QACtC,CAAC;AAAA,MACH,CAAC;AAED,YAAM,KAAK,MAAM,8BAA8B,OAAO,UAAU;AAC9D,cAAM,MAAM,QAAQ,EAAE,QAAQ,KAAK,aAAa,oBAAoB,MAAM,KAAK,UAAU,EAAE,QAAQ,MAAM,KAAK,oBAAoB,QAAQ,wBAAwB,CAAC,EAAE,CAAC;AAAA,MACxK,CAAC;AAED,YAAM,KAAK,MAAM,6BAA6B,OAAO,UAAU;AAC7D,cAAM,MAAM,QAAQ,EAAE,QAAQ,KAAK,aAAa,oBAAoB,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;AAAA,MAC3G,CAAC;AAED,YAAM,KAAK,KAAK,cAAc,EAAE,WAAW,mBAAmB,CAAC;AAE/D,YAAM,eAAe,KAAK,QAAQ,kCAAkC;AACpE,YAAM,OAAO,YAAY,EAAE,YAAY,EAAE,SAAS,IAAO,CAAC;AAG1D,YAAM,iBAAiB,KAAK,QAAQ,oCAAoC;AACxE,YAAM,eAAe,MAAM;AAC3B,YAAM,kBAAkB,KAAK,UAAU,UAAU,EAAE,MAAM,YAAY,CAAC;AACtE,YAAM,gBAAgB,MAAM;AAG5B,YAAM,cAAc,KAAK,QAAQ,iCAAiC;AAClE,YAAM,YAAY,MAAM;AACxB,YAAM,eAAe,KAAK,UAAU,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC3E,YAAM,aAAa,MAAM;AAGzB,YAAM,aAAa,KAAK,QAAQ,kCAAkC;AAClE,YAAM,WAAW,MAAM;AAEvB,YAAM,KAAK,eAAe,GAAG;AAC7B,aAAO,QAAQ,EAAE,uBAAuB,CAAC;AAAA,IAC3C,CAAC;AAED,SAAK,0DAA0D,OAAO,EAAE,KAAK,MAAM;AACjF,WAAK,WAAW,IAAO;AACvB,YAAM,MAAM,MAAM,YAAY;AAE9B,YAAM,uBAAuB;AAAA,QAC3B,GAAG;AAAA,QACH,gBAAgB;AAAA,UACd,YAAY;AAAA,UACZ,SAAS;AAAA,UACT,SAAS;AAAA,UACT,SAAS;AAAA,UACT,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC;AAAA,MACF;AAEA,UAAI,cAAc;AAClB,YAAM,KAAK,MAAM,gCAAgC,OAAO,UAAU;AAChE,YAAI,MAAM,QAAQ,EAAE,OAAO,MAAM,UAAU;AACzC,yBAAe;AACf,gBAAM,MAAM,QAAQ;AAAA,YAClB,QAAQ;AAAA,YACR,aAAa;AAAA,YACb,MAAM,KAAK,UAAU,EAAE,SAAS,KAAK,CAAC;AAAA,UACxC,CAAC;AACD;AAAA,QACF;AACA,cAAM,MAAM,QAAQ;AAAA,UAClB,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,MAAM,KAAK,UAAU,oBAAoB;AAAA,QAC3C,CAAC;AAAA,MACH,CAAC;AAED,YAAM,KAAK,MAAM,8BAA8B,OAAO,UAAU;AAC9D,cAAM,MAAM,QAAQ,EAAE,QAAQ,KAAK,aAAa,oBAAoB,MAAM,KAAK,UAAU,EAAE,QAAQ,MAAM,KAAK,oBAAoB,QAAQ,wBAAwB,CAAC,EAAE,CAAC;AAAA,MACxK,CAAC;AAED,YAAM,KAAK,MAAM,6BAA6B,OAAO,UAAU;AAC7D,cAAM,MAAM,QAAQ,EAAE,QAAQ,KAAK,aAAa,oBAAoB,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;AAAA,MAC3G,CAAC;AAED,YAAM,KAAK,KAAK,cAAc,EAAE,WAAW,mBAAmB,CAAC;AAG/D,YAAM,cAAc,KAAK,QAAQ,mCAAmC;AACpE,YAAM,OAAO,WAAW,EAAE,YAAY,EAAE,SAAS,IAAO,CAAC;AACzD,YAAM,YAAY,MAAM;AAExB,YAAM,KAAK,eAAe,GAAG;AAC7B,aAAO,WAAW,EAAE,uBAAuB,CAAC;AAAA,IAC9C,CAAC;AAAA,EACH,CAAC;AAKD,OAAK,SAAS,6DAA6D,MAAM;AAC/E,SAAK,kFAAkF,OAAO;AAAA,MAC5F;AAAA,IACF,MAAM;AACJ,WAAK,WAAW,IAAO;AACvB,YAAM,MAAM,MAAM,YAAY;AAE9B,YAAM,KAAK,MAAM,iCAAiC,OAAO,UAAU;AACjE,cAAM,MAAM,QAAQ;AAAA,UAClB,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,MAAM,KAAK,UAAU,aAAa;AAAA,QACpC,CAAC;AAAA,MACH,CAAC;AAED,YAAM,KAAK,MAAM,gCAAgC,OAAO,UAAU;AAChE,cAAM,MAAM,QAAQ;AAAA,UAClB,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,MAAM,KAAK,UAAU,eAAe;AAAA,QACtC,CAAC;AAAA,MACH,CAAC;AAED,YAAM,KAAK,KAAK,gBAAgB,EAAE,WAAW,mBAAmB,CAAC;AAGjE,YAAM,eAAe,KAAK,QAAQ,6DAA6D;AAC/F,YAAM,OAAO,YAAY,EAAE,YAAY,EAAE,SAAS,IAAO,CAAC;AAG1D,YAAM,kBAAkB,KAAK,QAAQ,yEAAyE;AAC9G,YAAM,OAAO,eAAe,EAAE,YAAY,EAAE,SAAS,KAAO,CAAC;AAG7D,YAAM,gBAAgB,KAAK,QAAQ,0CAA0C;AAC7E,YAAM,OAAO,aAAa,EAAE,YAAY;AAGxC,YAAM,aAAa,KAAK,QAAQ,uCAAuC;AACvE,YAAM,OAAO,UAAU,EAAE,YAAY;AAGrC,YAAM,cAAc,KAAK,QAAQ,wCAAwC;AACzE,YAAM,OAAO,WAAW,EAAE,YAAY;AAAA,IACxC,CAAC;AAED,SAAK,+EAA+E,OAAO;AAAA,MACzF;AAAA,IACF,MAAM;AACJ,WAAK,WAAW,IAAO;AACvB,YAAM,MAAM,MAAM,YAAY;AAE9B,YAAM,KAAK,MAAM,iCAAiC,OAAO,UAAU;AACjE,cAAM,MAAM,QAAQ;AAAA,UAClB,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,MAAM,KAAK,UAAU,aAAa;AAAA,QACpC,CAAC;AAAA,MACH,CAAC;AAED,YAAM,KAAK,MAAM,gCAAgC,OAAO,UAAU;AAChE,cAAM,MAAM,QAAQ;AAAA,UAClB,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,MAAM,KAAK,UAAU,eAAe;AAAA,QACtC,CAAC;AAAA,MACH,CAAC;AAGD,YAAM,KAAK,MAAM,0CAA0C,OAAO,UAAU;AAC1E,cAAM,MAAM,QAAQ;AAAA,UAClB,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,MAAM,KAAK,UAAU;AAAA,YACnB,SAAS;AAAA,YACT,sBAAsB;AAAA,YACtB,mBAAmB;AAAA,YACnB,gBAAgB;AAAA,YAChB,WAAW;AAAA,cACT;AAAA,gBACE,IAAI;AAAA,gBACJ,MAAM;AAAA,gBACN,WAAW;AAAA,gBACX,QAAQ;AAAA,kBACN,EAAE,IAAI,oBAAoB,MAAM,oBAAoB,WAAW,KAAK;AAAA,gBACtE;AAAA,cACF;AAAA,YACF;AAAA,UACF,CAAC;AAAA,QACH,CAAC;AAAA,MACH,CAAC;AAED,YAAM,KAAK,KAAK,gBAAgB,EAAE,WAAW,mBAAmB,CAAC;AAGjE,YAAM,gBAAgB,KAAK,QAAQ,6DAA6D;AAChG,YAAM,OAAO,aAAa,EAAE,YAAY,EAAE,SAAS,IAAO,CAAC;AAG3D,YAAM,qBAAqB,cAAc,QAAQ,gCAAgC;AAGjF,YAAM,gBAAgB,MAAM,mBAAmB,UAAU,EAAE,MAAM,MAAM,KAAK;AAC5E,UAAI,eAAe;AACjB,cAAM,OAAO,kBAAkB,EAAE,YAAY;AAAA,MAC/C,OAAO;AAEL,cAAM,OAAO,aAAa,EAAE,YAAY;AAAA,MAC1C;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAKD,OAAK,SAAS,sDAAiD,MAAM;AACnE,SAAK,mDAAmD,OAAO,EAAE,QAAQ,MAAM;AAC7E,YAAM,WAAW,MAAM,QAAQ,IAAI,4BAA4B;AAC/D,aAAO,CAAC,KAAK,KAAK,KAAK,GAAG,CAAC,EAAE,UAAU,SAAS,OAAO,CAAC;AAAA,IAC1D,CAAC;AAAA,EACH,CAAC;AAED,OAAK,SAAS,sDAAiD,MAAM;AACnE,SAAK,mCAAmC,OAAO,EAAE,QAAQ,MAAM;AAC7D,YAAM,WAAW,MAAM,QAAQ,IAAI,8BAA8B;AAAA,QAC/D,MAAM,EAAE,YAAY,aAAa,SAAS,mBAAmB;AAAA,QAC7D,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAChD,CAAC;AACD,aAAO,CAAC,KAAK,KAAK,GAAG,CAAC,EAAE,UAAU,SAAS,OAAO,CAAC;AAAA,IACrD,CAAC;AAAA,EACH,CAAC;AAED,OAAK,SAAS,yDAAoD,MAAM;AACtE,SAAK,sCAAsC,OAAO,EAAE,QAAQ,MAAM;AAChE,YAAM,WAAW,MAAM,QAAQ,OAAO,8BAA8B;AAAA,QAClE,MAAM,CAAC;AAAA,QACP,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAChD,CAAC;AACD,aAAO,CAAC,KAAK,KAAK,GAAG,CAAC,EAAE,UAAU,SAAS,OAAO,CAAC;AAAA,IACrD,CAAC;AAAA,EACH,CAAC;AAED,OAAK,SAAS,uEAAkE,MAAM;AACpF,SAAK,oDAAoD,OAAO,EAAE,QAAQ,MAAM;AAC9E,YAAM,WAAW,MAAM,QAAQ,IAAI,oEAAoE;AACvG,aAAO,CAAC,KAAK,KAAK,GAAG,CAAC,EAAE,UAAU,SAAS,OAAO,CAAC;AACnD,UAAI,SAAS,OAAO,MAAM,KAAK;AAC7B,cAAM,OAAO,MAAM,SAAS,KAAK;AACjC,eAAO,IAAI,EAAE,eAAe,SAAS;AACrC,eAAO,IAAI,EAAE,eAAe,sBAAsB;AAClD,eAAO,IAAI,EAAE,eAAe,WAAW;AAAA,MACzC;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
|
+
import { login } from "@open-mercato/core/modules/core/__integration__/helpers/auth";
|
|
3
|
+
const USAGE_PAGE = "/backend/config/ai-assistant/usage";
|
|
4
|
+
const EMPTY_DAILY_PAYLOAD = { rows: [], total: 0 };
|
|
5
|
+
const EMPTY_SESSIONS_PAYLOAD = { sessions: [], total: 0, limit: 50, offset: 0 };
|
|
6
|
+
const DAILY_ROW = {
|
|
7
|
+
id: "row-1",
|
|
8
|
+
tenantId: "tenant-1",
|
|
9
|
+
organizationId: null,
|
|
10
|
+
day: "2026-05-01",
|
|
11
|
+
agentId: "catalog.assistant",
|
|
12
|
+
modelId: "claude-haiku-4-5",
|
|
13
|
+
providerId: "anthropic",
|
|
14
|
+
inputTokens: "1000",
|
|
15
|
+
outputTokens: "500",
|
|
16
|
+
cachedInputTokens: "0",
|
|
17
|
+
reasoningTokens: "0",
|
|
18
|
+
stepCount: "5",
|
|
19
|
+
turnCount: "3",
|
|
20
|
+
sessionCount: "2",
|
|
21
|
+
createdAt: "2026-05-01T12:00:00.000Z",
|
|
22
|
+
updatedAt: "2026-05-01T12:00:00.000Z"
|
|
23
|
+
};
|
|
24
|
+
const SESSION_ROW = {
|
|
25
|
+
sessionId: "00000000-0000-0000-0000-000000000001",
|
|
26
|
+
agentId: "catalog.assistant",
|
|
27
|
+
moduleId: "catalog",
|
|
28
|
+
userId: "user-1",
|
|
29
|
+
startedAt: "2026-05-01T10:00:00.000Z",
|
|
30
|
+
lastEventAt: "2026-05-01T10:05:00.000Z",
|
|
31
|
+
stepCount: 5,
|
|
32
|
+
turnCount: 3,
|
|
33
|
+
inputTokens: 1e3,
|
|
34
|
+
outputTokens: 500,
|
|
35
|
+
cachedInputTokens: 0,
|
|
36
|
+
reasoningTokens: 0
|
|
37
|
+
};
|
|
38
|
+
const STEP_EVENT = {
|
|
39
|
+
id: "evt-1",
|
|
40
|
+
tenantId: "tenant-1",
|
|
41
|
+
organizationId: null,
|
|
42
|
+
userId: "user-1",
|
|
43
|
+
agentId: "catalog.assistant",
|
|
44
|
+
moduleId: "catalog",
|
|
45
|
+
sessionId: "00000000-0000-0000-0000-000000000001",
|
|
46
|
+
turnId: "00000000-0000-0000-0000-000000000002",
|
|
47
|
+
stepIndex: 0,
|
|
48
|
+
providerId: "anthropic",
|
|
49
|
+
modelId: "claude-haiku-4-5",
|
|
50
|
+
inputTokens: 1e3,
|
|
51
|
+
outputTokens: 500,
|
|
52
|
+
cachedInputTokens: null,
|
|
53
|
+
reasoningTokens: null,
|
|
54
|
+
finishReason: "stop",
|
|
55
|
+
loopAbortReason: null,
|
|
56
|
+
createdAt: "2026-05-01T10:00:00.000Z",
|
|
57
|
+
updatedAt: "2026-05-01T10:00:00.000Z"
|
|
58
|
+
};
|
|
59
|
+
test.describe("TC-AI-TOKEN-USAGE-001\u2013005: token usage stats page", () => {
|
|
60
|
+
test("TC-AI-TOKEN-USAGE-001: usage page renders summary tiles for superadmin", async ({ page }) => {
|
|
61
|
+
await login(page, "superadmin");
|
|
62
|
+
await page.route("**/api/ai_assistant/usage/daily**", async (route) => {
|
|
63
|
+
await route.fulfill({
|
|
64
|
+
status: 200,
|
|
65
|
+
contentType: "application/json",
|
|
66
|
+
body: JSON.stringify({ rows: [DAILY_ROW], total: 1 })
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
await page.route("**/api/ai_assistant/usage/sessions**", async (route) => {
|
|
70
|
+
await route.fulfill({
|
|
71
|
+
status: 200,
|
|
72
|
+
contentType: "application/json",
|
|
73
|
+
body: JSON.stringify(EMPTY_SESSIONS_PAYLOAD)
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
await page.goto(USAGE_PAGE, { waitUntil: "domcontentloaded" });
|
|
77
|
+
const summaryTile = page.locator("p.font-semibold.text-xl", { hasText: /^(1,000|500)$/ }).first();
|
|
78
|
+
await expect(summaryTile).toBeVisible({ timeout: 15e3 });
|
|
79
|
+
});
|
|
80
|
+
test("TC-AI-TOKEN-USAGE-002: apply filter triggers re-fetch with new date params", async ({ page }) => {
|
|
81
|
+
await login(page, "superadmin");
|
|
82
|
+
const fetchedUrls = [];
|
|
83
|
+
await page.route("**/api/ai_assistant/usage/daily**", async (route) => {
|
|
84
|
+
fetchedUrls.push(route.request().url());
|
|
85
|
+
await route.fulfill({
|
|
86
|
+
status: 200,
|
|
87
|
+
contentType: "application/json",
|
|
88
|
+
body: JSON.stringify(EMPTY_DAILY_PAYLOAD)
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
await page.route("**/api/ai_assistant/usage/sessions**", async (route) => {
|
|
92
|
+
await route.fulfill({
|
|
93
|
+
status: 200,
|
|
94
|
+
contentType: "application/json",
|
|
95
|
+
body: JSON.stringify(EMPTY_SESSIONS_PAYLOAD)
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
await page.goto(USAGE_PAGE, { waitUntil: "domcontentloaded" });
|
|
99
|
+
const fromInput = page.locator("#usage-from");
|
|
100
|
+
const toInput = page.locator("#usage-to");
|
|
101
|
+
const applyButton = page.getByRole("button", { name: /apply/i });
|
|
102
|
+
await expect(fromInput).toBeVisible({ timeout: 1e4 });
|
|
103
|
+
await fromInput.fill("2026-04-01");
|
|
104
|
+
await toInput.fill("2026-04-30");
|
|
105
|
+
await applyButton.click();
|
|
106
|
+
await page.waitForTimeout(500);
|
|
107
|
+
const hasNewDates = fetchedUrls.some((url) => url.includes("from=2026-04-01"));
|
|
108
|
+
expect(hasNewDates).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
test("TC-AI-TOKEN-USAGE-003: sessions list renders rows when API returns sessions", async ({ page }) => {
|
|
111
|
+
await login(page, "superadmin");
|
|
112
|
+
await page.route("**/api/ai_assistant/usage/daily**", async (route) => {
|
|
113
|
+
await route.fulfill({
|
|
114
|
+
status: 200,
|
|
115
|
+
contentType: "application/json",
|
|
116
|
+
body: JSON.stringify(EMPTY_DAILY_PAYLOAD)
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
await page.route("**/api/ai_assistant/usage/sessions**", async (route, request) => {
|
|
120
|
+
if (request.url().includes("/sessions/")) {
|
|
121
|
+
await route.continue();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
await route.fulfill({
|
|
125
|
+
status: 200,
|
|
126
|
+
contentType: "application/json",
|
|
127
|
+
body: JSON.stringify({ sessions: [SESSION_ROW], total: 1, limit: 50, offset: 0 })
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
await page.goto(USAGE_PAGE, { waitUntil: "domcontentloaded" });
|
|
131
|
+
const sessionCell = page.getByText("00000000").first();
|
|
132
|
+
await expect(sessionCell).toBeVisible({ timeout: 15e3 });
|
|
133
|
+
});
|
|
134
|
+
test("TC-AI-TOKEN-USAGE-004: clicking a session row opens the detail dialog", async ({ page }) => {
|
|
135
|
+
await login(page, "superadmin");
|
|
136
|
+
await page.route("**/api/ai_assistant/usage/daily**", async (route) => {
|
|
137
|
+
await route.fulfill({
|
|
138
|
+
status: 200,
|
|
139
|
+
contentType: "application/json",
|
|
140
|
+
body: JSON.stringify(EMPTY_DAILY_PAYLOAD)
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
await page.route("**/api/ai_assistant/usage/sessions/00000000-0000-0000-0000-000000000001", async (route) => {
|
|
144
|
+
await route.fulfill({
|
|
145
|
+
status: 200,
|
|
146
|
+
contentType: "application/json",
|
|
147
|
+
body: JSON.stringify({ events: [STEP_EVENT], total: 1, sessionId: SESSION_ROW.sessionId })
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
await page.route("**/api/ai_assistant/usage/sessions**", async (route, request) => {
|
|
151
|
+
if (request.url().includes("/00000000-0000-0000-0000-000000000001")) {
|
|
152
|
+
await route.fallback();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await route.fulfill({
|
|
156
|
+
status: 200,
|
|
157
|
+
contentType: "application/json",
|
|
158
|
+
body: JSON.stringify({ sessions: [SESSION_ROW], total: 1, limit: 50, offset: 0 })
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
await page.goto(USAGE_PAGE, { waitUntil: "domcontentloaded" });
|
|
162
|
+
const sessionCell = page.getByText("00000000").first();
|
|
163
|
+
await expect(sessionCell).toBeVisible({ timeout: 15e3 });
|
|
164
|
+
await sessionCell.click();
|
|
165
|
+
const dialogTitle = page.getByRole("dialog");
|
|
166
|
+
await expect(dialogTitle).toBeVisible({ timeout: 1e4 });
|
|
167
|
+
const modelCell = page.getByText("claude-haiku-4-5").first();
|
|
168
|
+
await expect(modelCell).toBeVisible({ timeout: 5e3 });
|
|
169
|
+
});
|
|
170
|
+
test("TC-AI-TOKEN-USAGE-005: unauthenticated visit redirects to login", async ({ browser }) => {
|
|
171
|
+
const context = await browser.newContext();
|
|
172
|
+
const page = await context.newPage();
|
|
173
|
+
try {
|
|
174
|
+
await page.goto(USAGE_PAGE, { waitUntil: "domcontentloaded" });
|
|
175
|
+
await page.waitForURL(/\/login/, { timeout: 15e3 });
|
|
176
|
+
expect(page.url()).toMatch(/\/login/);
|
|
177
|
+
} finally {
|
|
178
|
+
await context.close();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
//# sourceMappingURL=TC-AI-TOKEN-USAGE-001-005.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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 const fetchedUrls: string[] = [];\n\n await page.route('**/api/ai_assistant/usage/daily**', async (route) => {\n fetchedUrls.push(route.request().url());\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 await page.goto(USAGE_PAGE, { waitUntil: 'domcontentloaded' });\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 applyButton.click();\n\n await page.waitForTimeout(500);\n\n const hasNewDates = fetchedUrls.some((url) => url.includes('from=2026-04-01'));\n expect(hasNewDates).toBe(true);\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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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|