test: polish soap wizard test run flow
This commit is contained in:
@@ -2,36 +2,19 @@
|
|||||||
|
|
||||||
## Current
|
## Current
|
||||||
|
|
||||||
### `feat/streaming-test-run-results`
|
### `feat/websocket-test-run-polish`
|
||||||
|
|
||||||
Status: completed
|
Status: pending
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
- `POST /operations/{operation_id}/test-runs` returns mode-aware streaming metadata
|
- WebSocket wizard test-run shows reconnect / bounded-result semantics clearly
|
||||||
- window test-runs expose bounded result flags for the wizard status block
|
- WebSocket test-run flow is covered by local fixtures and Playwright
|
||||||
- session and async-job test-runs create persisted runtime state and return references for follow-up pages
|
- `TASKS.md` stays aligned with the actual streaming branch state
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
- `feat/soap-test-run-polish`
|
- `feat/live-authenticated-staging-entry`
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
- `feat/streaming-mcp-architecture`
|
- `feat/live-authenticated-staging-entry`
|
||||||
- `feat/mcp-streamable-http-alignment`
|
|
||||||
- `feat/streaming-core-model`
|
|
||||||
- `feat/stream-session-store`
|
|
||||||
- `feat/runtime-window-mode`
|
|
||||||
- `feat/rest-sse-adapter`
|
|
||||||
- `feat/grpc-server-streaming-adapter`
|
|
||||||
- `feat/session-and-job-tools`
|
|
||||||
- `feat/websocket-upstream-adapter`
|
|
||||||
- `feat/soap-architecture-and-core-model`
|
|
||||||
- `feat/soap-adapter-foundation`
|
|
||||||
- `feat/streaming-ui-config`
|
|
||||||
- `feat/streaming-e2e`
|
|
||||||
- `feat/auth-profile-secret-resolution`
|
|
||||||
- `feat/runtime-upstream-auth`
|
|
||||||
- `feat/secrets-ui`
|
|
||||||
- `feat/wizard-auth-selector`
|
|
||||||
- `feat/manual-regression-pass`
|
|
||||||
|
|||||||
@@ -1553,6 +1553,9 @@ function currentUpstream() {
|
|||||||
|
|
||||||
var customName = textValue('new-upstream-name');
|
var customName = textValue('new-upstream-name');
|
||||||
var customUrl = textValue('new-upstream-url');
|
var customUrl = textValue('new-upstream-url');
|
||||||
|
if (!customUrl && wizardProtocol === 'soap') {
|
||||||
|
customUrl = textValue('soap-endpoint-override');
|
||||||
|
}
|
||||||
if (!customUrl) return null;
|
if (!customUrl) return null;
|
||||||
|
|
||||||
var authMode = textValue('new-upstream-auth-mode') || 'none';
|
var authMode = textValue('new-upstream-auth-mode') || 'none';
|
||||||
|
|||||||
@@ -103,6 +103,59 @@ const server = http.createServer((request, response) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/soap/leads' && request.method === 'POST') {
|
||||||
|
let buffer = '';
|
||||||
|
request.setEncoding('utf8');
|
||||||
|
request.on('data', (chunk) => {
|
||||||
|
buffer += chunk;
|
||||||
|
});
|
||||||
|
request.on('end', () => {
|
||||||
|
const hasEmail = buffer.includes('<email>user@example.com</email>')
|
||||||
|
|| buffer.includes('<m:email>user@example.com</m:email>');
|
||||||
|
|
||||||
|
if (!hasEmail) {
|
||||||
|
response.writeHead(400, {
|
||||||
|
'Content-Type': 'text/xml; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
});
|
||||||
|
response.end([
|
||||||
|
'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">',
|
||||||
|
'<soap:Body>',
|
||||||
|
'<soap:Fault>',
|
||||||
|
'<faultcode>Client</faultcode>',
|
||||||
|
'<faultstring>missing email</faultstring>',
|
||||||
|
'</soap:Fault>',
|
||||||
|
'</soap:Body>',
|
||||||
|
'</soap:Envelope>',
|
||||||
|
].join(''));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.writeHead(200, {
|
||||||
|
'Content-Type': 'text/xml; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
});
|
||||||
|
response.end([
|
||||||
|
'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">',
|
||||||
|
'<soap:Body>',
|
||||||
|
'<CreateLeadResponse>',
|
||||||
|
'<id>lead_soap_123</id>',
|
||||||
|
'<status>created</status>',
|
||||||
|
'</CreateLeadResponse>',
|
||||||
|
'</soap:Body>',
|
||||||
|
'</soap:Envelope>',
|
||||||
|
].join(''));
|
||||||
|
});
|
||||||
|
request.on('error', () => {
|
||||||
|
response.writeHead(500, {
|
||||||
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
});
|
||||||
|
response.end('fixture_error');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
writeJson(response, 404, { error: 'not_found' });
|
writeJson(response, 404, { error: 'not_found' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,84 @@ function buildRestWindowOperationPayload(name) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SOAP_TEST_WSDL = `<?xml version="1.0"?>
|
||||||
|
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
|
||||||
|
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
|
||||||
|
xmlns:tns="urn:crm"
|
||||||
|
targetNamespace="urn:crm">
|
||||||
|
<binding name="LeadBinding" type="tns:LeadPortType">
|
||||||
|
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
|
||||||
|
<operation name="CreateLead">
|
||||||
|
<soap:operation soapAction="urn:createLead"/>
|
||||||
|
</operation>
|
||||||
|
</binding>
|
||||||
|
<service name="LeadService">
|
||||||
|
<port name="LeadPort" binding="tns:LeadBinding">
|
||||||
|
<soap:address location="http://127.0.0.1:${STREAM_FIXTURE_PORT}/soap/leads"/>
|
||||||
|
</port>
|
||||||
|
</service>
|
||||||
|
</definitions>`;
|
||||||
|
|
||||||
|
function buildSoapOperationPayload(name) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
display_name: 'Playwright SOAP Lead',
|
||||||
|
category: 'sales',
|
||||||
|
protocol: 'soap',
|
||||||
|
target: {
|
||||||
|
kind: 'soap',
|
||||||
|
wsdl_ref: 'sample_wsdl_pending',
|
||||||
|
service_name: 'Service',
|
||||||
|
port_name: 'Port',
|
||||||
|
operation_name: 'Operation',
|
||||||
|
endpoint_override: `http://127.0.0.1:${STREAM_FIXTURE_PORT}/soap/leads`,
|
||||||
|
soap_version: 'soap_11',
|
||||||
|
soap_action: 'urn:createLead',
|
||||||
|
binding_style: 'document_literal',
|
||||||
|
headers: [],
|
||||||
|
metadata: {
|
||||||
|
input_part_names: ['CreateLeadRequest'],
|
||||||
|
output_part_names: ['CreateLeadResponse'],
|
||||||
|
namespaces: ['urn:crm'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input_schema: schemaObject({
|
||||||
|
email: schemaString(true),
|
||||||
|
}),
|
||||||
|
output_schema: schemaObject({
|
||||||
|
id: schemaString(true),
|
||||||
|
}),
|
||||||
|
input_mapping: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
source: '$.mcp.email',
|
||||||
|
target: '$.request.body.email',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
output_mapping: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
source: '$.response.body.id',
|
||||||
|
target: '$.output.id',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
execution_config: {
|
||||||
|
timeout_ms: 1000,
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
tool_description: {
|
||||||
|
title: 'Playwright SOAP Lead',
|
||||||
|
description: 'Creates a lead through the local SOAP fixture.',
|
||||||
|
tags: ['playwright', 'soap'],
|
||||||
|
examples: [{ input: { email: 'user@example.com' } }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildGrpcSessionOperationPayload(name) {
|
function buildGrpcSessionOperationPayload(name) {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@@ -474,10 +552,12 @@ async function mcpToolCall(session, toolName, argumentsValue) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
ADMIN_EMAIL,
|
ADMIN_EMAIL,
|
||||||
ADMIN_PASSWORD,
|
ADMIN_PASSWORD,
|
||||||
|
SOAP_TEST_WSDL,
|
||||||
browserJson,
|
browserJson,
|
||||||
buildGrpcSessionOperationPayload,
|
buildGrpcSessionOperationPayload,
|
||||||
buildRestAsyncJobOperationPayload,
|
buildRestAsyncJobOperationPayload,
|
||||||
buildRestWindowOperationPayload,
|
buildRestWindowOperationPayload,
|
||||||
|
buildSoapOperationPayload,
|
||||||
createAgent,
|
createAgent,
|
||||||
createOperation,
|
createOperation,
|
||||||
createPlatformApiKey,
|
createPlatformApiKey,
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { login, localized } = require('./helpers');
|
const {
|
||||||
|
SOAP_TEST_WSDL,
|
||||||
|
buildSoapOperationPayload,
|
||||||
|
createOperation,
|
||||||
|
getCurrentWorkspace,
|
||||||
|
login,
|
||||||
|
localized,
|
||||||
|
uniqueName,
|
||||||
|
} = require('./helpers');
|
||||||
|
|
||||||
test('wizard loads and protocol selection updates flow', async ({ page }) => {
|
test('wizard loads and protocol selection updates flow', async ({ page }) => {
|
||||||
await login(page);
|
await login(page);
|
||||||
@@ -11,3 +19,77 @@ test('wizard loads and protocol selection updates flow', async ({ page }) => {
|
|||||||
await expect(page.locator('[data-step-counter]')).toContainText(/2/);
|
await expect(page.locator('[data-step-counter]')).toContainText(/2/);
|
||||||
await expect(page.locator('#step-panel-2 .step-panel-title')).toContainText(localized('Select upstream', 'Выберите upstream'));
|
await expect(page.locator('#step-panel-2 .step-panel-title')).toContainText(localized('Select upstream', 'Выберите upstream'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('soap wizard uploads wsdl, applies discovered binding and runs test', async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
const workspace = await getCurrentWorkspace(page);
|
||||||
|
const operation = await createOperation(
|
||||||
|
page,
|
||||||
|
workspace.id,
|
||||||
|
buildSoapOperationPayload(uniqueName('playwright_soap_wizard')),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto(`/wizard/?mode=edit&operationId=${encodeURIComponent(operation.operation_id)}`);
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
|
||||||
|
await expect(page.locator('#step-panel-3-soap .step-panel-title')).toContainText(/WSDL/i);
|
||||||
|
await page.evaluate((wsdl) => {
|
||||||
|
const fileName = document.getElementById('wizard-soap-wsdl-name');
|
||||||
|
if (fileName) fileName.textContent = 'lead.wsdl';
|
||||||
|
}, SOAP_TEST_WSDL);
|
||||||
|
await page.evaluate(async ({ workspaceId, operationId, wsdl }) => {
|
||||||
|
const uploaded = await window.CrankApi.uploadWsdlFile(
|
||||||
|
workspaceId,
|
||||||
|
operationId,
|
||||||
|
new TextEncoder().encode(wsdl),
|
||||||
|
'lead.wsdl',
|
||||||
|
);
|
||||||
|
const services = await window.CrankApi.listSoapServices(
|
||||||
|
workspaceId,
|
||||||
|
operationId,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
window.renderSoapServiceCatalog(services.services || []);
|
||||||
|
return uploaded;
|
||||||
|
}, {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
operationId: operation.operation_id,
|
||||||
|
wsdl: SOAP_TEST_WSDL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.locator('#wizard-soap-services-list button')).toContainText('CreateLead');
|
||||||
|
await page.locator('#wizard-soap-services-list button').getByText('CreateLead').click();
|
||||||
|
await expect(page.locator('#soap-service-name')).toHaveValue('LeadService');
|
||||||
|
await expect(page.locator('#soap-port-name')).toHaveValue('LeadPort');
|
||||||
|
await expect(page.locator('#soap-operation-name')).toHaveValue('CreateLead');
|
||||||
|
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
await expect(page.locator('#step-panel-4 .step-panel-title')).toBeVisible();
|
||||||
|
await page.locator('#tool-input-schema').fill(JSON.stringify({
|
||||||
|
type: 'object',
|
||||||
|
required: ['email'],
|
||||||
|
properties: {
|
||||||
|
email: { type: 'string' },
|
||||||
|
},
|
||||||
|
}, null, 2));
|
||||||
|
await page.locator('#tool-output-schema').fill(JSON.stringify({
|
||||||
|
type: 'object',
|
||||||
|
required: ['id'],
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string' },
|
||||||
|
},
|
||||||
|
}, null, 2));
|
||||||
|
await page.locator('#btn-continue').click();
|
||||||
|
await expect(page.locator('[data-i18n="wizard.step5.test_title"]')).toContainText(localized('Test run', 'Тестовый запуск'));
|
||||||
|
await page.locator('#tool-input-mapping').fill('email: "$.input.email"');
|
||||||
|
await page.locator('#tool-output-mapping').fill('id: "$.response.body.id"');
|
||||||
|
await page.locator('#tool-exec-config').fill('timeout_ms: 1000');
|
||||||
|
|
||||||
|
await page.locator('#wizard-test-input').fill(JSON.stringify({ email: 'user@example.com' }, null, 2));
|
||||||
|
await page.locator('#wizard-run-test').click();
|
||||||
|
|
||||||
|
await expect(page.locator('#wizard-test-request-preview')).toHaveValue(/user@example.com/);
|
||||||
|
await expect(page.locator('#wizard-test-response-preview')).toHaveValue(/lead_soap_123/);
|
||||||
|
await expect(page.locator('#wizard-test-errors')).toHaveValue('[]');
|
||||||
|
});
|
||||||
|
|||||||
@@ -2736,27 +2736,9 @@ impl PostgresRegistry {
|
|||||||
.bind(Json(serialize_json_value(&snapshot.output_mapping)?))
|
.bind(Json(serialize_json_value(&snapshot.output_mapping)?))
|
||||||
.bind(Json(serialize_json_value(&snapshot.execution_config)?))
|
.bind(Json(serialize_json_value(&snapshot.execution_config)?))
|
||||||
.bind(Json(serialize_json_value(&snapshot.tool_description)?))
|
.bind(Json(serialize_json_value(&snapshot.tool_description)?))
|
||||||
.bind(Json(
|
.bind(serialize_option_json_value(&snapshot.samples)?.map(Json))
|
||||||
snapshot
|
.bind(serialize_option_json_value(&snapshot.generated_draft)?.map(Json))
|
||||||
.samples
|
.bind(serialize_option_json_value(&snapshot.config_export)?.map(Json))
|
||||||
.clone()
|
|
||||||
.map(|value| serialize_json_value(&value))
|
|
||||||
.transpose()?,
|
|
||||||
))
|
|
||||||
.bind(Json(
|
|
||||||
snapshot
|
|
||||||
.generated_draft
|
|
||||||
.clone()
|
|
||||||
.map(|value| serialize_json_value(&value))
|
|
||||||
.transpose()?,
|
|
||||||
))
|
|
||||||
.bind(Json(
|
|
||||||
snapshot
|
|
||||||
.config_export
|
|
||||||
.clone()
|
|
||||||
.map(|value| serialize_json_value(&value))
|
|
||||||
.transpose()?,
|
|
||||||
))
|
|
||||||
.bind(snapshot.id.as_str())
|
.bind(snapshot.id.as_str())
|
||||||
.bind(to_db_version(snapshot.version))
|
.bind(to_db_version(snapshot.version))
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
@@ -4539,6 +4521,40 @@ mod tests {
|
|||||||
database.cleanup().await;
|
database.cleanup().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_operation_draft_persists_optional_json_columns_as_sql_null() {
|
||||||
|
let database = TestDatabase::new().await;
|
||||||
|
let registry = database.registry().await;
|
||||||
|
let mut operation = test_operation("op_rest_02b", 1, OperationStatus::Draft);
|
||||||
|
|
||||||
|
registry
|
||||||
|
.create_operation(&test_workspace_id(), &operation, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
operation.generated_draft = None;
|
||||||
|
operation.samples = None;
|
||||||
|
operation.config_export = None;
|
||||||
|
operation.updated_at = "2026-03-25T12:34:00Z".to_owned();
|
||||||
|
|
||||||
|
registry
|
||||||
|
.update_operation_draft(&test_workspace_id(), &operation)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let stored = registry
|
||||||
|
.get_operation_version(&test_workspace_id(), &operation.id, operation.version)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(stored.snapshot.generated_draft, None);
|
||||||
|
assert_eq!(stored.snapshot.samples, None);
|
||||||
|
assert_eq!(stored.snapshot.config_export, None);
|
||||||
|
|
||||||
|
database.cleanup().await;
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn stores_auth_profiles_and_artifact_metadata() {
|
async fn stores_auth_profiles_and_artifact_metadata() {
|
||||||
let database = TestDatabase::new().await;
|
let database = TestDatabase::new().await;
|
||||||
|
|||||||
Reference in New Issue
Block a user