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 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 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', }, }, 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/); }); 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, }, 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', }, ], }, 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, }, 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', }, }, 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 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, }); 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', }, }); 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', }, ]); });