ui: polish websocket wizard test runs
This commit is contained in:
@@ -2,20 +2,17 @@
|
|||||||
|
|
||||||
## Current
|
## Current
|
||||||
|
|
||||||
### `feat/websocket-test-run-polish`
|
### `feat/live-authenticated-staging-entry`
|
||||||
|
|
||||||
Status: ready
|
Status: ready
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
- websocket test-run flow clearly reports bounded results, reconnect/error states, and final status
|
- authenticated staging flow runs against the real deployed environment
|
||||||
- operator-facing UI reflects websocket-specific runtime semantics instead of generic streaming copy
|
- operator docs match the current deploy/runtime model
|
||||||
- targeted UI checks cover websocket wizard/test-run behavior
|
- staging validation is captured without relying on local fixtures
|
||||||
|
|
||||||
## Next
|
|
||||||
|
|
||||||
- `feat/live-authenticated-staging-entry`
|
|
||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
|
|
||||||
|
- `feat/websocket-test-run-polish`
|
||||||
- `feat/frontend-wizard-modularization`
|
- `feat/frontend-wizard-modularization`
|
||||||
- `feat/distributed-mcp-session-store`
|
- `feat/distributed-mcp-session-store`
|
||||||
|
|||||||
@@ -840,6 +840,19 @@ var TRANSLATIONS = {
|
|||||||
'wizard.test.window_truncated_note': 'Results were truncated.',
|
'wizard.test.window_truncated_note': 'Results were truncated.',
|
||||||
'wizard.test.window_more_note': 'More data is available if you continue from the streaming views.',
|
'wizard.test.window_more_note': 'More data is available if you continue from the streaming views.',
|
||||||
'wizard.test.window_incomplete_note': 'The upstream stream ended before the configured window was fully collected.',
|
'wizard.test.window_incomplete_note': 'The upstream stream ended before the configured window was fully collected.',
|
||||||
|
'wizard.test.websocket_window_completed': 'WebSocket window test completed',
|
||||||
|
'wizard.test.websocket_window_completed_body': 'Collected {items} WebSocket event(s) inside the bounded test window.',
|
||||||
|
'wizard.test.websocket_window_truncated_note': 'The bounded window stopped after reaching the configured item or byte limit.',
|
||||||
|
'wizard.test.websocket_window_more_note': 'The socket produced more events than this test window retained. Increase limits or switch to session mode if you need a longer stream.',
|
||||||
|
'wizard.test.websocket_window_incomplete_note': 'The upstream socket closed before the bounded window finished collecting all configured events.',
|
||||||
|
'wizard.test.websocket_session_started': 'WebSocket session test started',
|
||||||
|
'wizard.test.websocket_session_started_body': 'WebSocket session {session_id} was created. Continue from Stream sessions after a short delay.',
|
||||||
|
'wizard.test.websocket_async_job_started': 'WebSocket async job test started',
|
||||||
|
'wizard.test.websocket_async_job_started_body': 'WebSocket async job {job_id} was created. Continue from the async jobs view for status and result.',
|
||||||
|
'wizard.test.websocket_failed': 'WebSocket test returned errors',
|
||||||
|
'wizard.test.websocket_failed_body': 'The socket test failed before Crank could collect a bounded result window.',
|
||||||
|
'wizard.test.websocket_failed_reconnect_body': 'The upstream socket closed before enough events were collected, and the configured reconnect policy was exhausted.',
|
||||||
|
'wizard.test.websocket_failed_closed_body': 'The upstream socket closed before the bounded test window completed.',
|
||||||
'wizard.test.session_started': 'Session test started',
|
'wizard.test.session_started': 'Session test started',
|
||||||
'wizard.test.session_started_body': 'Session {session_id} was created. Check Stream sessions in a few seconds.',
|
'wizard.test.session_started_body': 'Session {session_id} was created. Check Stream sessions in a few seconds.',
|
||||||
'wizard.test.async_job_started': 'Async job test started',
|
'wizard.test.async_job_started': 'Async job test started',
|
||||||
@@ -1853,6 +1866,19 @@ var TRANSLATIONS = {
|
|||||||
'wizard.test.window_truncated_note': 'Результат был усечен.',
|
'wizard.test.window_truncated_note': 'Результат был усечен.',
|
||||||
'wizard.test.window_more_note': 'Доступно продолжение, если перейти к streaming-представлениям.',
|
'wizard.test.window_more_note': 'Доступно продолжение, если перейти к streaming-представлениям.',
|
||||||
'wizard.test.window_incomplete_note': 'Upstream-поток завершился раньше, чем удалось собрать всё окно.',
|
'wizard.test.window_incomplete_note': 'Upstream-поток завершился раньше, чем удалось собрать всё окно.',
|
||||||
|
'wizard.test.websocket_window_completed': 'WebSocket оконный тест завершен',
|
||||||
|
'wizard.test.websocket_window_completed_body': 'В пределах ограниченного тестового окна собрано {items} WebSocket event(s).',
|
||||||
|
'wizard.test.websocket_window_truncated_note': 'Ограниченное окно остановилось после достижения лимита по элементам или байтам.',
|
||||||
|
'wizard.test.websocket_window_more_note': 'Сокет выдал больше событий, чем сохранило это тестовое окно. Увеличьте лимиты или переключитесь в session mode, если нужен более длинный поток.',
|
||||||
|
'wizard.test.websocket_window_incomplete_note': 'Upstream WebSocket закрылся до того, как bounded окно успело собрать все настроенные события.',
|
||||||
|
'wizard.test.websocket_session_started': 'WebSocket session-тест запущен',
|
||||||
|
'wizard.test.websocket_session_started_body': 'WebSocket session {session_id} создана. Продолжайте в разделе Stream sessions через короткую паузу.',
|
||||||
|
'wizard.test.websocket_async_job_started': 'WebSocket async job тест запущен',
|
||||||
|
'wizard.test.websocket_async_job_started_body': 'Создан WebSocket async job {job_id}. Дальше отслеживайте статус и результат в представлении async jobs.',
|
||||||
|
'wizard.test.websocket_failed': 'WebSocket тест завершился с ошибками',
|
||||||
|
'wizard.test.websocket_failed_body': 'Тест сокета завершился до того, как Crank успел собрать bounded result window.',
|
||||||
|
'wizard.test.websocket_failed_reconnect_body': 'Upstream WebSocket закрылся раньше, чем удалось собрать достаточно событий, и настроенная reconnect policy была исчерпана.',
|
||||||
|
'wizard.test.websocket_failed_closed_body': 'Upstream WebSocket закрылся до завершения bounded test window.',
|
||||||
'wizard.test.session_started': 'Session-тест запущен',
|
'wizard.test.session_started': 'Session-тест запущен',
|
||||||
'wizard.test.session_started_body': 'Сессия {session_id} создана. Проверьте статус в разделе Stream sessions через несколько секунд.',
|
'wizard.test.session_started_body': 'Сессия {session_id} создана. Проверьте статус в разделе Stream sessions через несколько секунд.',
|
||||||
'wizard.test.async_job_started': 'Async job тест запущен',
|
'wizard.test.async_job_started': 'Async job тест запущен',
|
||||||
|
|||||||
@@ -390,6 +390,68 @@ function setTextareaValue(id, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function describeWizardTestResult(result) {
|
function describeWizardTestResult(result) {
|
||||||
|
if (wizardProtocol === 'websocket') {
|
||||||
|
if (!result.ok && result.errors && result.errors.length) {
|
||||||
|
var websocketError = result.errors[0] || {};
|
||||||
|
var websocketMessage = websocketError.message || '';
|
||||||
|
var websocketBody = tKey('wizard.test.websocket_failed_body');
|
||||||
|
if (/reconnect policy exhausted/i.test(websocketMessage)) {
|
||||||
|
websocketBody = tKey('wizard.test.websocket_failed_reconnect_body');
|
||||||
|
} else if (/close frame before collection completed|closed before collection completed/i.test(websocketMessage)) {
|
||||||
|
websocketBody = tKey('wizard.test.websocket_failed_closed_body');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: tKey('wizard.test.websocket_failed'),
|
||||||
|
body: websocketBody,
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.stream_session && result.stream_session.session_id) {
|
||||||
|
return {
|
||||||
|
title: tKey('wizard.test.websocket_session_started'),
|
||||||
|
body: tfKey('wizard.test.websocket_session_started_body', {
|
||||||
|
session_id: result.stream_session.session_id,
|
||||||
|
poll_after_ms: result.stream_session.poll_after_ms,
|
||||||
|
}),
|
||||||
|
isError: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.async_job && result.async_job.job_id) {
|
||||||
|
return {
|
||||||
|
title: tKey('wizard.test.websocket_async_job_started'),
|
||||||
|
body: tfKey('wizard.test.websocket_async_job_started_body', {
|
||||||
|
job_id: result.async_job.job_id,
|
||||||
|
}),
|
||||||
|
isError: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.window) {
|
||||||
|
var itemCount = Array.isArray(result.response_preview && result.response_preview.items)
|
||||||
|
? result.response_preview.items.length
|
||||||
|
: 0;
|
||||||
|
var websocketBodyParts = [
|
||||||
|
tfKey('wizard.test.websocket_window_completed_body', { items: itemCount })
|
||||||
|
];
|
||||||
|
if (result.window.truncated) {
|
||||||
|
websocketBodyParts.push(tKey('wizard.test.websocket_window_truncated_note'));
|
||||||
|
}
|
||||||
|
if (result.window.has_more) {
|
||||||
|
websocketBodyParts.push(tKey('wizard.test.websocket_window_more_note'));
|
||||||
|
}
|
||||||
|
if (!result.window.window_complete) {
|
||||||
|
websocketBodyParts.push(tKey('wizard.test.websocket_window_incomplete_note'));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: result.ok ? tKey('wizard.test.websocket_window_completed') : tKey('wizard.test.websocket_failed'),
|
||||||
|
body: websocketBodyParts.join(' '),
|
||||||
|
isError: !result.ok,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (result.stream_session && result.stream_session.session_id) {
|
if (result.stream_session && result.stream_session.session_id) {
|
||||||
return {
|
return {
|
||||||
title: tKey('wizard.test.session_started'),
|
title: tKey('wizard.test.session_started'),
|
||||||
|
|||||||
@@ -213,6 +213,74 @@ function buildRestWindowOperationPayload(name) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildWebsocketWindowOperationPayload(name, overrides = {}) {
|
||||||
|
const streaming = Object.assign({
|
||||||
|
mode: 'window',
|
||||||
|
transport_behavior: 'server_stream',
|
||||||
|
window_duration_ms: 1000,
|
||||||
|
upstream_timeout_ms: 1000,
|
||||||
|
max_items: 3,
|
||||||
|
max_bytes: 16384,
|
||||||
|
aggregation_mode: 'summary_plus_samples',
|
||||||
|
items_path: '$.items',
|
||||||
|
done_path: '$.done',
|
||||||
|
redacted_paths: [],
|
||||||
|
truncate_item_fields: false,
|
||||||
|
drop_duplicates: false,
|
||||||
|
tool_family: {},
|
||||||
|
}, overrides.streaming || {});
|
||||||
|
|
||||||
|
const websocketOptions = Object.assign({
|
||||||
|
heartbeat_interval_ms: 250,
|
||||||
|
reconnect_max_attempts: 1,
|
||||||
|
reconnect_backoff_ms: 10,
|
||||||
|
}, overrides.websocketOptions || {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
display_name: 'Playwright WebSocket Window',
|
||||||
|
category: 'streaming',
|
||||||
|
protocol: 'websocket',
|
||||||
|
target: {
|
||||||
|
kind: 'websocket',
|
||||||
|
url: `ws://127.0.0.1:${STREAM_FIXTURE_PORT}/events`,
|
||||||
|
subprotocols: [],
|
||||||
|
subscribe_message_template: null,
|
||||||
|
unsubscribe_message_template: null,
|
||||||
|
static_headers: {},
|
||||||
|
},
|
||||||
|
input_schema: schemaObject({
|
||||||
|
topic: schemaString(false),
|
||||||
|
}),
|
||||||
|
output_schema: schemaObject({}),
|
||||||
|
input_mapping: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
source: '$.mcp.topic',
|
||||||
|
target: '$.request.body.topic',
|
||||||
|
required: false,
|
||||||
|
default_value: 'telemetry',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
output_mapping: { rules: [] },
|
||||||
|
execution_config: {
|
||||||
|
timeout_ms: 1000,
|
||||||
|
headers: {},
|
||||||
|
protocol_options: {
|
||||||
|
websocket: websocketOptions,
|
||||||
|
},
|
||||||
|
streaming,
|
||||||
|
},
|
||||||
|
tool_description: {
|
||||||
|
title: 'Playwright WebSocket Window',
|
||||||
|
description: 'Collects a bounded WebSocket event window from the local fixture.',
|
||||||
|
tags: ['playwright', 'streaming', 'websocket', 'window'],
|
||||||
|
examples: [{ input: {} }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const SOAP_TEST_WSDL = `<?xml version="1.0"?>
|
const SOAP_TEST_WSDL = `<?xml version="1.0"?>
|
||||||
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
|
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
|
||||||
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
|
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
|
||||||
@@ -558,6 +626,7 @@ module.exports = {
|
|||||||
buildRestAsyncJobOperationPayload,
|
buildRestAsyncJobOperationPayload,
|
||||||
buildRestWindowOperationPayload,
|
buildRestWindowOperationPayload,
|
||||||
buildSoapOperationPayload,
|
buildSoapOperationPayload,
|
||||||
|
buildWebsocketWindowOperationPayload,
|
||||||
createAgent,
|
createAgent,
|
||||||
createOperation,
|
createOperation,
|
||||||
createPlatformApiKey,
|
createPlatformApiKey,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const { test, expect } = require('@playwright/test');
|
|||||||
const {
|
const {
|
||||||
SOAP_TEST_WSDL,
|
SOAP_TEST_WSDL,
|
||||||
buildSoapOperationPayload,
|
buildSoapOperationPayload,
|
||||||
|
buildWebsocketWindowOperationPayload,
|
||||||
createOperation,
|
createOperation,
|
||||||
getCurrentWorkspace,
|
getCurrentWorkspace,
|
||||||
login,
|
login,
|
||||||
@@ -96,3 +97,124 @@ test('soap wizard uploads wsdl, applies discovered binding and runs test', async
|
|||||||
await expect(page.locator('#wizard-test-response-preview')).toHaveValue(/lead_soap_123/);
|
await expect(page.locator('#wizard-test-response-preview')).toHaveValue(/lead_soap_123/);
|
||||||
await expect(page.locator('#wizard-test-errors')).toHaveValue('[]');
|
await expect(page.locator('#wizard-test-errors')).toHaveValue('[]');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('websocket wizard reports bounded window success and reconnect exhaustion clearly', async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
const workspace = await getCurrentWorkspace(page);
|
||||||
|
const successOperation = await createOperation(
|
||||||
|
page,
|
||||||
|
workspace.id,
|
||||||
|
buildWebsocketWindowOperationPayload(uniqueName('playwright_websocket_success')),
|
||||||
|
);
|
||||||
|
const failureOperation = await createOperation(
|
||||||
|
page,
|
||||||
|
workspace.id,
|
||||||
|
buildWebsocketWindowOperationPayload(uniqueName('playwright_websocket_failure'), {
|
||||||
|
streaming: {
|
||||||
|
max_items: 5,
|
||||||
|
},
|
||||||
|
websocketOptions: {
|
||||||
|
reconnect_max_attempts: 0,
|
||||||
|
reconnect_backoff_ms: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto(`/wizard/?mode=edit&operationId=${encodeURIComponent(successOperation.operation_id)}`);
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
await page.locator('#upstream-new-trigger').click();
|
||||||
|
await page.locator('#new-upstream-name').fill('fixture-websocket');
|
||||||
|
await page.locator('#new-upstream-url').fill('ws://127.0.0.1:3310');
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
await expect(page.locator('#step-panel-3-websocket .step-panel-title')).toContainText(/WebSocket/i);
|
||||||
|
await page.locator('#websocket-path').fill('/events');
|
||||||
|
await page.locator('#websocket-subprotocols').fill('');
|
||||||
|
await page.locator('#websocket-subscribe-template').fill('');
|
||||||
|
await page.locator('#websocket-unsubscribe-template').fill('');
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
await page.locator('#wizard-test-input').fill('{}');
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const original = window.CrankApi.runOperationTest;
|
||||||
|
window.__wizardRunTestOriginal = original;
|
||||||
|
window.CrankApi.runOperationTest = async () => ({
|
||||||
|
ok: true,
|
||||||
|
mode: 'window',
|
||||||
|
request_preview: {
|
||||||
|
body: { topic: 'telemetry' },
|
||||||
|
},
|
||||||
|
response_preview: {
|
||||||
|
items: [
|
||||||
|
{ seq: 1, value: 101 },
|
||||||
|
{ seq: 2, value: 102 },
|
||||||
|
{ seq: 3, value: 103 },
|
||||||
|
],
|
||||||
|
window_complete: true,
|
||||||
|
truncated: false,
|
||||||
|
has_more: false,
|
||||||
|
},
|
||||||
|
errors: [],
|
||||||
|
window: {
|
||||||
|
window_complete: true,
|
||||||
|
truncated: false,
|
||||||
|
has_more: false,
|
||||||
|
cursor: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await page.locator('#wizard-run-test').click();
|
||||||
|
await expect(page.locator('#wizard-live-status-title')).toContainText(localized('WebSocket window test completed', 'WebSocket оконный тест завершен'));
|
||||||
|
await expect(page.locator('#wizard-live-status-text')).toContainText(localized('bounded test window', 'тестового окна'));
|
||||||
|
await expect(page.locator('#wizard-test-response-preview')).toHaveValue(/"seq": 1/);
|
||||||
|
await expect(page.locator('#wizard-test-request-preview')).toHaveValue(/"topic": "telemetry"/);
|
||||||
|
await expect(page.locator('#wizard-test-errors')).toHaveValue('[]');
|
||||||
|
await page.evaluate(() => {
|
||||||
|
if (window.__wizardRunTestOriginal) {
|
||||||
|
window.CrankApi.runOperationTest = window.__wizardRunTestOriginal;
|
||||||
|
delete window.__wizardRunTestOriginal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/wizard/?mode=edit&operationId=${encodeURIComponent(failureOperation.operation_id)}`);
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
await page.locator('#upstream-new-trigger').click();
|
||||||
|
await page.locator('#new-upstream-name').fill('fixture-websocket');
|
||||||
|
await page.locator('#new-upstream-url').fill('ws://127.0.0.1:3310');
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
await page.locator('#websocket-path').fill('/events');
|
||||||
|
await page.locator('#websocket-subprotocols').fill('');
|
||||||
|
await page.locator('#websocket-subscribe-template').fill('');
|
||||||
|
await page.locator('#websocket-unsubscribe-template').fill('');
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
await page.locator('#wizard-test-input').fill('{}');
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const original = window.CrankApi.runOperationTest;
|
||||||
|
window.__wizardRunTestOriginal = original;
|
||||||
|
window.CrankApi.runOperationTest = async () => ({
|
||||||
|
ok: false,
|
||||||
|
mode: 'window',
|
||||||
|
request_preview: {
|
||||||
|
body: { topic: 'telemetry' },
|
||||||
|
},
|
||||||
|
response_preview: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
code: 'runtime_websocket_error',
|
||||||
|
message: 'websocket reconnect policy exhausted',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
window: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await page.locator('#wizard-run-test').click();
|
||||||
|
await expect(page.locator('#wizard-live-status-title')).toContainText(localized('WebSocket test returned errors', 'WebSocket тест завершился с ошибками'));
|
||||||
|
await expect(page.locator('#wizard-live-status-text')).toContainText(localized('reconnect policy was exhausted', 'reconnect policy была исчерпана'));
|
||||||
|
await expect(page.locator('#wizard-test-errors')).toHaveValue(/runtime_websocket_error/);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
if (window.__wizardRunTestOriginal) {
|
||||||
|
window.CrankApi.runOperationTest = window.__wizardRunTestOriginal;
|
||||||
|
delete window.__wizardRunTestOriginal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user