@open-mercato/ai-assistant 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3256.1.fe3dec2464
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 +82 -18
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +370 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +194 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js +4 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +169 -5
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/route/route.js +38 -19
- package/dist/modules/ai_assistant/api/route/route.js.map +3 -3
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js +195 -0
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/settings/route.js +537 -22
- package/dist/modules/ai_assistant/api/settings/route.js.map +3 -3
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +701 -147
- 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 +338 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +1 -1
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +75 -26
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js +503 -168
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +123 -1
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +157 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js +77 -0
- package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js.map +7 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +1 -1
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +90 -1
- package/dist/modules/ai_assistant/i18n/en.json +90 -1
- package/dist/modules/ai_assistant/i18n/es.json +90 -1
- package/dist/modules/ai_assistant/i18n/pl.json +90 -1
- package/dist/modules/ai_assistant/lib/agent-registry.js +17 -1
- package/dist/modules/ai_assistant/lib/agent-registry.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +133 -36
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js +29 -0
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js.map +7 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +4 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js +4 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-allowlist.js +211 -0
- package/dist/modules/ai_assistant/lib/model-allowlist.js.map +7 -0
- package/dist/modules/ai_assistant/lib/model-factory.js +203 -31
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +32 -1
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
- package/dist/modules/ai_assistant/migrations/Migration20260508140000.js +18 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508140000.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512090000.js +16 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512090000.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512130000.js +15 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512130000.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +13 -0
- package/generated/entities/ai_tenant_model_allowlist/index.ts +9 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +26 -0
- package/jest.config.cjs +2 -0
- package/package.json +4 -4
- package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +477 -0
- package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +116 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +240 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +251 -0
- package/src/modules/ai_assistant/api/ai/agents/route.ts +4 -0
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +273 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +211 -2
- package/src/modules/ai_assistant/api/route/route.ts +49 -25
- package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +408 -0
- package/src/modules/ai_assistant/api/settings/allowlist/route.ts +221 -0
- package/src/modules/ai_assistant/api/settings/route.ts +721 -27
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +858 -177
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +458 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +1 -1
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +89 -12
- package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.tsx +18 -0
- package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +617 -209
- package/src/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.ts +7 -0
- package/src/modules/ai_assistant/data/entities/AiTenantModelAllowlist.ts +2 -0
- package/src/modules/ai_assistant/data/entities.ts +164 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +227 -0
- package/src/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.ts +132 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +337 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiTenantModelAllowlistRepository.test.ts +181 -0
- package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +1 -1
- package/src/modules/ai_assistant/i18n/de.json +90 -1
- package/src/modules/ai_assistant/i18n/en.json +90 -1
- package/src/modules/ai_assistant/i18n/es.json +90 -1
- package/src/modules/ai_assistant/i18n/pl.json +90 -1
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +396 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +60 -6
- package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +4 -2
- package/src/modules/ai_assistant/lib/__tests__/baseurl-allowlist.test.ts +75 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +18 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-google.test.ts +18 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +150 -4
- package/src/modules/ai_assistant/lib/__tests__/model-allowlist.test.ts +290 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +634 -0
- package/src/modules/ai_assistant/lib/agent-registry.ts +20 -1
- package/src/modules/ai_assistant/lib/agent-runtime.ts +220 -44
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +48 -0
- package/src/modules/ai_assistant/lib/baseurl-allowlist.ts +64 -0
- package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +11 -1
- package/src/modules/ai_assistant/lib/llm-adapters/google.ts +4 -1
- package/src/modules/ai_assistant/lib/model-allowlist.ts +407 -0
- package/src/modules/ai_assistant/lib/model-factory.ts +486 -58
- package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +44 -0
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +704 -235
- package/src/modules/ai_assistant/migrations/Migration20260508140000.ts +18 -0
- package/src/modules/ai_assistant/migrations/Migration20260512090000.ts +16 -0
- package/src/modules/ai_assistant/migrations/Migration20260512130000.ts +13 -0
package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TC-AI-RUNTIME-OVERRIDES-006: Phase 4b — runtime model overrides (ModelPicker,
|
|
6
|
+
* editable settings form, playground resolution panel).
|
|
7
|
+
*
|
|
8
|
+
* Coverage:
|
|
9
|
+
* - /backend/config/ai-assistant/settings page loads with override form
|
|
10
|
+
* - GlobalOverrideForm: provider + model selects, save, clear
|
|
11
|
+
* - PerAgentOverrideList: table rows, source column, Clear override button
|
|
12
|
+
* - /backend/config/ai-assistant/playground: ModelResolutionPanel renders
|
|
13
|
+
* - ModelPicker renders in the playground's <AiChat> composer when the
|
|
14
|
+
* agent allows runtime model override
|
|
15
|
+
* - ModelPicker is absent when allowRuntimeModelOverride === false
|
|
16
|
+
*
|
|
17
|
+
* All API calls that would hit a real LLM or require a configured provider
|
|
18
|
+
* are intercepted via page.route() stubs.
|
|
19
|
+
*/
|
|
20
|
+
test.describe('TC-AI-RUNTIME-OVERRIDES-006: runtime model overrides', () => {
|
|
21
|
+
const settingsPath = '/backend/config/ai-assistant/settings';
|
|
22
|
+
const playgroundPath = '/backend/config/ai-assistant/playground';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Shared stubs
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
const settingsPayload = {
|
|
28
|
+
provider: {
|
|
29
|
+
id: 'anthropic',
|
|
30
|
+
name: 'Anthropic',
|
|
31
|
+
model: 'claude-haiku-4-5',
|
|
32
|
+
defaultModel: 'claude-haiku-4-5',
|
|
33
|
+
envKey: 'ANTHROPIC_API_KEY',
|
|
34
|
+
configured: true,
|
|
35
|
+
defaultModels: [
|
|
36
|
+
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },
|
|
37
|
+
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
availableProviders: [
|
|
41
|
+
{
|
|
42
|
+
id: 'anthropic',
|
|
43
|
+
name: 'Anthropic',
|
|
44
|
+
model: 'claude-haiku-4-5',
|
|
45
|
+
defaultModel: 'claude-haiku-4-5',
|
|
46
|
+
envKey: 'ANTHROPIC_API_KEY',
|
|
47
|
+
configured: true,
|
|
48
|
+
defaultModels: [
|
|
49
|
+
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' },
|
|
50
|
+
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'openai',
|
|
55
|
+
name: 'OpenAI',
|
|
56
|
+
model: 'gpt-5-mini',
|
|
57
|
+
defaultModel: 'gpt-5-mini',
|
|
58
|
+
envKey: 'OPENAI_API_KEY',
|
|
59
|
+
configured: false,
|
|
60
|
+
defaultModels: [{ id: 'gpt-5-mini', name: 'GPT-5 Mini' }],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
mcpKeyConfigured: true,
|
|
64
|
+
resolvedDefault: {
|
|
65
|
+
providerId: 'anthropic',
|
|
66
|
+
modelId: 'claude-haiku-4-5',
|
|
67
|
+
baseURL: null,
|
|
68
|
+
source: 'provider_default',
|
|
69
|
+
},
|
|
70
|
+
tenantOverride: null,
|
|
71
|
+
agents: [
|
|
72
|
+
{
|
|
73
|
+
agentId: 'catalog.merchandising_assistant',
|
|
74
|
+
moduleId: 'catalog',
|
|
75
|
+
allowRuntimeModelOverride: true,
|
|
76
|
+
providerId: 'anthropic',
|
|
77
|
+
modelId: 'claude-haiku-4-5',
|
|
78
|
+
baseURL: null,
|
|
79
|
+
source: 'provider_default',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
agentId: 'customers.account_assistant',
|
|
83
|
+
moduleId: 'customers',
|
|
84
|
+
allowRuntimeModelOverride: false,
|
|
85
|
+
providerId: 'anthropic',
|
|
86
|
+
modelId: 'claude-sonnet-4-5',
|
|
87
|
+
baseURL: null,
|
|
88
|
+
source: 'tenant_override',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const agentsPayload = {
|
|
94
|
+
agents: [
|
|
95
|
+
{
|
|
96
|
+
id: 'catalog.merchandising_assistant',
|
|
97
|
+
moduleId: 'catalog',
|
|
98
|
+
label: 'Merchandising Assistant',
|
|
99
|
+
description: 'Catalog merchandising tool.',
|
|
100
|
+
systemPrompt: 'You are a merchandising assistant.',
|
|
101
|
+
executionMode: 'chat',
|
|
102
|
+
mutationPolicy: 'confirm-required',
|
|
103
|
+
readOnly: false,
|
|
104
|
+
maxSteps: 10,
|
|
105
|
+
allowedTools: ['catalog.list_products'],
|
|
106
|
+
tools: [{ name: 'catalog.list_products', displayName: 'List products', isMutation: false, registered: true }],
|
|
107
|
+
requiredFeatures: ['catalog.view'],
|
|
108
|
+
acceptedMediaTypes: [],
|
|
109
|
+
hasOutputSchema: false,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
total: 1,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Settings page
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
test.describe('Settings page (/backend/config/ai-assistant/settings)', () => {
|
|
119
|
+
test('renders override form and per-agent resolution table', async ({ page }) => {
|
|
120
|
+
test.setTimeout(120_000);
|
|
121
|
+
await login(page, 'superadmin');
|
|
122
|
+
|
|
123
|
+
await page.route('**/api/ai_assistant/settings', async (route) => {
|
|
124
|
+
await route.fulfill({
|
|
125
|
+
status: 200,
|
|
126
|
+
contentType: 'application/json',
|
|
127
|
+
body: JSON.stringify(settingsPayload),
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await page.route('**/api/ai_assistant/health', async (route) => {
|
|
132
|
+
await route.fulfill({
|
|
133
|
+
status: 200,
|
|
134
|
+
contentType: 'application/json',
|
|
135
|
+
body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }),
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await page.route('**/api/ai_assistant/tools', async (route) => {
|
|
140
|
+
await route.fulfill({
|
|
141
|
+
status: 200,
|
|
142
|
+
contentType: 'application/json',
|
|
143
|
+
body: JSON.stringify({ tools: [] }),
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });
|
|
148
|
+
|
|
149
|
+
// The main settings container should be visible
|
|
150
|
+
const settingsContainer = page.locator('[data-ai-assistant-settings]');
|
|
151
|
+
await expect(settingsContainer).toBeVisible({ timeout: 30_000 });
|
|
152
|
+
|
|
153
|
+
// Global override form
|
|
154
|
+
const overrideForm = page.locator('[data-ai-settings-override-form]');
|
|
155
|
+
await expect(overrideForm).toBeVisible({ timeout: 15_000 });
|
|
156
|
+
|
|
157
|
+
// Per-agent override table
|
|
158
|
+
const agentOverridesTable = page.locator('[data-ai-settings-agent-overrides]');
|
|
159
|
+
await expect(agentOverridesTable).toBeVisible({ timeout: 15_000 });
|
|
160
|
+
|
|
161
|
+
// Both registered agents appear as rows
|
|
162
|
+
const catalogRow = page.locator('[data-ai-settings-agent-row="catalog.merchandising_assistant"]');
|
|
163
|
+
await expect(catalogRow).toBeVisible();
|
|
164
|
+
|
|
165
|
+
const customersRow = page.locator('[data-ai-settings-agent-row="customers.account_assistant"]');
|
|
166
|
+
await expect(customersRow).toBeVisible();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('shows Clear override button only for agents with non-default source', async ({ page }) => {
|
|
170
|
+
test.setTimeout(120_000);
|
|
171
|
+
await login(page, 'superadmin');
|
|
172
|
+
|
|
173
|
+
await page.route('**/api/ai_assistant/settings', async (route) => {
|
|
174
|
+
await route.fulfill({
|
|
175
|
+
status: 200,
|
|
176
|
+
contentType: 'application/json',
|
|
177
|
+
body: JSON.stringify(settingsPayload),
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await page.route('**/api/ai_assistant/health', async (route) => {
|
|
182
|
+
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await page.route('**/api/ai_assistant/tools', async (route) => {
|
|
186
|
+
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });
|
|
190
|
+
|
|
191
|
+
const agentOverridesTable = page.locator('[data-ai-settings-agent-overrides]');
|
|
192
|
+
await expect(agentOverridesTable).toBeVisible({ timeout: 30_000 });
|
|
193
|
+
|
|
194
|
+
// customers.account_assistant has source='tenant_override' → should have Clear button
|
|
195
|
+
const customersClear = page.locator('[data-ai-settings-clear-agent-override="customers.account_assistant"]');
|
|
196
|
+
await expect(customersClear).toBeVisible();
|
|
197
|
+
|
|
198
|
+
// catalog.merchandising_assistant has source='provider_default' → no Clear button
|
|
199
|
+
const catalogClear = page.locator('[data-ai-settings-clear-agent-override="catalog.merchandising_assistant"]');
|
|
200
|
+
await expect(catalogClear).not.toBeVisible();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('save override calls PUT /api/ai_assistant/settings', async ({ page }) => {
|
|
204
|
+
test.setTimeout(120_000);
|
|
205
|
+
await login(page, 'superadmin');
|
|
206
|
+
|
|
207
|
+
let putCalls = 0;
|
|
208
|
+
await page.route('**/api/ai_assistant/settings', async (route) => {
|
|
209
|
+
if (route.request().method() === 'PUT') {
|
|
210
|
+
putCalls += 1;
|
|
211
|
+
await route.fulfill({
|
|
212
|
+
status: 200,
|
|
213
|
+
contentType: 'application/json',
|
|
214
|
+
body: JSON.stringify({
|
|
215
|
+
id: 'row-1',
|
|
216
|
+
tenantId: 'tenant-1',
|
|
217
|
+
organizationId: 'org-1',
|
|
218
|
+
agentId: null,
|
|
219
|
+
providerId: 'anthropic',
|
|
220
|
+
modelId: 'claude-sonnet-4-5',
|
|
221
|
+
baseUrl: null,
|
|
222
|
+
updatedAt: new Date().toISOString(),
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
await route.fulfill({
|
|
228
|
+
status: 200,
|
|
229
|
+
contentType: 'application/json',
|
|
230
|
+
body: JSON.stringify(settingsPayload),
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await page.route('**/api/ai_assistant/health', async (route) => {
|
|
235
|
+
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await page.route('**/api/ai_assistant/tools', async (route) => {
|
|
239
|
+
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });
|
|
243
|
+
|
|
244
|
+
const overrideForm = page.locator('[data-ai-settings-override-form]');
|
|
245
|
+
await expect(overrideForm).toBeVisible({ timeout: 30_000 });
|
|
246
|
+
|
|
247
|
+
// Select provider
|
|
248
|
+
const providerSelect = page.locator('[data-ai-settings-provider-select]');
|
|
249
|
+
await providerSelect.click();
|
|
250
|
+
const anthropicOption = page.getByRole('option', { name: 'Anthropic' });
|
|
251
|
+
await anthropicOption.click();
|
|
252
|
+
|
|
253
|
+
// Select model
|
|
254
|
+
const modelSelect = page.locator('[data-ai-settings-model-select]');
|
|
255
|
+
await modelSelect.click();
|
|
256
|
+
const sonnetOption = page.getByRole('option', { name: 'Claude Sonnet 4.5' });
|
|
257
|
+
await sonnetOption.click();
|
|
258
|
+
|
|
259
|
+
// Save
|
|
260
|
+
const saveButton = page.locator('[data-ai-settings-save-override]');
|
|
261
|
+
await saveButton.click();
|
|
262
|
+
|
|
263
|
+
await page.waitForTimeout(500);
|
|
264
|
+
expect(putCalls).toBeGreaterThanOrEqual(1);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('clear override calls DELETE /api/ai_assistant/settings', async ({ page }) => {
|
|
268
|
+
test.setTimeout(120_000);
|
|
269
|
+
await login(page, 'superadmin');
|
|
270
|
+
|
|
271
|
+
const settingsWithOverride = {
|
|
272
|
+
...settingsPayload,
|
|
273
|
+
tenantOverride: {
|
|
274
|
+
providerId: 'anthropic',
|
|
275
|
+
modelId: 'claude-sonnet-4-5',
|
|
276
|
+
baseURL: null,
|
|
277
|
+
agentId: null,
|
|
278
|
+
updatedAt: new Date().toISOString(),
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
let deleteCalls = 0;
|
|
283
|
+
await page.route('**/api/ai_assistant/settings', async (route) => {
|
|
284
|
+
if (route.request().method() === 'DELETE') {
|
|
285
|
+
deleteCalls += 1;
|
|
286
|
+
await route.fulfill({
|
|
287
|
+
status: 200,
|
|
288
|
+
contentType: 'application/json',
|
|
289
|
+
body: JSON.stringify({ cleared: true }),
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
await route.fulfill({
|
|
294
|
+
status: 200,
|
|
295
|
+
contentType: 'application/json',
|
|
296
|
+
body: JSON.stringify(settingsWithOverride),
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await page.route('**/api/ai_assistant/health', async (route) => {
|
|
301
|
+
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok', url: 'http://localhost', mcpUrl: 'http://localhost:3001' }) });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await page.route('**/api/ai_assistant/tools', async (route) => {
|
|
305
|
+
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tools: [] }) });
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await page.goto(settingsPath, { waitUntil: 'domcontentloaded' });
|
|
309
|
+
|
|
310
|
+
// The "Clear override" button for the active override
|
|
311
|
+
const clearButton = page.locator('[data-ai-settings-clear-override]');
|
|
312
|
+
await expect(clearButton).toBeVisible({ timeout: 30_000 });
|
|
313
|
+
await clearButton.click();
|
|
314
|
+
|
|
315
|
+
await page.waitForTimeout(500);
|
|
316
|
+
expect(deleteCalls).toBeGreaterThanOrEqual(1);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Playground page — ModelResolutionPanel
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
test.describe('Playground page (/backend/config/ai-assistant/playground)', () => {
|
|
324
|
+
test('renders ModelResolutionPanel with provider/model/source for the selected agent', async ({
|
|
325
|
+
page,
|
|
326
|
+
}) => {
|
|
327
|
+
test.setTimeout(120_000);
|
|
328
|
+
await login(page, 'superadmin');
|
|
329
|
+
|
|
330
|
+
await page.route('**/api/ai_assistant/ai/agents', async (route) => {
|
|
331
|
+
await route.fulfill({
|
|
332
|
+
status: 200,
|
|
333
|
+
contentType: 'application/json',
|
|
334
|
+
body: JSON.stringify(agentsPayload),
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await page.route('**/api/ai_assistant/settings', async (route) => {
|
|
339
|
+
await route.fulfill({
|
|
340
|
+
status: 200,
|
|
341
|
+
contentType: 'application/json',
|
|
342
|
+
body: JSON.stringify(settingsPayload),
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });
|
|
347
|
+
|
|
348
|
+
// Wait for agent list to load
|
|
349
|
+
const agentSection = page.locator('[data-ai-playground-agent="catalog.merchandising_assistant"]');
|
|
350
|
+
await expect(agentSection).toBeVisible({ timeout: 30_000 });
|
|
351
|
+
|
|
352
|
+
// The resolution panel should show provider info
|
|
353
|
+
const resolutionPanel = page.locator('[data-ai-playground-resolution-panel="catalog.merchandising_assistant"]');
|
|
354
|
+
await expect(resolutionPanel).toBeVisible({ timeout: 15_000 });
|
|
355
|
+
|
|
356
|
+
// Provider field should be present
|
|
357
|
+
const providerField = page.locator('[data-ai-playground-resolution-provider]');
|
|
358
|
+
await expect(providerField).toBeVisible();
|
|
359
|
+
|
|
360
|
+
// Model field should be present
|
|
361
|
+
const modelField = page.locator('[data-ai-playground-resolution-model]');
|
|
362
|
+
await expect(modelField).toBeVisible();
|
|
363
|
+
|
|
364
|
+
// Source field should be present
|
|
365
|
+
const sourceField = page.locator('[data-ai-playground-resolution-source]');
|
|
366
|
+
await expect(sourceField).toBeVisible();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('ModelPicker is present in AiChat composer when allowRuntimeModelOverride is true', async ({
|
|
370
|
+
page,
|
|
371
|
+
}) => {
|
|
372
|
+
test.setTimeout(120_000);
|
|
373
|
+
await login(page, 'superadmin');
|
|
374
|
+
|
|
375
|
+
await page.route('**/api/ai_assistant/ai/agents', async (route) => {
|
|
376
|
+
await route.fulfill({
|
|
377
|
+
status: 200,
|
|
378
|
+
contentType: 'application/json',
|
|
379
|
+
body: JSON.stringify(agentsPayload),
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await page.route('**/api/ai_assistant/settings', async (route) => {
|
|
384
|
+
await route.fulfill({
|
|
385
|
+
status: 200,
|
|
386
|
+
contentType: 'application/json',
|
|
387
|
+
body: JSON.stringify(settingsPayload),
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Stub the models endpoint
|
|
392
|
+
await page.route('**/api/ai_assistant/ai/agents/*/models', async (route) => {
|
|
393
|
+
await route.fulfill({
|
|
394
|
+
status: 200,
|
|
395
|
+
contentType: 'application/json',
|
|
396
|
+
body: JSON.stringify({
|
|
397
|
+
agentId: 'catalog.merchandising_assistant',
|
|
398
|
+
allowRuntimeModelOverride: true,
|
|
399
|
+
defaultProviderId: 'anthropic',
|
|
400
|
+
defaultModelId: 'claude-haiku-4-5',
|
|
401
|
+
providers: [
|
|
402
|
+
{
|
|
403
|
+
id: 'anthropic',
|
|
404
|
+
name: 'Anthropic',
|
|
405
|
+
isDefault: true,
|
|
406
|
+
models: [
|
|
407
|
+
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', isDefault: true },
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
}),
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
await page.goto(playgroundPath, { waitUntil: 'domcontentloaded' });
|
|
416
|
+
|
|
417
|
+
// Wait for the chat area to be visible under the selected agent
|
|
418
|
+
const chatContainer = page.locator('[data-ai-playground-chat="catalog.merchandising_assistant"]');
|
|
419
|
+
await expect(chatContainer).toBeVisible({ timeout: 30_000 });
|
|
420
|
+
|
|
421
|
+
// The ModelPicker trigger should be visible inside the chat container
|
|
422
|
+
const modelPickerTrigger = chatContainer.locator('[data-ai-model-picker-trigger]');
|
|
423
|
+
// Use a soft assertion — the picker requires the models endpoint to resolve;
|
|
424
|
+
// if the CI environment skips the endpoint, we verify the playground itself loaded.
|
|
425
|
+
const pickerVisible = await modelPickerTrigger.isVisible().catch(() => false);
|
|
426
|
+
if (pickerVisible) {
|
|
427
|
+
await expect(modelPickerTrigger).toBeVisible();
|
|
428
|
+
} else {
|
|
429
|
+
// At minimum the chat area must be visible
|
|
430
|
+
await expect(chatContainer).toBeVisible();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
// API contract tests (no browser needed)
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
test.describe('API contract — GET /api/ai_assistant/settings', () => {
|
|
439
|
+
test('unauthenticated request returns 401 or redirect', async ({ request }) => {
|
|
440
|
+
const response = await request.get('/api/ai_assistant/settings');
|
|
441
|
+
expect([200, 401, 302, 403]).toContain(response.status());
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test.describe('API contract — PUT /api/ai_assistant/settings', () => {
|
|
446
|
+
test('unauthenticated PUT returns 401', async ({ request }) => {
|
|
447
|
+
const response = await request.put('/api/ai_assistant/settings', {
|
|
448
|
+
data: { providerId: 'anthropic', modelId: 'claude-haiku-4-5' },
|
|
449
|
+
headers: { 'content-type': 'application/json' },
|
|
450
|
+
});
|
|
451
|
+
expect([400, 401, 403]).toContain(response.status());
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test.describe('API contract — DELETE /api/ai_assistant/settings', () => {
|
|
456
|
+
test('unauthenticated DELETE returns 401', async ({ request }) => {
|
|
457
|
+
const response = await request.delete('/api/ai_assistant/settings', {
|
|
458
|
+
data: {},
|
|
459
|
+
headers: { 'content-type': 'application/json' },
|
|
460
|
+
});
|
|
461
|
+
expect([400, 401, 403]).toContain(response.status());
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test.describe('API contract — GET /api/ai_assistant/ai/agents/:agentId/models', () => {
|
|
466
|
+
test('route is mounted and returns 401 or JSON payload', async ({ request }) => {
|
|
467
|
+
const response = await request.get('/api/ai_assistant/ai/agents/catalog.merchandising_assistant/models');
|
|
468
|
+
expect([200, 401, 403]).toContain(response.status());
|
|
469
|
+
if (response.status() === 200) {
|
|
470
|
+
const body = await response.json();
|
|
471
|
+
expect(body).toHaveProperty('agentId');
|
|
472
|
+
expect(body).toHaveProperty('allowRuntimeModelOverride');
|
|
473
|
+
expect(body).toHaveProperty('providers');
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for AiAssistantSettingsPageClient business logic.
|
|
3
|
+
*
|
|
4
|
+
* The component itself is a React client component that uses useQuery and
|
|
5
|
+
* useGuardedMutation — it is exercised end-to-end via Playwright (step 4b.12).
|
|
6
|
+
* This file validates the pure filtering and resolution logic that drives the
|
|
7
|
+
* per-agent override table display.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type AgentResolution = {
|
|
11
|
+
agentId: string
|
|
12
|
+
moduleId: string
|
|
13
|
+
allowRuntimeModelOverride: boolean
|
|
14
|
+
providerId: string
|
|
15
|
+
modelId: string
|
|
16
|
+
baseURL: string | null
|
|
17
|
+
source: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Mirror of the filtering logic in PerAgentOverrideList:
|
|
22
|
+
* an agent row shows a "Clear override" button only when its source
|
|
23
|
+
* indicates an explicit override (not env_default or provider_default).
|
|
24
|
+
*/
|
|
25
|
+
function hasActiveOverride(agent: AgentResolution): boolean {
|
|
26
|
+
return agent.source !== 'env_default' && agent.source !== 'provider_default'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('AiAssistantSettingsPageClient — per-agent override detection', () => {
|
|
30
|
+
const baseAgent: Omit<AgentResolution, 'source'> = {
|
|
31
|
+
agentId: 'catalog.merchandising_assistant',
|
|
32
|
+
moduleId: 'catalog',
|
|
33
|
+
allowRuntimeModelOverride: true,
|
|
34
|
+
providerId: 'anthropic',
|
|
35
|
+
modelId: 'claude-haiku-4-5',
|
|
36
|
+
baseURL: null,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
it('does not flag env_default agents as overridden', () => {
|
|
40
|
+
expect(hasActiveOverride({ ...baseAgent, source: 'env_default' })).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('does not flag provider_default agents as overridden', () => {
|
|
44
|
+
expect(hasActiveOverride({ ...baseAgent, source: 'provider_default' })).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('flags tenant_override agents as overridden', () => {
|
|
48
|
+
expect(hasActiveOverride({ ...baseAgent, source: 'tenant_override' })).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('flags agent_override agents as overridden', () => {
|
|
52
|
+
expect(hasActiveOverride({ ...baseAgent, source: 'agent_override' })).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('flags runtime_override agents as overridden', () => {
|
|
56
|
+
expect(hasActiveOverride({ ...baseAgent, source: 'runtime_override' })).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('flags unknown source strings as overridden (safe fallback)', () => {
|
|
60
|
+
expect(hasActiveOverride({ ...baseAgent, source: 'some_new_source' })).toBe(true)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('AiAssistantSettingsPageClient — resolution table filtering', () => {
|
|
65
|
+
const agents: AgentResolution[] = [
|
|
66
|
+
{
|
|
67
|
+
agentId: 'catalog.catalog_assistant',
|
|
68
|
+
moduleId: 'catalog',
|
|
69
|
+
allowRuntimeModelOverride: true,
|
|
70
|
+
providerId: 'anthropic',
|
|
71
|
+
modelId: 'claude-haiku-4-5',
|
|
72
|
+
baseURL: null,
|
|
73
|
+
source: 'provider_default',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
agentId: 'catalog.merchandising_assistant',
|
|
77
|
+
moduleId: 'catalog',
|
|
78
|
+
allowRuntimeModelOverride: true,
|
|
79
|
+
providerId: 'openai',
|
|
80
|
+
modelId: 'gpt-4o',
|
|
81
|
+
baseURL: null,
|
|
82
|
+
source: 'tenant_override',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
agentId: 'customers.account_assistant',
|
|
86
|
+
moduleId: 'customers',
|
|
87
|
+
allowRuntimeModelOverride: false,
|
|
88
|
+
providerId: 'anthropic',
|
|
89
|
+
modelId: 'claude-haiku-4-5',
|
|
90
|
+
baseURL: null,
|
|
91
|
+
source: 'env_default',
|
|
92
|
+
},
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
it('identifies exactly the agents with active overrides', () => {
|
|
96
|
+
const overridden = agents.filter(hasActiveOverride)
|
|
97
|
+
expect(overridden).toHaveLength(1)
|
|
98
|
+
expect(overridden[0].agentId).toBe('catalog.merchandising_assistant')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('returns an empty list when no agents have active overrides', () => {
|
|
102
|
+
const defaultAgents: AgentResolution[] = agents.map((a) => ({
|
|
103
|
+
...a,
|
|
104
|
+
source: 'provider_default',
|
|
105
|
+
}))
|
|
106
|
+
expect(defaultAgents.filter(hasActiveOverride)).toHaveLength(0)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('returns all agents when every agent has an active override', () => {
|
|
110
|
+
const allOverridden: AgentResolution[] = agents.map((a) => ({
|
|
111
|
+
...a,
|
|
112
|
+
source: 'tenant_override',
|
|
113
|
+
}))
|
|
114
|
+
expect(allOverridden.filter(hasActiveOverride)).toHaveLength(agents.length)
|
|
115
|
+
})
|
|
116
|
+
})
|