Polish community UI copy and cleanup
This commit is contained in:
+2
-227
@@ -196,9 +196,8 @@ mod tests {
|
||||
StreamSession, StreamStatus, TransportBehavior,
|
||||
};
|
||||
use crank_core::{
|
||||
ExecutionConfig, GraphqlOperationType, GraphqlTarget, HttpMethod, MembershipRole,
|
||||
OperationSecurityLevel, Protocol, ResponseCachePolicy, RestTarget, SecretKind, Target,
|
||||
ToolDescription, WebsocketTarget, WorkspaceId,
|
||||
ExecutionConfig, HttpMethod, MembershipRole, OperationSecurityLevel, Protocol,
|
||||
ResponseCachePolicy, RestTarget, SecretKind, Target, ToolDescription, WorkspaceId,
|
||||
};
|
||||
use crank_core::{IdentityError, IdentityProvider, IdentityProviderKind, LoginOutcome};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
@@ -1242,78 +1241,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn rejects_graphql_protocol_for_community_operation_create() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("community_graphql_reject_create");
|
||||
let upstream_base_url = spawn_graphql_server().await;
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
|
||||
let response = client
|
||||
.post(format!("{base_url}/operations"))
|
||||
.json(&test_graphql_operation_payload(
|
||||
&upstream_base_url,
|
||||
"crm_graphql_not_in_community",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let body = response.json::<Value>().await.unwrap();
|
||||
|
||||
assert_eq!(status, reqwest::StatusCode::BAD_REQUEST);
|
||||
assert_eq!(body["error"]["code"], "validation_error");
|
||||
assert_eq!(
|
||||
body["error"]["message"],
|
||||
"protocol graphql is not supported in Community"
|
||||
);
|
||||
assert_eq!(
|
||||
body["error"]["context"],
|
||||
json!({
|
||||
"protocol": "graphql",
|
||||
"edition": "community",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn rejects_unsupported_protocol_for_community_operation_create() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("community_protocol_reject");
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
|
||||
let response = client
|
||||
.post(format!("{base_url}/operations"))
|
||||
.json(&test_websocket_operation_payload(
|
||||
"wss://example.com/stream",
|
||||
"telemetry_ws",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let body = response.json::<Value>().await.unwrap();
|
||||
|
||||
assert_eq!(status, reqwest::StatusCode::BAD_REQUEST);
|
||||
assert_eq!(body["error"]["code"], "validation_error");
|
||||
assert_eq!(
|
||||
body["error"]["message"],
|
||||
"protocol websocket is not supported in Community"
|
||||
);
|
||||
assert_eq!(
|
||||
body["error"]["context"],
|
||||
json!({
|
||||
"protocol": "websocket",
|
||||
"edition": "community",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn rejects_premium_security_level_for_community_operation_create() {
|
||||
@@ -3306,18 +3233,6 @@ mod tests {
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
async fn spawn_graphql_server() -> String {
|
||||
let app = Router::new().route("/", post(graphql_handler));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
#[cfg(any())]
|
||||
async fn spawn_soap_server() -> String {
|
||||
let app = Router::new().route("/", post(soap_handler));
|
||||
@@ -3501,24 +3416,6 @@ mod tests {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn graphql_handler(Json(payload): Json<Value>) -> Json<Value> {
|
||||
let email = payload
|
||||
.get("variables")
|
||||
.and_then(|variables| variables.get("email"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
|
||||
Json(json!({
|
||||
"data": {
|
||||
"createLead": {
|
||||
"id": "lead_123",
|
||||
"status": "created",
|
||||
"email": email
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(any())]
|
||||
async fn soap_handler(body: String) -> (axum::http::StatusCode, String) {
|
||||
assert!(body.contains("<email>user@example.com</email>"));
|
||||
@@ -3647,8 +3544,6 @@ mod tests {
|
||||
response_cache: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
streaming: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create Lead".to_owned(),
|
||||
@@ -3660,65 +3555,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_graphql_operation_payload(endpoint: &str, name: &str) -> OperationPayload {
|
||||
OperationPayload {
|
||||
name: name.to_owned(),
|
||||
display_name: "Create Lead GraphQL".to_owned(),
|
||||
category: "sales".to_owned(),
|
||||
protocol: Protocol::Graphql,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
target: Target::Graphql(GraphqlTarget {
|
||||
endpoint: endpoint.to_owned(),
|
||||
operation_type: GraphqlOperationType::Mutation,
|
||||
operation_name: "CreateLead".to_owned(),
|
||||
query_template:
|
||||
"mutation CreateLead($email: String!) { createLead(email: $email) { id status email } }"
|
||||
.to_owned(),
|
||||
response_path: "$.response.body.data.createLead".to_owned(),
|
||||
}),
|
||||
input_schema: object_schema("email"),
|
||||
output_schema: object_schema("id"),
|
||||
input_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.email".to_owned(),
|
||||
target: "$.request.variables.email".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.data.id".to_owned(),
|
||||
target: "$.output.id".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 1_000,
|
||||
retry_policy: None,
|
||||
response_cache: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
streaming: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create Lead GraphQL".to_owned(),
|
||||
description: "Creates a CRM lead through GraphQL".to_owned(),
|
||||
tags: vec!["crm".to_owned(), "graphql".to_owned()],
|
||||
examples: Vec::new(),
|
||||
},
|
||||
wizard_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any())]
|
||||
fn test_grpc_operation_payload(server_addr: &str, name: &str) -> OperationPayload {
|
||||
OperationPayload {
|
||||
@@ -3766,8 +3602,6 @@ mod tests {
|
||||
response_cache: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
streaming: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Unary Echo gRPC".to_owned(),
|
||||
@@ -3930,8 +3764,6 @@ mod tests {
|
||||
response_cache: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
streaming: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create Lead SOAP".to_owned(),
|
||||
@@ -3943,63 +3775,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_websocket_operation_payload(url: &str, name: &str) -> OperationPayload {
|
||||
OperationPayload {
|
||||
name: name.to_owned(),
|
||||
display_name: "Telemetry WebSocket".to_owned(),
|
||||
category: "telemetry".to_owned(),
|
||||
protocol: Protocol::Websocket,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
target: Target::Websocket(WebsocketTarget {
|
||||
url: url.to_owned(),
|
||||
subprotocols: Vec::new(),
|
||||
subscribe_message_template: Some(json!({ "subscribe": true })),
|
||||
unsubscribe_message_template: None,
|
||||
static_headers: BTreeMap::new(),
|
||||
}),
|
||||
input_schema: object_schema("channel"),
|
||||
output_schema: object_schema("event"),
|
||||
input_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.channel".to_owned(),
|
||||
target: "$.request.body.channel".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.body.event".to_owned(),
|
||||
target: "$.output.event".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 1_000,
|
||||
retry_policy: None,
|
||||
response_cache: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
streaming: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Telemetry WebSocket".to_owned(),
|
||||
description: "Streams telemetry events".to_owned(),
|
||||
tags: vec!["telemetry".to_owned(), "websocket".to_owned()],
|
||||
examples: Vec::new(),
|
||||
},
|
||||
wizard_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any())]
|
||||
const SOAP_TEST_WSDL: &str = r#"<?xml version="1.0"?>
|
||||
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
|
||||
|
||||
@@ -432,18 +432,12 @@ fn runtime_test_failure_code(error: &RuntimeError) -> &'static str {
|
||||
match error {
|
||||
RuntimeError::Schema(_) => "runtime_schema_error",
|
||||
RuntimeError::Mapping(_) => "runtime_mapping_error",
|
||||
RuntimeError::GraphqlAdapter(_) => "runtime_graphql_error",
|
||||
RuntimeError::GrpcAdapter(_) => "runtime_grpc_error",
|
||||
RuntimeError::RestAdapter(_) => "runtime_rest_error",
|
||||
RuntimeError::ProtocolAdapter(_) => "runtime_adapter_error",
|
||||
RuntimeError::SoapAdapter(_) => "runtime_soap_error",
|
||||
RuntimeError::WebsocketAdapter(_) => "runtime_websocket_error",
|
||||
RuntimeError::UnsupportedProtocol { .. } => "runtime_protocol_error",
|
||||
RuntimeError::ConcurrencyLimitExceeded { .. } => "runtime_overloaded",
|
||||
RuntimeError::InvalidPreparedRequest { .. } => "runtime_request_error",
|
||||
RuntimeError::MissingStreamingConfig { .. } => "runtime_streaming_config_error",
|
||||
RuntimeError::UnsupportedExecutionMode { .. } => "runtime_streaming_mode_error",
|
||||
RuntimeError::InvalidStreamingPayload { .. } => "runtime_streaming_payload_error",
|
||||
RuntimeError::MissingAuthProfile { .. } => "runtime_auth_profile_error",
|
||||
RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => {
|
||||
"runtime_secret_error"
|
||||
@@ -459,10 +453,6 @@ pub fn runtime_error_context(error: &RuntimeError) -> Option<Value> {
|
||||
"field": field,
|
||||
"reason": reason,
|
||||
})),
|
||||
RuntimeError::InvalidStreamingPayload { field, reason } => Some(json!({
|
||||
"field": field,
|
||||
"reason": reason,
|
||||
})),
|
||||
RuntimeError::InvalidAuthSecretValue { secret_id, reason } => Some(json!({
|
||||
"secret_id": secret_id,
|
||||
"reason": reason,
|
||||
@@ -481,9 +471,6 @@ pub fn runtime_error_context(error: &RuntimeError) -> Option<Value> {
|
||||
"secret_id": secret_id,
|
||||
"version": version,
|
||||
})),
|
||||
RuntimeError::MissingStreamingConfig { operation_id } => Some(json!({
|
||||
"operation_id": operation_id,
|
||||
})),
|
||||
RuntimeError::UnsupportedExecutionMode { operation_id, mode } => Some(json!({
|
||||
"operation_id": operation_id,
|
||||
"mode": mode,
|
||||
|
||||
@@ -88,9 +88,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
info!(
|
||||
runtime_max_concurrent_unary = runtime_limits.max_concurrent_unary,
|
||||
runtime_max_concurrent_window = runtime_limits.max_concurrent_window,
|
||||
runtime_max_concurrent_sessions = runtime_limits.max_concurrent_sessions,
|
||||
runtime_max_concurrent_jobs = runtime_limits.max_concurrent_jobs,
|
||||
admin_rate_limit_rps = api_rate_limit.requests_per_second,
|
||||
admin_rate_limit_burst = api_rate_limit.burst,
|
||||
cache_backend = %cache_config.backend,
|
||||
|
||||
+45
-351
@@ -4,31 +4,29 @@ use std::sync::Arc;
|
||||
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use crank_core::{
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AggregationMode,
|
||||
AsyncJobHandle, AuditSink, AuthConfig, AuthKind, AuthProfile, AuthProfileId, CapabilityProfile,
|
||||
CommunityCapabilityProfile, ConfigExport, EditionCapabilities, ExecutionMode, ExportMode,
|
||||
GeneratedDraft, GeneratedDraftStatus, IdentityError, IdentityProvider, InvocationLevel,
|
||||
InvocationLog, InvocationLogId, InvocationSource, InvocationStatus, JobStatus, LoginOutcome,
|
||||
MachineTokenIssuer, MembershipRole, NoMachineTokenIssuer, NoopAuditSink, OperationId,
|
||||
OperationSecurityLevel, OperationStatus, OwnerOnlyPolicyEngine, PlatformApiKey,
|
||||
PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, PolicyEngine, ProductEdition,
|
||||
Protocol, ResponseCachePolicy, SampleId, Samples, Secret, SecretId, SecretKind, SecretStatus,
|
||||
StreamSession, StreamStatus, Target, TransportBehavior, UsagePeriod, UserId, UserSessionId,
|
||||
WizardState, Workspace, WorkspaceId, WorkspaceStatus,
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuditSink, AuthConfig,
|
||||
AuthKind, AuthProfile, AuthProfileId, CapabilityProfile, CommunityCapabilityProfile,
|
||||
ConfigExport, EditionCapabilities, ExecutionMode, ExportMode, GeneratedDraft,
|
||||
GeneratedDraftStatus, IdentityError, IdentityProvider, InvocationLevel, InvocationLog,
|
||||
InvocationLogId, InvocationSource, InvocationStatus, LoginOutcome, MachineTokenIssuer,
|
||||
MembershipRole, NoMachineTokenIssuer, NoopAuditSink, OperationId, OperationSecurityLevel,
|
||||
OperationStatus, OwnerOnlyPolicyEngine, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope,
|
||||
PlatformApiKeyStatus, PolicyEngine, ProductEdition, Protocol, ResponseCachePolicy, SampleId,
|
||||
Samples, Secret, SecretId, SecretKind, SecretStatus, Target, UsagePeriod, UserId,
|
||||
UserSessionId, WizardState, Workspace, WorkspaceId, WorkspaceStatus,
|
||||
};
|
||||
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
|
||||
use crank_registry::{
|
||||
AgentSummary, AgentVersionRecord, CreateAgentDraftVersionRequest, CreateAgentRequest,
|
||||
CreateAsyncJobRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest,
|
||||
CreateSecretRequest, CreateStreamSessionRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
||||
InvocationLogRecord, ListInvocationLogsQuery, OperationAgentRef, OperationSampleMetadata,
|
||||
OperationSummary, OperationUsageSummary, OperationVersionRecord, PlatformApiKeyRecord,
|
||||
PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation,
|
||||
RotateSecretRequest, SampleKind, SaveAgentBindingsRequest, SaveAuthProfileRequest,
|
||||
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, UpdateAsyncJobStatusRequest,
|
||||
UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket, UsageOperationBreakdown, UsageQuery,
|
||||
UsageSummary, UsageTimelinePoint, WorkspaceMembershipRecord, WorkspaceRecord,
|
||||
WorkspaceUpstream, WorkspaceUpstreamId,
|
||||
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateSecretRequest,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, InvocationLogRecord, ListInvocationLogsQuery,
|
||||
OperationAgentRef, OperationSampleMetadata, OperationSummary, OperationUsageSummary,
|
||||
OperationVersionRecord, PlatformApiKeyRecord, PostgresRegistry, PublishAgentRequest,
|
||||
PublishRequest, RegistryError, RegistryOperation, RotateSecretRequest, SampleKind,
|
||||
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveSampleMetadataRequest,
|
||||
SaveWorkspaceUpstreamRequest, UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket,
|
||||
UsageOperationBreakdown, UsageQuery, UsageSummary, UsageTimelinePoint,
|
||||
WorkspaceMembershipRecord, WorkspaceRecord, WorkspaceUpstream, WorkspaceUpstreamId,
|
||||
};
|
||||
use crank_runtime::{
|
||||
PreparedRequest, ResolvedAuth, RuntimeError, RuntimeExecutor, RuntimeOperation,
|
||||
@@ -154,120 +152,6 @@ pub struct TestRunResult {
|
||||
pub request_preview: Value,
|
||||
pub response_preview: Value,
|
||||
pub errors: Vec<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window: Option<WindowTestRunView>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream_session: Option<StreamSessionStartView>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub async_job: Option<AsyncJobStartView>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct WindowTestRunView {
|
||||
pub window_complete: bool,
|
||||
pub truncated: bool,
|
||||
pub has_more: bool,
|
||||
pub cursor: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct StreamSessionStartView {
|
||||
pub session_id: String,
|
||||
pub status: StreamStatus,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub expires_at: OffsetDateTime,
|
||||
pub poll_after_ms: u64,
|
||||
pub preview: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct AsyncJobStartView {
|
||||
pub job_id: String,
|
||||
pub status: JobStatus,
|
||||
pub progress: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct StoredSessionState {
|
||||
input: Value,
|
||||
summary: Value,
|
||||
items: Vec<Value>,
|
||||
next_index: usize,
|
||||
batch_size: usize,
|
||||
}
|
||||
|
||||
enum TestRunOutcome {
|
||||
Unary {
|
||||
output: Value,
|
||||
},
|
||||
Window {
|
||||
output: crank_runtime::WindowExecutionResult,
|
||||
},
|
||||
Session {
|
||||
output: StreamSessionStartView,
|
||||
},
|
||||
AsyncJob {
|
||||
output: AsyncJobStartView,
|
||||
},
|
||||
}
|
||||
|
||||
impl TestRunOutcome {
|
||||
fn into_result_views(
|
||||
self,
|
||||
) -> (
|
||||
&'static str,
|
||||
Value,
|
||||
Option<WindowTestRunView>,
|
||||
Option<StreamSessionStartView>,
|
||||
Option<AsyncJobStartView>,
|
||||
) {
|
||||
match self {
|
||||
Self::Unary { output } => ("admin test run completed", output, None, None, None),
|
||||
Self::Window { output } => (
|
||||
"admin window test run completed",
|
||||
json!({
|
||||
"summary": output.summary,
|
||||
"items": output.items,
|
||||
"cursor": output.cursor,
|
||||
"window_complete": output.window_complete,
|
||||
"truncated": output.truncated,
|
||||
"has_more": output.has_more,
|
||||
}),
|
||||
Some(WindowTestRunView {
|
||||
window_complete: output.window_complete,
|
||||
truncated: output.truncated,
|
||||
has_more: output.has_more,
|
||||
cursor: output.cursor,
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
Self::Session { output } => (
|
||||
"admin session test run started",
|
||||
json!({
|
||||
"session_id": output.session_id,
|
||||
"status": output.status,
|
||||
"expires_at": format_timestamp(output.expires_at),
|
||||
"poll_after_ms": output.poll_after_ms,
|
||||
"preview": output.preview,
|
||||
}),
|
||||
None,
|
||||
Some(output),
|
||||
None,
|
||||
),
|
||||
Self::AsyncJob { output } => (
|
||||
"admin async job test run started",
|
||||
json!({
|
||||
"job_id": output.job_id,
|
||||
"status": output.status,
|
||||
"progress": output.progress,
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
Some(output),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
@@ -437,12 +321,12 @@ pub struct UsageOverviewResponse {
|
||||
pub struct ProtocolCapabilityView {
|
||||
pub protocol: Protocol,
|
||||
pub supports_execution_modes: Vec<ExecutionMode>,
|
||||
pub supports_transport_behaviors: Vec<TransportBehavior>,
|
||||
pub supports_transport_behaviors: Vec<String>,
|
||||
pub supports_auth_kinds: Vec<String>,
|
||||
pub supports_upload_artifacts: Vec<String>,
|
||||
pub supports_cursor_path: bool,
|
||||
pub supports_done_path: bool,
|
||||
pub supports_aggregation_mode: Vec<AggregationMode>,
|
||||
pub supports_aggregation_mode: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
@@ -1801,12 +1685,7 @@ impl AdminService {
|
||||
.get_operation_version(workspace_id, operation_id, payload.version)
|
||||
.await?;
|
||||
let runtime = RuntimeOperation::from(record.snapshot.clone());
|
||||
let mode = runtime
|
||||
.execution_config
|
||||
.streaming
|
||||
.as_ref()
|
||||
.map(|streaming| streaming.mode)
|
||||
.unwrap_or(ExecutionMode::Unary);
|
||||
let mode = ExecutionMode::Unary;
|
||||
let request_preview =
|
||||
match build_request_preview(&record.snapshot.input_mapping, &payload.input) {
|
||||
Ok(preview) => preview,
|
||||
@@ -1835,9 +1714,6 @@ impl AdminService {
|
||||
errors: vec![crate::error::runtime_test_failure(&RuntimeError::Mapping(
|
||||
error,
|
||||
))],
|
||||
window: None,
|
||||
stream_session: None,
|
||||
async_job: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1847,9 +1723,8 @@ impl AdminService {
|
||||
.await;
|
||||
let started_at = std::time::Instant::now();
|
||||
match match resolved_auth {
|
||||
Ok(resolved_auth) => match mode {
|
||||
ExecutionMode::Unary => self
|
||||
.runtime
|
||||
Ok(resolved_auth) => {
|
||||
self.runtime
|
||||
.execute_with_auth_and_context(
|
||||
&runtime,
|
||||
&payload.input,
|
||||
@@ -1857,46 +1732,12 @@ impl AdminService {
|
||||
Some(&runtime_request_context),
|
||||
)
|
||||
.await
|
||||
.map(|output| TestRunOutcome::Unary { output }),
|
||||
ExecutionMode::Window => self
|
||||
.runtime
|
||||
.execute_window_with_auth_and_context(
|
||||
&runtime,
|
||||
&payload.input,
|
||||
resolved_auth.as_ref(),
|
||||
Some(&runtime_request_context),
|
||||
)
|
||||
.await
|
||||
.map(|output| TestRunOutcome::Window { output }),
|
||||
ExecutionMode::Session => self
|
||||
.start_stream_session_test(
|
||||
workspace_id,
|
||||
&record.snapshot,
|
||||
&runtime,
|
||||
&payload.input,
|
||||
resolved_auth.as_ref(),
|
||||
&runtime_request_context,
|
||||
)
|
||||
.await
|
||||
.map(|output| TestRunOutcome::Session { output }),
|
||||
ExecutionMode::AsyncJob => self
|
||||
.start_async_job_test(
|
||||
workspace_id,
|
||||
&record.snapshot,
|
||||
&runtime,
|
||||
&payload.input,
|
||||
&runtime_request_context,
|
||||
)
|
||||
.await
|
||||
.map(|output| TestRunOutcome::AsyncJob { output }),
|
||||
},
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
} {
|
||||
Ok(outcome) => {
|
||||
Ok(response_preview) => {
|
||||
let duration_ms =
|
||||
u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
let (message, response_preview, window, stream_session, async_job) =
|
||||
outcome.into_result_views();
|
||||
self.record_invocation(InvocationRecordRequest {
|
||||
workspace_id,
|
||||
agent_id: None,
|
||||
@@ -1905,7 +1746,7 @@ impl AdminService {
|
||||
source: InvocationSource::AdminTestRun,
|
||||
level: InvocationLevel::Info,
|
||||
status: InvocationStatus::Ok,
|
||||
message: message.to_owned(),
|
||||
message: "admin test run completed".to_owned(),
|
||||
status_code: None,
|
||||
error_kind: None,
|
||||
duration_ms,
|
||||
@@ -1919,9 +1760,6 @@ impl AdminService {
|
||||
request_preview,
|
||||
response_preview,
|
||||
errors: Vec::new(),
|
||||
window,
|
||||
stream_session,
|
||||
async_job,
|
||||
})
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -1949,9 +1787,6 @@ impl AdminService {
|
||||
request_preview,
|
||||
response_preview: Value::Null,
|
||||
errors: vec![crate::error::runtime_test_failure(&error)],
|
||||
window: None,
|
||||
stream_session: None,
|
||||
async_job: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1983,6 +1818,7 @@ impl AdminService {
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
#[cfg(any())]
|
||||
async fn start_stream_session_test(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
@@ -2060,6 +1896,7 @@ impl AdminService {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any())]
|
||||
async fn start_async_job_test(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
@@ -3230,7 +3067,6 @@ impl AdminService {
|
||||
fn validate_operation_payload(&self, payload: &OperationPayload) -> Result<(), ApiError> {
|
||||
self.validate_operation_capabilities(payload.protocol, payload.security_level)?;
|
||||
validate_protocol_target(payload.protocol, &payload.target)?;
|
||||
validate_streaming_policy(&payload.execution_config)?;
|
||||
validate_response_cache_policy(&payload.target, &payload.execution_config)?;
|
||||
payload.input_mapping.validate_paths()?;
|
||||
payload.output_mapping.validate_paths()?;
|
||||
@@ -3240,7 +3076,6 @@ impl AdminService {
|
||||
fn validate_registry_operation(&self, operation: &RegistryOperation) -> Result<(), ApiError> {
|
||||
self.validate_operation_capabilities(operation.protocol, operation.security_level)?;
|
||||
validate_protocol_target(operation.protocol, &operation.target)?;
|
||||
validate_streaming_policy(&operation.execution_config)?;
|
||||
validate_response_cache_policy(&operation.target, &operation.execution_config)?;
|
||||
operation.input_mapping.validate_paths()?;
|
||||
operation.output_mapping.validate_paths()?;
|
||||
@@ -3640,21 +3475,12 @@ fn build_request_preview(
|
||||
"path": prepared.path_params,
|
||||
"query": prepared.query_params,
|
||||
"headers": prepared.headers,
|
||||
"grpc": prepared.grpc.unwrap_or(Value::Null),
|
||||
"variables": prepared.variables.unwrap_or(Value::Null),
|
||||
"body": prepared.body.unwrap_or(Value::Null)
|
||||
}))
|
||||
}
|
||||
|
||||
fn validate_protocol_target(protocol: Protocol, target: &Target) -> Result<(), ApiError> {
|
||||
let is_match = matches!(
|
||||
(protocol, target),
|
||||
(Protocol::Rest, Target::Rest(_))
|
||||
| (Protocol::Graphql, Target::Graphql(_))
|
||||
| (Protocol::Grpc, Target::Grpc(_))
|
||||
| (Protocol::Websocket, Target::Websocket(_))
|
||||
| (Protocol::Soap, Target::Soap(_))
|
||||
);
|
||||
let is_match = matches!((protocol, target), (Protocol::Rest, Target::Rest(_)));
|
||||
|
||||
if is_match {
|
||||
return Ok(());
|
||||
@@ -3825,8 +3651,6 @@ fn demo_rest_operation_payload() -> OperationPayload {
|
||||
response_cache: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
streaming: None,
|
||||
},
|
||||
tool_description: crank_core::ToolDescription {
|
||||
title: "Create CRM Lead".to_owned(),
|
||||
@@ -3893,8 +3717,6 @@ fn demo_archived_operation_payload() -> OperationPayload {
|
||||
response_cache: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
streaming: None,
|
||||
},
|
||||
tool_description: crank_core::ToolDescription {
|
||||
title: "Archive Marketing Contact".to_owned(),
|
||||
@@ -3977,85 +3799,16 @@ fn map_identity_error(error: IdentityError) -> ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_runtime_auth_for_task(
|
||||
registry: &PostgresRegistry,
|
||||
secret_crypto: &SecretCrypto,
|
||||
workspace_id: &WorkspaceId,
|
||||
execution_config: &crank_core::ExecutionConfig,
|
||||
) -> Result<Option<ResolvedAuth>, RuntimeError> {
|
||||
let Some(auth_profile_id) = execution_config.auth_profile_ref.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let auth_profile = registry
|
||||
.get_auth_profile(workspace_id, auth_profile_id)
|
||||
.await
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "load auth profile",
|
||||
details: error.to_string(),
|
||||
})?
|
||||
.ok_or_else(|| RuntimeError::MissingAuthProfile {
|
||||
auth_profile_id: auth_profile_id.as_str().to_owned(),
|
||||
})?;
|
||||
|
||||
let mut secrets = BTreeMap::new();
|
||||
let used_at = OffsetDateTime::now_utc();
|
||||
|
||||
for secret_id in auth_profile.config.secret_ids() {
|
||||
let secret = registry
|
||||
.get_secret(workspace_id, secret_id)
|
||||
.await
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "load secret",
|
||||
details: error.to_string(),
|
||||
})?
|
||||
.ok_or_else(|| RuntimeError::MissingSecret {
|
||||
secret_id: secret_id.as_str().to_owned(),
|
||||
})?;
|
||||
let version = registry
|
||||
.get_current_secret_version(workspace_id, secret_id)
|
||||
.await
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "load current secret version",
|
||||
details: error.to_string(),
|
||||
})?
|
||||
.ok_or_else(|| RuntimeError::MissingSecretVersion {
|
||||
secret_id: secret_id.as_str().to_owned(),
|
||||
version: secret.secret.current_version,
|
||||
})?;
|
||||
let plaintext = secret_crypto.decrypt(
|
||||
&version.secret_version.key_version,
|
||||
&version.secret_version.ciphertext,
|
||||
)?;
|
||||
registry
|
||||
.touch_secret(workspace_id, secret_id, &used_at)
|
||||
.await
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "touch secret",
|
||||
details: error.to_string(),
|
||||
})?;
|
||||
secrets.insert(secret_id.clone(), plaintext);
|
||||
}
|
||||
|
||||
ResolvedAuth::from_profile(&auth_profile, &secrets).map(Some)
|
||||
}
|
||||
|
||||
fn runtime_error_code(error: &RuntimeError) -> &'static str {
|
||||
match error {
|
||||
RuntimeError::Schema(_) => "schema_error",
|
||||
RuntimeError::Mapping(_) => "mapping_error",
|
||||
RuntimeError::InvalidPreparedRequest { .. } => "invalid_request",
|
||||
RuntimeError::GraphqlAdapter(_) => "graphql_error",
|
||||
RuntimeError::GrpcAdapter(_) => "grpc_error",
|
||||
RuntimeError::RestAdapter(_) => "rest_error",
|
||||
RuntimeError::ProtocolAdapter(_) => "adapter_error",
|
||||
RuntimeError::SoapAdapter(_) => "soap_error",
|
||||
RuntimeError::WebsocketAdapter(_) => "websocket_error",
|
||||
RuntimeError::UnsupportedProtocol { .. } => "unsupported_protocol",
|
||||
RuntimeError::ConcurrencyLimitExceeded { .. } => "runtime_overloaded",
|
||||
RuntimeError::MissingStreamingConfig { .. } => "streaming_config_error",
|
||||
RuntimeError::UnsupportedExecutionMode { .. } => "streaming_mode_error",
|
||||
RuntimeError::InvalidStreamingPayload { .. } => "streaming_payload_error",
|
||||
RuntimeError::MissingAuthProfile { .. } => "auth_profile_not_found",
|
||||
RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => {
|
||||
"secret_not_found"
|
||||
@@ -4065,40 +3818,16 @@ fn runtime_error_code(error: &RuntimeError) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn protocol_capability_view(protocol: Protocol, edition: ProductEdition) -> ProtocolCapabilityView {
|
||||
let community_build = matches!(edition, ProductEdition::Community);
|
||||
let supports_execution_modes = if community_build {
|
||||
vec![ExecutionMode::Unary]
|
||||
} else {
|
||||
[
|
||||
ExecutionMode::Unary,
|
||||
ExecutionMode::Window,
|
||||
ExecutionMode::Session,
|
||||
ExecutionMode::AsyncJob,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|mode| protocol.supports_execution_mode(*mode))
|
||||
.collect()
|
||||
};
|
||||
let supports_transport_behaviors = if community_build {
|
||||
vec![TransportBehavior::RequestResponse]
|
||||
} else {
|
||||
[
|
||||
TransportBehavior::RequestResponse,
|
||||
TransportBehavior::ServerStream,
|
||||
TransportBehavior::StatefulSession,
|
||||
TransportBehavior::DeferredResult,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|behavior| protocol.supports_transport_behavior(*behavior))
|
||||
.collect()
|
||||
};
|
||||
fn protocol_capability_view(
|
||||
protocol: Protocol,
|
||||
_edition: ProductEdition,
|
||||
) -> ProtocolCapabilityView {
|
||||
let supports_upload_artifacts = Vec::new();
|
||||
|
||||
ProtocolCapabilityView {
|
||||
protocol,
|
||||
supports_execution_modes,
|
||||
supports_transport_behaviors,
|
||||
supports_execution_modes: vec![ExecutionMode::Unary],
|
||||
supports_transport_behaviors: vec!["request_response".to_owned()],
|
||||
supports_auth_kinds: vec![
|
||||
"none".to_owned(),
|
||||
"bearer".to_owned(),
|
||||
@@ -4107,15 +3836,9 @@ fn protocol_capability_view(protocol: Protocol, edition: ProductEdition) -> Prot
|
||||
"api_key_query".to_owned(),
|
||||
],
|
||||
supports_upload_artifacts,
|
||||
supports_cursor_path: !matches!(protocol, Protocol::Graphql),
|
||||
supports_done_path: !matches!(protocol, Protocol::Graphql),
|
||||
supports_aggregation_mode: vec![
|
||||
AggregationMode::RawItems,
|
||||
AggregationMode::SummaryOnly,
|
||||
AggregationMode::SummaryPlusSamples,
|
||||
AggregationMode::Stats,
|
||||
AggregationMode::LatestState,
|
||||
],
|
||||
supports_cursor_path: false,
|
||||
supports_done_path: false,
|
||||
supports_aggregation_mode: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4246,22 +3969,6 @@ fn agent_mcp_endpoint(workspace_slug: &str, agent_slug: &str) -> String {
|
||||
format!("/mcp/v1/{workspace_slug}/{agent_slug}")
|
||||
}
|
||||
|
||||
fn validate_streaming_policy(
|
||||
execution_config: &crank_core::ExecutionConfig,
|
||||
) -> Result<(), ApiError> {
|
||||
if execution_config.streaming.is_some() {
|
||||
return Err(ApiError::validation_with_context(
|
||||
"streaming execution is not supported in Community".to_owned(),
|
||||
json!({
|
||||
"field": "execution_config.streaming",
|
||||
"edition": "community",
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_response_cache_policy(
|
||||
target: &Target,
|
||||
execution_config: &crank_core::ExecutionConfig,
|
||||
@@ -4279,15 +3986,6 @@ fn validate_response_cache_policy(
|
||||
));
|
||||
}
|
||||
|
||||
if execution_config.streaming.is_some() {
|
||||
return Err(ApiError::validation_with_context(
|
||||
"response cache is supported only for unary operations".to_owned(),
|
||||
json!({
|
||||
"field": "execution_config.response_cache",
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
match target {
|
||||
Target::Rest(rest_target) if rest_target.method == crank_core::HttpMethod::Get => {}
|
||||
_ => {
|
||||
@@ -4330,8 +4028,6 @@ mod tests {
|
||||
response_cache: Some(ResponseCachePolicy { ttl_ms: 5_000 }),
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
streaming: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4350,14 +4046,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_response_cache_for_non_rest_target() {
|
||||
let target = Target::Graphql(crank_core::GraphqlTarget {
|
||||
endpoint: "http://example.invalid/graphql".to_owned(),
|
||||
operation_type: crank_core::GraphqlOperationType::Query,
|
||||
operation_name: "LookupLead".to_owned(),
|
||||
query_template:
|
||||
"query LookupLead($email: String!) { lookupLead(email: $email) { id } }".to_owned(),
|
||||
response_path: "$.response.body.data.lookupLead".to_owned(),
|
||||
fn rejects_response_cache_for_non_get_rest_operation() {
|
||||
let target = Target::Rest(RestTarget {
|
||||
base_url: "http://example.invalid".to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/catalog".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
});
|
||||
|
||||
let error =
|
||||
|
||||
Reference in New Issue
Block a user