793 lines
23 KiB
Rust
793 lines
23 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, 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 = 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_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 = client
|
|
.post(format!("{base_url}/operations/{operation_id}/test-runs"))
|
|
.json(&json!({
|
|
"version": 1,
|
|
"input": { "email": "user@example.com" }
|
|
}))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.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");
|
|
}
|
|
|
|
#[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 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");
|
|
}
|