969 lines
28 KiB
Rust
969 lines
28 KiB
Rust
#![allow(dead_code, unused_imports)]
|
|
|
|
use super::common::*;
|
|
|
|
use std::{
|
|
collections::BTreeMap,
|
|
env, fmt,
|
|
sync::Arc,
|
|
time::{SystemTime, UNIX_EPOCH},
|
|
};
|
|
|
|
use async_trait::async_trait;
|
|
use axum::{Json, Router, extract::State, http::HeaderMap, routing::post};
|
|
use crank_core::{
|
|
ExecutionConfig, HttpMethod, MembershipRole, OperationSecurityLevel, Protocol,
|
|
ResponseCachePolicy, RestTarget, SecretKind, Target, ToolDescription, WorkspaceId,
|
|
};
|
|
use crank_core::{IdentityError, IdentityProvider, IdentityProviderKind, LoginOutcome};
|
|
use crank_mapping::{MappingRule, MappingSet};
|
|
use crank_registry::PostgresRegistry;
|
|
use crank_runtime::SecretCrypto;
|
|
use crank_schema::{Schema, SchemaKind};
|
|
use serde_json::{Value, json};
|
|
use serial_test::serial;
|
|
use tokio::net::TcpListener;
|
|
|
|
use admin_api::{
|
|
app::build_app,
|
|
auth::{AuthSettings, BootstrapAdminConfig, hash_password},
|
|
service::{AdminService, AdminServiceBuilder, OperationPayload},
|
|
state::AppState,
|
|
};
|
|
|
|
const DEFAULT_WORKSPACE_ID: &str = "ws_default";
|
|
const TEST_AUTH_EMAIL: &str = "owner@crank.local";
|
|
const TEST_AUTH_PASSWORD: &str = "test-password";
|
|
const TEST_PASSWORD_PEPPER: &str = "test-password-pepper";
|
|
const TEST_SESSION_SECRET: &str = "test-session-secret";
|
|
const TEST_MASTER_KEY: &str = "test-master-key";
|
|
|
|
struct TestServer {
|
|
base_url: String,
|
|
shutdown: Option<tokio::sync::oneshot::Sender<()>>,
|
|
handle: Option<tokio::task::JoinHandle<()>>,
|
|
}
|
|
|
|
impl Drop for TestServer {
|
|
fn drop(&mut self) {
|
|
let shutdown = self.shutdown.take();
|
|
let handle = self.handle.take();
|
|
|
|
tokio::task::block_in_place(|| {
|
|
if let Some(shutdown) = shutdown {
|
|
let _ = shutdown.send(());
|
|
}
|
|
if let Some(handle) = handle {
|
|
let _ = tokio::runtime::Handle::current().block_on(handle);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for TestServer {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter.write_str(&self.base_url)
|
|
}
|
|
}
|
|
|
|
impl AsRef<str> for TestServer {
|
|
fn as_ref(&self) -> &str {
|
|
&self.base_url
|
|
}
|
|
}
|
|
|
|
struct RejectingIdentityProvider;
|
|
|
|
#[async_trait]
|
|
impl IdentityProvider for RejectingIdentityProvider {
|
|
fn id(&self) -> &str {
|
|
"rejecting-test-provider"
|
|
}
|
|
|
|
fn kind(&self) -> IdentityProviderKind {
|
|
IdentityProviderKind::Password
|
|
}
|
|
|
|
async fn login_password(
|
|
&self,
|
|
_payload: crank_core::LoginPayload,
|
|
) -> Result<LoginOutcome, IdentityError> {
|
|
Err(IdentityError::BadCredentials)
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[serial]
|
|
async fn creates_publishes_and_tests_rest_operation() {
|
|
let registry = test_registry().await;
|
|
let storage_root = test_storage_root("lifecycle");
|
|
let (upstream_base_url, observed_upstream_headers) = spawn_correlation_upstream_server().await;
|
|
let base_url = spawn_admin_api(build_test_app(registry.clone(), storage_root)).await;
|
|
let client = authorized_client(&base_url).await;
|
|
|
|
let created = client
|
|
.post(format!("{base_url}/operations"))
|
|
.json(&test_operation_payload(
|
|
&upstream_base_url,
|
|
"crm_create_lead",
|
|
))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
let operation_id = created["operation_id"].as_str().unwrap().to_owned();
|
|
|
|
let listed = client
|
|
.get(format!("{base_url}/operations"))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
let published = client
|
|
.post(format!("{base_url}/operations/{operation_id}/publish"))
|
|
.json(&json!({ "version": 1 }))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
let test_run_response = client
|
|
.post(format!("{base_url}/operations/{operation_id}/test-runs"))
|
|
.header("x-request-id", "req_admin_test_run")
|
|
.header(
|
|
"traceparent",
|
|
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
|
|
)
|
|
.json(&json!({
|
|
"version": 1,
|
|
"input": { "email": "user@example.com" }
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
test_run_response.headers()["x-trace-id"].to_str().unwrap(),
|
|
"0af7651916cd43dd8448eb211c80319c"
|
|
);
|
|
let test_run = test_run_response.json::<Value>().await.unwrap();
|
|
|
|
assert_eq!(listed["items"][0]["name"], "crm_create_lead");
|
|
assert_eq!(
|
|
listed["items"][0]["target_url"],
|
|
format!("{upstream_base_url}/crm/leads")
|
|
);
|
|
assert_eq!(listed["items"][0]["target_action"], "POST");
|
|
assert_eq!(published["published_version"], 1);
|
|
assert_eq!(test_run["ok"], true);
|
|
assert_eq!(
|
|
test_run["request_preview"]["body"]["email"],
|
|
"user@example.com"
|
|
);
|
|
assert_eq!(test_run["response_preview"]["id"], "lead_123");
|
|
let logs = registry
|
|
.list_invocation_logs(crank_registry::ListInvocationLogsQuery {
|
|
workspace_id: &WorkspaceId::new(DEFAULT_WORKSPACE_ID),
|
|
level: None,
|
|
search_text: None,
|
|
source: Some(crank_core::InvocationSource::AdminTestRun),
|
|
operation_id: Some(&crank_core::OperationId::new(&operation_id)),
|
|
agent_id: None,
|
|
created_after: None,
|
|
limit: 10,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(logs.len(), 1);
|
|
assert_eq!(
|
|
logs[0].log.request_id.as_deref(),
|
|
Some("req_admin_test_run")
|
|
);
|
|
assert_eq!(
|
|
logs[0].log.trace_id.as_deref(),
|
|
Some("0af7651916cd43dd8448eb211c80319c")
|
|
);
|
|
let upstream_headers = observed_upstream_headers.lock().await;
|
|
let upstream_headers = upstream_headers.as_ref().unwrap();
|
|
assert_eq!(
|
|
upstream_headers["x-request-id"].to_str().unwrap(),
|
|
"req_admin_test_run"
|
|
);
|
|
assert_eq!(
|
|
&upstream_headers["traceparent"].to_str().unwrap()[3..35],
|
|
"0af7651916cd43dd8448eb211c80319c"
|
|
);
|
|
}
|
|
|
|
async fn spawn_correlation_upstream_server() -> (String, Arc<tokio::sync::Mutex<Option<HeaderMap>>>)
|
|
{
|
|
let observed = Arc::new(tokio::sync::Mutex::new(None));
|
|
let app = Router::new()
|
|
.route("/crm/leads", post(capture_correlation_and_create_lead))
|
|
.with_state(Arc::clone(&observed));
|
|
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}"), observed)
|
|
}
|
|
|
|
async fn capture_correlation_and_create_lead(
|
|
State(observed): State<Arc<tokio::sync::Mutex<Option<HeaderMap>>>>,
|
|
headers: HeaderMap,
|
|
Json(payload): Json<Value>,
|
|
) -> Json<Value> {
|
|
*observed.lock().await = Some(headers);
|
|
Json(json!({
|
|
"id": "lead_123",
|
|
"status": "created",
|
|
"input": payload,
|
|
}))
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[serial]
|
|
async fn updates_archives_and_deletes_operation() {
|
|
let registry = test_registry().await;
|
|
let storage_root = test_storage_root("operation_mutations");
|
|
let upstream_base_url = spawn_upstream_server().await;
|
|
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
|
let client = authorized_client(&base_url).await;
|
|
|
|
let created = client
|
|
.post(format!("{base_url}/operations"))
|
|
.json(&test_operation_payload(
|
|
&upstream_base_url,
|
|
"crm_mutable_operation",
|
|
))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
let operation_id = created["operation_id"].as_str().unwrap().to_owned();
|
|
|
|
let listed = client
|
|
.get(format!("{base_url}/operations"))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(listed["total"], 1);
|
|
assert_eq!(listed["items"][0]["category"], "sales");
|
|
assert_eq!(
|
|
listed["items"][0]["target_url"],
|
|
format!("{upstream_base_url}/crm/leads")
|
|
);
|
|
assert_eq!(listed["items"][0]["target_action"], "POST");
|
|
|
|
let updated = client
|
|
.patch(format!("{base_url}/operations/{operation_id}"))
|
|
.json(&json!({
|
|
"display_name": "Create Lead Updated",
|
|
"category": "marketing",
|
|
"target": {
|
|
"kind": "rest",
|
|
"base_url": upstream_base_url,
|
|
"method": "POST",
|
|
"path_template": "/crm/leads",
|
|
"static_headers": {}
|
|
},
|
|
"input_schema": {
|
|
"type": "object",
|
|
"required": true,
|
|
"nullable": false,
|
|
"fields": {
|
|
"email": {
|
|
"type": "string",
|
|
"required": true,
|
|
"nullable": false
|
|
}
|
|
}
|
|
},
|
|
"output_schema": {
|
|
"type": "object",
|
|
"required": true,
|
|
"nullable": false,
|
|
"fields": {
|
|
"id": {
|
|
"type": "string",
|
|
"required": true,
|
|
"nullable": false
|
|
}
|
|
}
|
|
},
|
|
"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": "Create Lead Updated",
|
|
"description": "Creates a CRM lead",
|
|
"tags": ["crm"]
|
|
}
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(updated["status"], "draft");
|
|
|
|
let detail = client
|
|
.get(format!("{base_url}/operations/{operation_id}"))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(detail["display_name"], "Create Lead Updated");
|
|
assert_eq!(detail["category"], "marketing");
|
|
|
|
let archived = client
|
|
.post(format!("{base_url}/operations/{operation_id}/archive"))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(archived["status"], "archived");
|
|
|
|
let deleted = client
|
|
.delete(format!("{base_url}/operations/{operation_id}"))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(deleted["operation_id"], operation_id);
|
|
|
|
let missing = client
|
|
.get(format!("{base_url}/operations/{operation_id}"))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let missing_status = missing.status();
|
|
let missing = missing.json::<Value>().await.unwrap();
|
|
assert_eq!(missing_status, reqwest::StatusCode::NOT_FOUND);
|
|
assert_eq!(missing["error"]["code"], "not_found");
|
|
assert_eq!(
|
|
missing["error"]["context"],
|
|
json!({
|
|
"operation_id": operation_id
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[serial]
|
|
async fn creates_binds_and_publishes_agent() {
|
|
let registry = test_registry().await;
|
|
let storage_root = test_storage_root("agent_lifecycle");
|
|
let upstream_base_url = spawn_upstream_server().await;
|
|
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
|
let client = authorized_client(&base_url).await;
|
|
|
|
let operation = client
|
|
.post(format!("{base_url}/operations"))
|
|
.json(&test_operation_payload(
|
|
&upstream_base_url,
|
|
"crm_create_lead_agent",
|
|
))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let operation = assert_success_json(operation).await;
|
|
let operation_id = operation["operation_id"].as_str().unwrap().to_owned();
|
|
|
|
client
|
|
.post(format!("{base_url}/operations/{operation_id}/publish"))
|
|
.json(&json!({ "version": 1 }))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
|
|
let agent = client
|
|
.post(format!("{base_url}/agents"))
|
|
.json(&json!({
|
|
"slug": "sales-assistant",
|
|
"display_name": "Sales Assistant",
|
|
"description": "Curated sales toolset",
|
|
"instructions": {},
|
|
"tool_selection_policy": {}
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
let agent_id = agent["agent_id"].as_str().unwrap().to_owned();
|
|
|
|
let bindings = client
|
|
.post(format!("{base_url}/agents/{agent_id}/bindings"))
|
|
.json(&json!([
|
|
{
|
|
"operation_id": operation_id,
|
|
"operation_version": 1,
|
|
"tool_name": "crm_create_lead_agent",
|
|
"tool_title": "Create Lead",
|
|
"enabled": true
|
|
}
|
|
]))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
let published = client
|
|
.post(format!("{base_url}/agents/{agent_id}/publish"))
|
|
.json(&json!({ "version": 1 }))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
bindings["bindings"][0]["tool_name"],
|
|
"crm_create_lead_agent"
|
|
);
|
|
assert_eq!(published["published_version"], 1);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[serial]
|
|
async fn saves_and_previews_versioned_agent_tool_search_policy() {
|
|
let registry = test_registry().await;
|
|
let storage_root = test_storage_root("agent_tool_search");
|
|
let upstream_base_url = spawn_upstream_server().await;
|
|
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
|
let client = authorized_client(&base_url).await;
|
|
|
|
let operation = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/operations"))
|
|
.json(&test_operation_payload(
|
|
&upstream_base_url,
|
|
"finance_create_invoice",
|
|
))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
let operation_id = operation["operation_id"].as_str().unwrap().to_owned();
|
|
assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/operations/{operation_id}/publish"))
|
|
.json(&json!({"version": 1}))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
|
|
let agent = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/agents"))
|
|
.json(&json!({
|
|
"slug": "finance-agent",
|
|
"display_name": "Finance Agent",
|
|
"description": "Finance workflows",
|
|
"instructions": {},
|
|
"tool_selection_policy": {}
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
let agent_id = agent["agent_id"].as_str().unwrap().to_owned();
|
|
let catalog = json!({
|
|
"bindings": [{
|
|
"operation_id": operation_id,
|
|
"operation_version": 1,
|
|
"tool_name": "finance_create_invoice",
|
|
"tool_title": "Create Lead",
|
|
"tool_description_override": "Creates an invoice for a customer",
|
|
"enabled": true
|
|
}],
|
|
"tool_selection_policy": {
|
|
"mode": "search",
|
|
"groups": [{
|
|
"id": "finance",
|
|
"name": "Finance",
|
|
"description": "Invoices and payments",
|
|
"tool_names": ["finance_create_invoice"]
|
|
}],
|
|
"search": {"max_results": 5}
|
|
}
|
|
});
|
|
let saved = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/agents/{agent_id}/bindings"))
|
|
.json(&catalog)
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
assert_eq!(saved["snapshot"]["tool_selection_policy"]["mode"], "search");
|
|
|
|
let preview = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/agents/tool-search/preview"))
|
|
.json(&json!({
|
|
"query": "create invoice",
|
|
"group_ids": ["finance"],
|
|
"bindings": catalog["bindings"],
|
|
"tool_selection_policy": catalog["tool_selection_policy"]
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
preview["items"][0]["tool"]["name"],
|
|
"finance_create_invoice"
|
|
);
|
|
|
|
let published = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/agents/{agent_id}/publish"))
|
|
.json(&json!({"version": 1}))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
assert_eq!(published["published_version"], 1);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[serial]
|
|
async fn agent_publish_skips_draft_operation_bindings_and_preserves_draft() {
|
|
let registry = test_registry().await;
|
|
let storage_root = test_storage_root("agent_publish_filters_drafts");
|
|
let upstream_base_url = spawn_upstream_server().await;
|
|
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
|
let client = authorized_client(&base_url).await;
|
|
|
|
let published_operation = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/operations"))
|
|
.json(&test_operation_payload(
|
|
&upstream_base_url,
|
|
"crm_published_tool",
|
|
))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
let published_operation_id = published_operation["operation_id"]
|
|
.as_str()
|
|
.unwrap()
|
|
.to_owned();
|
|
assert_success_json(
|
|
client
|
|
.post(format!(
|
|
"{base_url}/operations/{published_operation_id}/publish"
|
|
))
|
|
.json(&json!({ "version": 1 }))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
|
|
let draft_operation = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/operations"))
|
|
.json(&test_operation_payload(
|
|
&upstream_base_url,
|
|
"crm_draft_tool",
|
|
))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
let draft_operation_id = draft_operation["operation_id"].as_str().unwrap().to_owned();
|
|
|
|
let agent = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/agents"))
|
|
.json(&json!({
|
|
"slug": "collaborative-agent",
|
|
"display_name": "Collaborative Agent",
|
|
"description": "Agent with published and draft tools",
|
|
"instructions": {},
|
|
"tool_selection_policy": {}
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
let agent_id = agent["agent_id"].as_str().unwrap().to_owned();
|
|
|
|
assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/agents/{agent_id}/bindings"))
|
|
.json(&json!([
|
|
{
|
|
"operation_id": published_operation_id,
|
|
"operation_version": 1,
|
|
"tool_name": "crm_published_tool",
|
|
"tool_title": "Published Tool",
|
|
"tool_description_override": null,
|
|
"enabled": true
|
|
},
|
|
{
|
|
"operation_id": draft_operation_id,
|
|
"operation_version": 1,
|
|
"tool_name": "crm_draft_tool",
|
|
"tool_title": "Draft Tool",
|
|
"tool_description_override": null,
|
|
"enabled": true
|
|
}
|
|
]))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
|
|
let published = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/agents/{agent_id}/publish"))
|
|
.json(&json!({ "version": 1 }))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
let agent_detail = assert_success_json(
|
|
client
|
|
.get(format!("{base_url}/agents/{agent_id}"))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
let published_version = assert_success_json(
|
|
client
|
|
.get(format!("{base_url}/agents/{agent_id}/versions/1"))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
let draft_version = assert_success_json(
|
|
client
|
|
.get(format!("{base_url}/agents/{agent_id}/versions/2"))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(published["published_version"], 1);
|
|
assert_eq!(agent_detail["current_draft_version"], 2);
|
|
assert_eq!(agent_detail["latest_published_version"], 1);
|
|
assert_eq!(published_version["bindings"].as_array().unwrap().len(), 1);
|
|
assert_eq!(
|
|
published_version["bindings"][0]["operation_id"],
|
|
published_operation_id
|
|
);
|
|
assert_eq!(draft_version["bindings"].as_array().unwrap().len(), 2);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[serial]
|
|
async fn updates_lists_and_deletes_agent() {
|
|
let registry = test_registry().await;
|
|
let storage_root = test_storage_root("agent_mutations");
|
|
let upstream_base_url = spawn_upstream_server().await;
|
|
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
|
let client = authorized_client(&base_url).await;
|
|
|
|
let operation = client
|
|
.post(format!("{base_url}/operations"))
|
|
.json(&test_operation_payload(
|
|
&upstream_base_url,
|
|
"crm_create_lead_agents_page",
|
|
))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let operation = assert_success_json(operation).await;
|
|
let operation_id = operation["operation_id"].as_str().unwrap().to_owned();
|
|
|
|
client
|
|
.post(format!("{base_url}/operations/{operation_id}/publish"))
|
|
.json(&json!({ "version": 1 }))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
|
|
let created = client
|
|
.post(format!("{base_url}/agents"))
|
|
.json(&json!({
|
|
"slug": "support-team",
|
|
"display_name": "Support Team",
|
|
"description": "Support workflows",
|
|
"instructions": {},
|
|
"tool_selection_policy": {}
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
let agent_id = created["agent_id"].as_str().unwrap().to_owned();
|
|
|
|
client
|
|
.post(format!("{base_url}/agents/{agent_id}/bindings"))
|
|
.json(&json!([
|
|
{
|
|
"operation_id": operation_id,
|
|
"operation_version": 1,
|
|
"tool_name": "crm_create_lead_agents_page",
|
|
"tool_title": "Create Lead",
|
|
"tool_description_override": null,
|
|
"enabled": true
|
|
}
|
|
]))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
|
|
client
|
|
.post(format!("{base_url}/agents/{agent_id}/publish"))
|
|
.json(&json!({ "version": 1 }))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
|
|
let listed = client
|
|
.get(format!("{base_url}/agents"))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(listed["items"][0]["operation_count"], 1);
|
|
assert_eq!(listed["items"][0]["operation_ids"][0], operation_id);
|
|
assert_eq!(
|
|
listed["items"][0]["mcp_endpoint"],
|
|
"/mcp/v1/default/support-team"
|
|
);
|
|
assert_eq!(listed["items"][0]["status"], "published");
|
|
|
|
let updated = client
|
|
.patch(format!("{base_url}/agents/{agent_id}"))
|
|
.json(&json!({
|
|
"slug": "support-escalation",
|
|
"display_name": "Support Escalation",
|
|
"description": "Escalation workflows"
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(updated["agent_id"], agent_id);
|
|
|
|
let detail = client
|
|
.get(format!("{base_url}/agents/{agent_id}"))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(detail["slug"], "support-escalation");
|
|
assert_eq!(detail["display_name"], "Support Escalation");
|
|
assert_eq!(detail["operation_count"], 1);
|
|
assert_eq!(detail["mcp_endpoint"], "/mcp/v1/default/support-escalation");
|
|
|
|
let deleted = client
|
|
.delete(format!("{base_url}/agents/{agent_id}"))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json::<Value>()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(deleted["agent_id"], agent_id);
|
|
|
|
let missing = client
|
|
.get(format!("{base_url}/agents/{agent_id}"))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let missing_status = missing.status();
|
|
let missing = missing.json::<Value>().await.unwrap();
|
|
assert_eq!(missing_status, reqwest::StatusCode::NOT_FOUND);
|
|
assert_eq!(missing["error"]["code"], "not_found");
|
|
assert_eq!(
|
|
missing["error"]["context"],
|
|
json!({
|
|
"agent_id": agent_id
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[serial]
|
|
async fn unpublishes_and_archives_agent() {
|
|
let registry = test_registry().await;
|
|
let storage_root = test_storage_root("agent_statuses");
|
|
let upstream_base_url = spawn_upstream_server().await;
|
|
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
|
let client = authorized_client(&base_url).await;
|
|
|
|
let operation = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/operations"))
|
|
.json(&test_operation_payload(
|
|
&upstream_base_url,
|
|
"crm_create_lead_agent_status",
|
|
))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
let operation_id = operation["operation_id"].as_str().unwrap().to_owned();
|
|
|
|
client
|
|
.post(format!("{base_url}/operations/{operation_id}/publish"))
|
|
.json(&json!({ "version": 1 }))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
|
|
let created = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/agents"))
|
|
.json(&json!({
|
|
"slug": "sales-routing",
|
|
"display_name": "Sales Routing",
|
|
"description": "Routing agent",
|
|
"instructions": {},
|
|
"tool_selection_policy": {}
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
let agent_id = created["agent_id"].as_str().unwrap().to_owned();
|
|
|
|
client
|
|
.post(format!("{base_url}/agents/{agent_id}/bindings"))
|
|
.json(&json!([
|
|
{
|
|
"operation_id": operation_id,
|
|
"operation_version": 1,
|
|
"tool_name": "crm_create_lead_agent_status",
|
|
"tool_title": "Create Lead",
|
|
"tool_description_override": null,
|
|
"enabled": true
|
|
}
|
|
]))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
|
|
client
|
|
.post(format!("{base_url}/agents/{agent_id}/publish"))
|
|
.json(&json!({ "version": 1 }))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
|
|
let unpublished = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/agents/{agent_id}/unpublish"))
|
|
.json(&json!({}))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
assert_eq!(unpublished["agent_id"], agent_id);
|
|
|
|
let draft_detail = assert_success_json(
|
|
client
|
|
.get(format!("{base_url}/agents/{agent_id}"))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
assert_eq!(draft_detail["status"], "draft");
|
|
assert_eq!(draft_detail["latest_published_version"], 1);
|
|
|
|
let archived = assert_success_json(
|
|
client
|
|
.post(format!("{base_url}/agents/{agent_id}/archive"))
|
|
.json(&json!({}))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
assert_eq!(archived["agent_id"], agent_id);
|
|
|
|
let archived_detail = assert_success_json(
|
|
client
|
|
.get(format!("{base_url}/agents/{agent_id}"))
|
|
.send()
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
.await;
|
|
assert_eq!(archived_detail["status"], "archived");
|
|
}
|