test: polish soap wizard test run flow
This commit is contained in:
@@ -2,36 +2,19 @@
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/streaming-test-run-results`
|
||||
### `feat/websocket-test-run-polish`
|
||||
|
||||
Status: completed
|
||||
Status: pending
|
||||
|
||||
DoD:
|
||||
- `POST /operations/{operation_id}/test-runs` returns mode-aware streaming metadata
|
||||
- window test-runs expose bounded result flags for the wizard status block
|
||||
- session and async-job test-runs create persisted runtime state and return references for follow-up pages
|
||||
- WebSocket wizard test-run shows reconnect / bounded-result semantics clearly
|
||||
- WebSocket test-run flow is covered by local fixtures and Playwright
|
||||
- `TASKS.md` stays aligned with the actual streaming branch state
|
||||
|
||||
## Next
|
||||
|
||||
- `feat/soap-test-run-polish`
|
||||
- `feat/live-authenticated-staging-entry`
|
||||
|
||||
## Backlog
|
||||
|
||||
- `feat/streaming-mcp-architecture`
|
||||
- `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`
|
||||
- `feat/live-authenticated-staging-entry`
|
||||
|
||||
@@ -1553,6 +1553,9 @@ function currentUpstream() {
|
||||
|
||||
var customName = textValue('new-upstream-name');
|
||||
var customUrl = textValue('new-upstream-url');
|
||||
if (!customUrl && wizardProtocol === 'soap') {
|
||||
customUrl = textValue('soap-endpoint-override');
|
||||
}
|
||||
if (!customUrl) return null;
|
||||
|
||||
var authMode = textValue('new-upstream-auth-mode') || 'none';
|
||||
|
||||
@@ -103,6 +103,59 @@ const server = http.createServer((request, response) => {
|
||||
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' });
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
return {
|
||||
name,
|
||||
@@ -474,10 +552,12 @@ async function mcpToolCall(session, toolName, argumentsValue) {
|
||||
module.exports = {
|
||||
ADMIN_EMAIL,
|
||||
ADMIN_PASSWORD,
|
||||
SOAP_TEST_WSDL,
|
||||
browserJson,
|
||||
buildGrpcSessionOperationPayload,
|
||||
buildRestAsyncJobOperationPayload,
|
||||
buildRestWindowOperationPayload,
|
||||
buildSoapOperationPayload,
|
||||
createAgent,
|
||||
createOperation,
|
||||
createPlatformApiKey,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
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 }) => {
|
||||
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('#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.execution_config)?))
|
||||
.bind(Json(serialize_json_value(&snapshot.tool_description)?))
|
||||
.bind(Json(
|
||||
snapshot
|
||||
.samples
|
||||
.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(serialize_option_json_value(&snapshot.samples)?.map(Json))
|
||||
.bind(serialize_option_json_value(&snapshot.generated_draft)?.map(Json))
|
||||
.bind(serialize_option_json_value(&snapshot.config_export)?.map(Json))
|
||||
.bind(snapshot.id.as_str())
|
||||
.bind(to_db_version(snapshot.version))
|
||||
.execute(&mut *tx)
|
||||
@@ -4539,6 +4521,40 @@ mod tests {
|
||||
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]
|
||||
async fn stores_auth_profiles_and_artifact_metadata() {
|
||||
let database = TestDatabase::new().await;
|
||||
|
||||
Reference in New Issue
Block a user