913 lines
34 KiB
JavaScript
913 lines
34 KiB
JavaScript
const { test, expect } = require('@playwright/test');
|
|
const { getCurrentWorkspace, login, localized } = require('./helpers');
|
|
|
|
test('wizard loads and protocol selection updates flow', async ({ page }) => {
|
|
await login(page);
|
|
await page.goto('/wizard/');
|
|
await expect(page.locator('[data-step-counter]').first()).toContainText(localized('Step', 'Шаг'));
|
|
await expect(page.locator('.step-pane:visible')).toHaveCount(1);
|
|
await expect(page.locator('#step-panel-1')).toBeVisible();
|
|
await expect(page.locator('#step-panel-1 .step-panel-title')).toContainText(localized('Choose a protocol', 'Выберите протокол'));
|
|
|
|
for (const selector of ['.btn-back', '.btn-save-draft']) {
|
|
const box = await page.locator(selector).boundingBox();
|
|
const viewport = page.viewportSize();
|
|
expect(box).not.toBeNull();
|
|
expect(box.y).toBeGreaterThanOrEqual(0);
|
|
expect(box.y + box.height).toBeLessThanOrEqual(viewport.height);
|
|
}
|
|
|
|
await page.locator('[data-testid="wizard-protocol-rest"]').click();
|
|
await page.locator('#btn-continue').click();
|
|
await expect(page.locator('#step-panel-2 .step-panel-title')).toContainText(localized('Select upstream', 'Выберите upstream'));
|
|
});
|
|
|
|
test('community wizard exposes REST-only protocol flow', async ({ page }) => {
|
|
await login(page);
|
|
await page.goto('/wizard/');
|
|
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
|
|
|
|
await page.locator('[data-testid="wizard-protocol-rest"]').click();
|
|
await page.locator('#btn-continue').click();
|
|
await page.locator('#btn-continue').click();
|
|
await page.locator('#btn-continue').click();
|
|
await page.locator('#btn-continue').click();
|
|
|
|
await expect(page.locator('#wizard-streaming-config-card')).toBeHidden();
|
|
await expect(page.locator('#wizard-streaming-capability-note')).toHaveCount(0);
|
|
});
|
|
|
|
test('REST method callout clears when switching to methods without request body', async ({ page }) => {
|
|
await login(page);
|
|
const workspace = await getCurrentWorkspace(page);
|
|
|
|
await page.route(
|
|
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/upstreams`,
|
|
async (route) => {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
items: [
|
|
{
|
|
id: 'upstream_e2e_rest_methods',
|
|
workspace_id: workspace.id,
|
|
name: 'E2E REST target',
|
|
base_url: 'https://api.example.test',
|
|
static_headers: {},
|
|
auth_profile_id: null,
|
|
created_at: '2026-06-19T00:00:00Z',
|
|
updated_at: '2026-06-19T00:00:00Z',
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
},
|
|
);
|
|
|
|
await page.goto('/wizard/');
|
|
await expect(page.locator('[data-step-counter]').first()).toContainText(localized('Step', 'Шаг'));
|
|
|
|
await page.locator('[data-testid="wizard-protocol-rest"]').click();
|
|
await page.locator('#btn-continue').click();
|
|
await expect(page.locator('#step-panel-2')).toBeVisible();
|
|
await page.locator('#upstream-combobox-trigger').click();
|
|
await page.getByText('E2E REST target').click();
|
|
await page.locator('#btn-continue').click();
|
|
await expect(page.locator('#step-panel-3-rest')).toBeVisible();
|
|
|
|
const callout = page.locator('#method-callout-rest');
|
|
await page.locator('.method-card[data-method="POST"]').click();
|
|
await expect(callout).toBeVisible();
|
|
await expect(callout).toContainText(/POST|Выбран POST/);
|
|
|
|
await page.locator('.method-card[data-method="PATCH"]').click();
|
|
await expect(callout).toBeVisible();
|
|
await expect(callout).toContainText(/PATCH|Выбран PATCH/);
|
|
|
|
await page.locator('.method-card[data-method="DELETE"]').click();
|
|
await expect(callout).toBeVisible();
|
|
await expect(callout).toContainText(/DELETE|Выбран DELETE/);
|
|
await expect(callout).toContainText(/confirmation|подтверждение/i);
|
|
|
|
await page.locator('.method-card[data-method="GET"]').click();
|
|
await expect(callout).toBeHidden();
|
|
await expect(callout).toHaveText('');
|
|
});
|
|
|
|
test('wizard loads reusable upstreams from backend', async ({ page }) => {
|
|
await login(page);
|
|
const workspace = await getCurrentWorkspace(page);
|
|
|
|
await page.route(
|
|
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/upstreams`,
|
|
async (route) => {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
items: [
|
|
{
|
|
id: 'upstream_frankfurter',
|
|
workspace_id: workspace.id,
|
|
name: 'Frankfurter',
|
|
base_url: 'https://api.frankfurter.app',
|
|
static_headers: { Accept: 'application/json' },
|
|
auth_profile_id: null,
|
|
created_at: '2026-06-19T00:00:00Z',
|
|
updated_at: '2026-06-19T00:00:00Z',
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
},
|
|
);
|
|
|
|
await page.goto('/wizard/');
|
|
await expect(page.locator('#step-panel-1')).toBeVisible();
|
|
await page.locator('[data-testid="wizard-protocol-rest"]').click();
|
|
await page.locator('.btn-continue').click();
|
|
await expect(page.locator('#step-panel-2')).toBeVisible();
|
|
await page.locator('#upstream-combobox-trigger').click();
|
|
|
|
await expect(page.locator('#upstream-dropdown-list')).toContainText('Frankfurter');
|
|
await expect(page.locator('#upstream-dropdown-list')).toContainText('https://api.frankfurter.app');
|
|
await expect(page.locator('#upstream-dropdown-list')).not.toContainText('Open Meteo');
|
|
});
|
|
|
|
test('wizard builds visual request mappings from JSON sample and path params', async ({ page }) => {
|
|
await login(page);
|
|
|
|
await page.goto('/wizard/');
|
|
await page.locator('[data-testid="wizard-protocol-rest"]').click();
|
|
await page.evaluate(() => window.CrankWizardShell.doGoToStep(2));
|
|
await expect(page.locator('#step-panel-2')).toBeVisible();
|
|
await page.locator('#endpoint-path').fill('/rates/{date}');
|
|
|
|
await page.evaluate(() => window.CrankWizardShell.doGoToStep(3));
|
|
await expect(page.locator('#step-panel-3-rest')).toBeVisible();
|
|
await page.locator('.method-card[data-method="GET"]').click();
|
|
|
|
await page.evaluate(() => window.CrankWizardShell.doGoToStep(5));
|
|
await expect(page.locator('#step-panel-5')).toBeVisible();
|
|
|
|
await page.locator('#wizard-input-sample').fill(JSON.stringify({
|
|
date: '2024-01-01',
|
|
base: 'USD',
|
|
symbols: 'EUR',
|
|
}, null, 2));
|
|
await page.locator('#wizard-output-sample').fill(JSON.stringify({
|
|
base: 'USD',
|
|
rates: {
|
|
EUR: 0.91,
|
|
},
|
|
providers: [
|
|
{
|
|
name: 'ecb',
|
|
priority: 1,
|
|
},
|
|
],
|
|
}, null, 2));
|
|
|
|
await page.locator('#wizard-format-samples').click();
|
|
|
|
await expect(page.locator('[data-testid="wizard-request-mapping-rows"] [data-role="input"]').first()).toHaveValue('date');
|
|
await expect(page.locator('[data-testid="wizard-request-mapping-rows"] [data-role="input"]').nth(1)).toHaveValue('base');
|
|
await expect(page.locator('#tool-input-mapping')).toHaveValue(/path\.date/);
|
|
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.base/);
|
|
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.symbols/);
|
|
await expect(page.locator('#tool-input-schema')).toHaveValue(/"date"/);
|
|
await expect(page.locator('#tool-output-schema')).toHaveValue(/"rates"/);
|
|
await expect(page.locator('[data-testid="wizard-response-json-tree"]')).toContainText('rates.EUR');
|
|
await expect(page.locator('[data-testid="wizard-response-json-tree"]')).toContainText('providers[0].name');
|
|
await expect(page.locator('#tool-output-mapping')).toHaveValue(/providers_0_name: \$\.response\.body\.providers\[0\]\.name/);
|
|
await expect(page.locator('[data-testid="wizard-mapping-warnings"]')).toContainText(/одного элемента массива/);
|
|
|
|
await page.locator('#wizard-add-request-mapping-row').click();
|
|
const defaultRow = page.locator('[data-testid="wizard-request-mapping-rows"] [data-mapping-row="request"]').last();
|
|
await defaultRow.locator('[data-role="input"]').fill('group');
|
|
await defaultRow.locator('[data-role="target"]').selectOption('query');
|
|
await defaultRow.locator('[data-role="apiName"]').fill('group');
|
|
await defaultRow.locator('[data-role="defaultValue"]').fill('month');
|
|
await defaultRow.locator('[data-role="transform"]').selectOption('to_string');
|
|
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.group:/);
|
|
await expect(page.locator('#tool-input-mapping')).toHaveValue(/default_value: month/);
|
|
await expect(page.locator('#tool-input-mapping')).toHaveValue(/transform: to_string/);
|
|
|
|
await page.locator('[data-testid="wizard-request-mapping-rows"] .mapping-row-remove').first().click();
|
|
await expect(page.locator('[data-testid="wizard-mapping-warnings"]')).toContainText(/date/);
|
|
});
|
|
|
|
test('wizard edit mode hydrates fields from operation version snapshot', async ({ page }) => {
|
|
await login(page);
|
|
const workspace = await getCurrentWorkspace(page);
|
|
const operationId = 'frankfurter_monthly_rates';
|
|
const version = 7;
|
|
|
|
await page.route(
|
|
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}`,
|
|
async (route) => {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
id: operationId,
|
|
workspace_id: workspace.id,
|
|
name: operationId,
|
|
display_name: 'Frankfurter monthly rates',
|
|
category: 'finance',
|
|
protocol: 'rest',
|
|
security_level: 'standard',
|
|
status: 'draft',
|
|
current_draft_version: version,
|
|
latest_published_version: null,
|
|
created_at: '2026-06-19T00:00:00Z',
|
|
updated_at: '2026-06-19T00:00:00Z',
|
|
published_at: null,
|
|
draft_version_ref: { version, status: 'draft' },
|
|
published_version_ref: null,
|
|
agent_refs: [],
|
|
}),
|
|
});
|
|
},
|
|
);
|
|
|
|
await page.route(
|
|
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}/versions/${version}`,
|
|
async (route) => {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
operation_id: operationId,
|
|
workspace_id: workspace.id,
|
|
version,
|
|
status: 'draft',
|
|
change_note: null,
|
|
created_at: '2026-06-19T00:00:00Z',
|
|
created_by: null,
|
|
snapshot: {
|
|
id: operationId,
|
|
name: operationId,
|
|
display_name: 'Frankfurter monthly rates',
|
|
category: 'finance',
|
|
protocol: 'rest',
|
|
security_level: 'standard',
|
|
status: 'draft',
|
|
version,
|
|
target: {
|
|
kind: 'rest',
|
|
base_url: 'https://api.frankfurter.app',
|
|
method: 'GET',
|
|
path_template: '/2024-01..2024-03',
|
|
static_headers: {
|
|
Accept: 'application/json',
|
|
'X-Demo': 'frankfurter',
|
|
},
|
|
},
|
|
input_schema: {
|
|
type: 'object',
|
|
description: null,
|
|
required: false,
|
|
nullable: false,
|
|
default_value: null,
|
|
fields: {
|
|
from: {
|
|
type: 'string',
|
|
description: 'Base currency',
|
|
required: true,
|
|
nullable: false,
|
|
default_value: null,
|
|
fields: {},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
to: {
|
|
type: 'string',
|
|
description: 'Quote currency',
|
|
required: false,
|
|
nullable: false,
|
|
default_value: 'EUR',
|
|
fields: {},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
output_schema: {
|
|
type: 'object',
|
|
description: null,
|
|
required: false,
|
|
nullable: false,
|
|
default_value: null,
|
|
fields: {
|
|
rates: {
|
|
type: 'object',
|
|
description: 'Rates by date',
|
|
required: true,
|
|
nullable: false,
|
|
default_value: null,
|
|
fields: {},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
input_mapping: {
|
|
rules: [
|
|
{ source: '$.mcp.from', target: '$.request.query.from' },
|
|
],
|
|
},
|
|
output_mapping: {
|
|
rules: [
|
|
{ source: '$.response.body.rates', target: '$.output.rates' },
|
|
],
|
|
},
|
|
execution_config: {
|
|
timeout_ms: 9000,
|
|
retry_policy: { max_attempts: 2 },
|
|
auth_profile_ref: null,
|
|
headers: {},
|
|
protocol_options: null,
|
|
streaming: null,
|
|
},
|
|
tool_description: {
|
|
title: 'Получить историю курсов за месяц',
|
|
description: 'Возвращает историю курсов валют Frankfurter за заданный период.',
|
|
tags: [],
|
|
examples: [
|
|
{
|
|
input: {
|
|
from: 'USD',
|
|
to: 'EUR',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
wizard_state: {
|
|
input_sample: {
|
|
from: 'USD',
|
|
to: 'EUR',
|
|
},
|
|
output_sample: {
|
|
rates: {
|
|
'2024-01': {
|
|
EUR: 0.91,
|
|
},
|
|
},
|
|
},
|
|
test_input: {
|
|
from: 'GBP',
|
|
to: 'USD',
|
|
},
|
|
import_findings: [
|
|
{
|
|
severity: 'warning',
|
|
code: 'openapi_import.weak_tool_description',
|
|
message: 'Описание инструмента слишком короткое или техническое.',
|
|
suggested_action: 'Уточните, когда агент должен вызывать инструмент.',
|
|
field_path: 'GET /rates',
|
|
},
|
|
],
|
|
},
|
|
samples: [],
|
|
generated_draft: null,
|
|
config_export: null,
|
|
},
|
|
}),
|
|
});
|
|
},
|
|
);
|
|
|
|
await page.goto(`/wizard/?mode=edit&operationId=${operationId}`);
|
|
|
|
await expect(page.locator('#tool-name')).toHaveValue(operationId);
|
|
await expect(page.locator('#tool-display-name')).toHaveValue('Frankfurter monthly rates');
|
|
await expect(page.locator('#new-upstream-url')).toHaveValue('https://api.frankfurter.app');
|
|
await expect(page.locator('#new-upstream-static-headers')).toHaveValue(/"Accept": "application\/json"/);
|
|
await expect(page.locator('#new-upstream-static-headers')).toHaveValue(/"X-Demo": "frankfurter"/);
|
|
await expect(page.locator('#endpoint-path')).toHaveValue('/2024-01..2024-03');
|
|
await expect(page.locator('.method-card.active')).toHaveAttribute('data-method', 'GET');
|
|
await expect(page.locator('#tool-title')).toHaveValue('Получить историю курсов за месяц');
|
|
await expect(page.locator('#tool-description')).toHaveValue('Возвращает историю курсов валют Frankfurter за заданный период.');
|
|
await expect(page.locator('#tool-input-schema')).toHaveValue(/"from"/);
|
|
await expect(page.locator('#tool-input-schema')).toHaveValue(/"to"/);
|
|
await expect(page.locator('#tool-input-schema')).not.toHaveValue(/first_name/);
|
|
await expect(page.locator('#tool-output-schema')).toHaveValue(/"rates"/);
|
|
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.from/);
|
|
await expect(page.locator('#tool-output-mapping')).toHaveValue(/rates: \$\.response\.body\.rates/);
|
|
await expect(page.locator('#tool-exec-config')).toHaveValue(/9000/);
|
|
await expect(page.locator('#tool-exec-config')).toHaveValue(/max_attempts: 2/);
|
|
await expect(page.locator('#wizard-input-sample')).toHaveValue(/"from": "USD"/);
|
|
await expect(page.locator('#wizard-input-sample')).toHaveValue(/"to": "EUR"/);
|
|
await expect(page.locator('#wizard-input-sample')).not.toHaveValue(/Ada/);
|
|
await expect(page.locator('#wizard-output-sample')).toHaveValue(/"rates":/);
|
|
await expect(page.locator('#wizard-output-sample')).toHaveValue(/0\.91/);
|
|
await expect(page.locator('#wizard-test-input')).toHaveValue(/"from": "GBP"/);
|
|
await expect(page.locator('#wizard-test-input')).toHaveValue(/"to": "USD"/);
|
|
await expect(page.locator('#wizard-test-input')).not.toHaveValue(/Ada/);
|
|
|
|
await page.evaluate(() => window.CrankWizardShell.doGoToStep(5));
|
|
await expect(page.locator('#wizard-quality-findings')).toContainText('Описание инструмента слишком короткое');
|
|
await expect(page.locator('#wizard-quality-findings')).toContainText('GET /rates');
|
|
await page.getByRole('button', { name: 'Перейти к описанию' }).click();
|
|
await expect(page.locator('#step-panel-4')).toBeVisible();
|
|
await expect(page.locator('#tool-description')).toBeFocused();
|
|
});
|
|
|
|
test('wizard shows agent-facing MCP preview from current draft fields', async ({ page }) => {
|
|
await login(page);
|
|
const workspace = await getCurrentWorkspace(page);
|
|
const operationId = 'frankfurter_monthly_rates';
|
|
const version = 7;
|
|
|
|
await page.route(
|
|
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}`,
|
|
async (route) => {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
id: operationId,
|
|
workspace_id: workspace.id,
|
|
name: operationId,
|
|
display_name: 'Frankfurter monthly rates',
|
|
category: 'finance',
|
|
protocol: 'rest',
|
|
security_level: 'standard',
|
|
status: 'draft',
|
|
current_draft_version: version,
|
|
latest_published_version: null,
|
|
created_at: '2026-06-19T00:00:00Z',
|
|
updated_at: '2026-06-19T00:00:00Z',
|
|
published_at: null,
|
|
draft_version_ref: { version, status: 'draft' },
|
|
published_version_ref: null,
|
|
agent_refs: [],
|
|
}),
|
|
});
|
|
},
|
|
);
|
|
|
|
await page.route(
|
|
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}/versions/${version}`,
|
|
async (route) => {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
operation_id: operationId,
|
|
workspace_id: workspace.id,
|
|
version,
|
|
status: 'draft',
|
|
change_note: null,
|
|
created_at: '2026-06-19T00:00:00Z',
|
|
created_by: null,
|
|
snapshot: {
|
|
id: operationId,
|
|
name: operationId,
|
|
display_name: 'Frankfurter monthly rates',
|
|
category: 'finance',
|
|
protocol: 'rest',
|
|
security_level: 'standard',
|
|
status: 'draft',
|
|
version,
|
|
target: {
|
|
kind: 'rest',
|
|
base_url: 'https://api.frankfurter.app',
|
|
method: 'GET',
|
|
path_template: '/2024-01..2024-03',
|
|
static_headers: { Accept: 'application/json' },
|
|
},
|
|
input_schema: {
|
|
type: 'object',
|
|
description: null,
|
|
required: false,
|
|
nullable: false,
|
|
default_value: null,
|
|
fields: {
|
|
from: {
|
|
type: 'string',
|
|
description: 'Base currency',
|
|
required: true,
|
|
nullable: false,
|
|
default_value: 'USD',
|
|
fields: {},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
to: {
|
|
type: 'string',
|
|
description: 'Quote currency',
|
|
required: false,
|
|
nullable: false,
|
|
default_value: 'EUR',
|
|
fields: {},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
output_schema: {
|
|
type: 'object',
|
|
description: null,
|
|
required: false,
|
|
nullable: false,
|
|
default_value: null,
|
|
fields: {
|
|
rates: {
|
|
type: 'object',
|
|
description: 'Rates by date',
|
|
required: true,
|
|
nullable: false,
|
|
default_value: null,
|
|
fields: {},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
input_mapping: { rules: [] },
|
|
output_mapping: {
|
|
rules: [{ source: '$.response.body.rates', target: '$.output.rates' }],
|
|
},
|
|
execution_config: {
|
|
timeout_ms: 9000,
|
|
retry_policy: null,
|
|
auth_profile_ref: null,
|
|
headers: {},
|
|
protocol_options: null,
|
|
streaming: null,
|
|
approval_policy: {
|
|
required: true,
|
|
risk_level: 'financial',
|
|
ttl_seconds: 180,
|
|
show_payload_preview: true,
|
|
payload_preview_mode: 'masked_json',
|
|
},
|
|
},
|
|
tool_description: {
|
|
title: 'Получить историю курсов за месяц',
|
|
description: 'Возвращает историю курсов валют Frankfurter за заданный период.',
|
|
tags: [],
|
|
examples: [],
|
|
},
|
|
wizard_state: {
|
|
input_sample: { from: 'USD', to: 'EUR' },
|
|
output_sample: { rates: { '2024-01': { EUR: 0.91 } } },
|
|
test_input: { from: 'GBP', to: 'USD' },
|
|
},
|
|
samples: [],
|
|
generated_draft: null,
|
|
config_export: null,
|
|
},
|
|
}),
|
|
});
|
|
},
|
|
);
|
|
|
|
await page.route(
|
|
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/analyze-quality`,
|
|
async (route) => {
|
|
const payload = route.request().postDataJSON();
|
|
expect(payload.output_mapping.rules.length).toBeGreaterThan(0);
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
blocking: false,
|
|
findings: [
|
|
{
|
|
severity: 'warning',
|
|
code: 'response_projection_full_body',
|
|
message: 'Результат инструмента отдает агенту весь ответ API.',
|
|
suggested_action: 'Выберите только поля, которые нужны агенту.',
|
|
field_path: 'output_mapping.rules.0.source',
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
},
|
|
);
|
|
|
|
await page.goto(`/wizard/?mode=edit&operationId=${operationId}`);
|
|
await page.locator('#btn-continue').click();
|
|
await page.locator('#btn-continue').click();
|
|
await page.locator('#btn-continue').click();
|
|
await page.locator('#btn-continue').click();
|
|
|
|
await expect(page.locator('#agent-preview-tool-name')).toHaveText(operationId);
|
|
await expect(page.locator('#agent-preview-tool-description')).toContainText('Frankfurter');
|
|
await expect(page.locator('#agent-preview-input-schema')).toHaveValue(/"from"/);
|
|
await expect(page.locator('#agent-preview-tool-call')).toHaveValue(/"name": "frankfurter_monthly_rates"/);
|
|
await expect(page.locator('#agent-preview-tool-call')).toHaveValue(/"from": "USD"/);
|
|
await expect(page.locator('#agent-preview-success-response')).toHaveValue(/0\.91/);
|
|
await page.evaluate(() => window.CrankWizardShell.doGoToStep(5));
|
|
await expect(page.locator('#step-panel-5')).toBeVisible();
|
|
await page.locator('#wizard-run-quality').scrollIntoViewIfNeeded();
|
|
await page.locator('#wizard-run-quality').click();
|
|
await expect(page.locator('#wizard-quality-findings')).toContainText(/весь ответ API|full response/i);
|
|
await expect(page.locator('#wizard-quality-findings')).toContainText('output_mapping.rules.0.source');
|
|
});
|
|
|
|
test('wizard edit mode preserves explicit request mapping targets on save', async ({ page }) => {
|
|
await login(page);
|
|
const workspace = await getCurrentWorkspace(page);
|
|
const operationId = 'frankfurter_latest_rate';
|
|
const version = 3;
|
|
let updatePayload = null;
|
|
|
|
await page.route(
|
|
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}`,
|
|
async (route) => {
|
|
if (route.request().method() === 'PATCH') {
|
|
updatePayload = route.request().postDataJSON();
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
operation_id: operationId,
|
|
workspace_id: workspace.id,
|
|
version,
|
|
status: 'draft',
|
|
updated_at: '2026-06-19T00:00:00Z',
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
id: operationId,
|
|
workspace_id: workspace.id,
|
|
name: operationId,
|
|
display_name: 'Frankfurter latest rate',
|
|
category: 'finance',
|
|
protocol: 'rest',
|
|
security_level: 'standard',
|
|
status: 'draft',
|
|
current_draft_version: version,
|
|
latest_published_version: null,
|
|
created_at: '2026-06-19T00:00:00Z',
|
|
updated_at: '2026-06-19T00:00:00Z',
|
|
published_at: null,
|
|
draft_version_ref: { version, status: 'draft' },
|
|
published_version_ref: null,
|
|
agent_refs: [],
|
|
}),
|
|
});
|
|
},
|
|
);
|
|
|
|
await page.route(
|
|
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}/versions/${version}`,
|
|
async (route) => {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
operation_id: operationId,
|
|
workspace_id: workspace.id,
|
|
version,
|
|
status: 'draft',
|
|
change_note: null,
|
|
created_at: '2026-06-19T00:00:00Z',
|
|
created_by: null,
|
|
snapshot: {
|
|
id: operationId,
|
|
name: operationId,
|
|
display_name: 'Frankfurter latest rate',
|
|
category: 'finance',
|
|
protocol: 'rest',
|
|
security_level: 'standard',
|
|
status: 'draft',
|
|
version,
|
|
target: {
|
|
kind: 'rest',
|
|
base_url: 'https://api.frankfurter.app',
|
|
method: 'GET',
|
|
path_template: '/latest',
|
|
static_headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
},
|
|
input_schema: {
|
|
type: 'object',
|
|
description: null,
|
|
required: false,
|
|
nullable: false,
|
|
default_value: null,
|
|
fields: {
|
|
base: {
|
|
type: 'string',
|
|
description: null,
|
|
required: true,
|
|
nullable: false,
|
|
default_value: null,
|
|
fields: {},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
symbols: {
|
|
type: 'string',
|
|
description: 'Quote symbols',
|
|
required: false,
|
|
nullable: false,
|
|
default_value: 'EUR',
|
|
fields: {},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
output_schema: {
|
|
type: 'object',
|
|
description: null,
|
|
required: false,
|
|
nullable: false,
|
|
default_value: null,
|
|
fields: {},
|
|
items: null,
|
|
enum_values: [],
|
|
variants: [],
|
|
},
|
|
input_mapping: {
|
|
rules: [
|
|
{ source: '$.mcp.base', target: '$.request.query.base', required: true },
|
|
{ source: '$.mcp.symbols', target: '$.request.query.symbols', required: true },
|
|
{ source: '$.mcp.date', target: '$.request.path.date', required: true },
|
|
{
|
|
source: '$.mcp.group',
|
|
target: '$.request.query.group',
|
|
required: false,
|
|
default_value: 'month',
|
|
transform: { kind: 'to_string' },
|
|
},
|
|
],
|
|
},
|
|
output_mapping: {
|
|
rules: [
|
|
{ source: '$.response.body.base', target: '$.output.base', required: true },
|
|
],
|
|
},
|
|
execution_config: {
|
|
timeout_ms: 9000,
|
|
retry_policy: null,
|
|
auth_profile_ref: null,
|
|
headers: {},
|
|
protocol_options: null,
|
|
streaming: null,
|
|
approval_policy: {
|
|
required: true,
|
|
risk_level: 'financial',
|
|
ttl_seconds: 180,
|
|
show_payload_preview: true,
|
|
payload_preview_mode: 'masked_json',
|
|
},
|
|
},
|
|
tool_description: {
|
|
title: 'Получить последний курс',
|
|
description: 'Возвращает последний курс.',
|
|
tags: ['finance'],
|
|
examples: [{ input: { base: 'USD', symbols: 'EUR' } }],
|
|
},
|
|
wizard_state: {
|
|
input_sample: {
|
|
base: 'USD',
|
|
symbols: 'EUR',
|
|
},
|
|
output_sample: {
|
|
base: 'USD',
|
|
rates: {
|
|
EUR: 0.91,
|
|
},
|
|
},
|
|
test_input: {
|
|
base: 'CHF',
|
|
symbols: 'JPY',
|
|
},
|
|
import_findings: [
|
|
{
|
|
severity: 'warning',
|
|
code: 'openapi_import.weak_tool_description',
|
|
message: 'Описание инструмента слишком короткое или техническое.',
|
|
suggested_action: 'Уточните описание.',
|
|
field_path: 'GET /latest',
|
|
},
|
|
],
|
|
},
|
|
samples: [],
|
|
generated_draft: null,
|
|
config_export: null,
|
|
},
|
|
}),
|
|
});
|
|
},
|
|
);
|
|
|
|
await page.goto(`/wizard/?mode=edit&operationId=${operationId}`);
|
|
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.base/);
|
|
await expect(page.locator('#tool-input-mapping')).toHaveValue(/path\.date/);
|
|
await expect(page.locator('#tool-input-mapping')).toHaveValue(/transform: to_string/);
|
|
await page.evaluate(() => window.CrankWizardShell.doGoToStep(3));
|
|
await expect(page.locator('#approval-required')).toBeChecked();
|
|
await expect(page.locator('#approval-config-fields')).toBeVisible();
|
|
await expect(page.locator('#approval-mode')).toHaveValue('custom');
|
|
await expect(page.locator('#approval-risk-level')).toHaveCount(0);
|
|
await expect(page.locator('#approval-ttl-seconds')).toHaveValue('180');
|
|
await expect(page.locator('#approval-payload-preview-mode')).toHaveValue('masked_json');
|
|
await page.locator('.btn-save-draft').click();
|
|
await expect.poll(() => updatePayload).not.toBeNull();
|
|
|
|
expect(updatePayload.display_name).toBe('Frankfurter latest rate');
|
|
expect(updatePayload.target).toEqual({
|
|
kind: 'rest',
|
|
base_url: 'https://api.frankfurter.app',
|
|
method: 'GET',
|
|
path_template: '/latest',
|
|
static_headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
});
|
|
expect(updatePayload.input_schema.fields).toHaveProperty('base');
|
|
expect(updatePayload.input_schema.fields).toHaveProperty('symbols');
|
|
expect(updatePayload.output_mapping.rules).toEqual([
|
|
{ source: '$.response.body.base', target: '$.output.base', required: true },
|
|
]);
|
|
expect(updatePayload.execution_config).toEqual({
|
|
timeout_ms: 9000,
|
|
retry_policy: null,
|
|
auth_profile_ref: null,
|
|
headers: {},
|
|
protocol_options: null,
|
|
streaming: null,
|
|
approval_policy: {
|
|
required: true,
|
|
mode: 'custom',
|
|
risk_level: 'normal',
|
|
ttl_seconds: 180,
|
|
show_payload_preview: true,
|
|
payload_preview_mode: 'masked_json',
|
|
elicitation_message: null,
|
|
},
|
|
});
|
|
expect(updatePayload.tool_description).toEqual({
|
|
title: 'Получить последний курс',
|
|
description: 'Возвращает последний курс.',
|
|
tags: ['finance'],
|
|
examples: [{ input: { base: 'USD', symbols: 'EUR' } }],
|
|
});
|
|
expect(updatePayload.wizard_state).toEqual({
|
|
input_sample: {
|
|
base: 'USD',
|
|
symbols: 'EUR',
|
|
},
|
|
output_sample: {
|
|
base: 'USD',
|
|
rates: {
|
|
EUR: 0.91,
|
|
},
|
|
},
|
|
test_input: {
|
|
base: 'CHF',
|
|
symbols: 'JPY',
|
|
},
|
|
import_findings: [
|
|
{
|
|
severity: 'warning',
|
|
code: 'openapi_import.weak_tool_description',
|
|
message: 'Описание инструмента слишком короткое или техническое.',
|
|
suggested_action: 'Уточните описание.',
|
|
field_path: 'GET /latest',
|
|
},
|
|
],
|
|
});
|
|
expect(updatePayload.input_mapping.rules).toEqual([
|
|
{ source: '$.mcp.base', target: '$.request.query.base', required: true },
|
|
{ source: '$.mcp.symbols', target: '$.request.query.symbols', required: true },
|
|
{ source: '$.mcp.date', target: '$.request.path.date', required: true },
|
|
{
|
|
source: '$.mcp.group',
|
|
target: '$.request.query.group',
|
|
required: false,
|
|
default_value: 'month',
|
|
transform: { kind: 'to_string' },
|
|
},
|
|
]);
|
|
});
|