diff --git a/TASKS.md b/TASKS.md
index ada9d74..a37df50 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -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`
diff --git a/apps/ui/js/wizard.js b/apps/ui/js/wizard.js
index 30d1f9d..4099581 100644
--- a/apps/ui/js/wizard.js
+++ b/apps/ui/js/wizard.js
@@ -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';
diff --git a/apps/ui/scripts/stream-fixture-server.js b/apps/ui/scripts/stream-fixture-server.js
index f08d0fd..f37e9a5 100644
--- a/apps/ui/scripts/stream-fixture-server.js
+++ b/apps/ui/scripts/stream-fixture-server.js
@@ -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('user@example.com')
+ || buffer.includes('user@example.com');
+
+ if (!hasEmail) {
+ response.writeHead(400, {
+ 'Content-Type': 'text/xml; charset=utf-8',
+ 'Cache-Control': 'no-store',
+ });
+ response.end([
+ '',
+ '',
+ '',
+ 'Client',
+ 'missing email',
+ '',
+ '',
+ '',
+ ].join(''));
+ return;
+ }
+
+ response.writeHead(200, {
+ 'Content-Type': 'text/xml; charset=utf-8',
+ 'Cache-Control': 'no-store',
+ });
+ response.end([
+ '',
+ '',
+ '',
+ 'lead_soap_123',
+ 'created',
+ '',
+ '',
+ '',
+ ].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' });
});
diff --git a/apps/ui/tests/e2e/helpers.js b/apps/ui/tests/e2e/helpers.js
index bc0cf7a..046029d 100644
--- a/apps/ui/tests/e2e/helpers.js
+++ b/apps/ui/tests/e2e/helpers.js
@@ -213,6 +213,84 @@ function buildRestWindowOperationPayload(name) {
};
}
+const SOAP_TEST_WSDL = `
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+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,
diff --git a/apps/ui/tests/e2e/wizard.spec.js b/apps/ui/tests/e2e/wizard.spec.js
index 1df7a4a..5b8eb41 100644
--- a/apps/ui/tests/e2e/wizard.spec.js
+++ b/apps/ui/tests/e2e/wizard.spec.js
@@ -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('[]');
+});
diff --git a/crates/crank-registry/src/postgres.rs b/crates/crank-registry/src/postgres.rs
index cac5446..60fb54b 100644
--- a/crates/crank-registry/src/postgres.rs
+++ b/crates/crank-registry/src/postgres.rs
@@ -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;