use std::collections::BTreeMap;
use appctl::{
ai::run_agent,
config::{AppConfig, BehaviorConfig, ConfigPaths, ProviderConfig, ProviderKind, TargetConfig},
executor::ExecutionContext,
safety::SafetyMode,
schema::{
Action, AuthStrategy, Field, FieldType, HttpMethod, ParameterLocation, Provenance,
Resource, Safety, Schema, SyncSource, Transport, Verb,
},
tools::schema_to_tools,
};
use serde_json::json;
use tempfile::tempdir;
use uuid::Uuid;
use wiremock::{
Mock, MockServer, ResponseTemplate,
matchers::{body_string_contains, method, path},
};
#[tokio::test]
async fn run_agent_executes_tool_call_and_returns_follow_up_message() {
let llm = MockServer::start().await;
let target = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(body_string_contains("\"role\":\"tool\""))
.and(body_string_contains("\"tool_call_id\":\"call_1\""))
.and(body_string_contains("\"tool_calls\":["))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"choices": [{
"message": {
"content": "Created pet Rex",
"tool_calls": []
}
}]
})))
.mount(&llm)
.await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(body_string_contains("\"role\":\"user\""))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"choices": [{
"message": {
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {
"name": "create_pet",
"arguments": "{\"name\":\"Rex\"}"
}
}]
}
}]
})))
.mount(&llm)
.await;
Mock::given(method("POST"))
.and(path("/pets"))
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
"id": 1,
"name": "Rex"
})))
.mount(&target)
.await;
let dir = tempdir().unwrap();
let paths = ConfigPaths::new(dir.path().join(".appctl"));
let config = AppConfig {
default: "mock".to_string(),
providers: vec![ProviderConfig {
name: "mock".to_string(),
kind: ProviderKind::OpenAiCompatible,
base_url: format!("{}/", llm.uri()),
model: "mock-model".to_string(),
api_key_ref: None,
extra_headers: BTreeMap::new(),
}],
target: TargetConfig::default(),
behavior: BehaviorConfig {
max_iterations: 4,
history_limit: 50,
},
};
config.save(&paths).unwrap();
let schema = Schema {
source: SyncSource::Openapi,
base_url: Some(target.uri()),
auth: AuthStrategy::None,
resources: vec![Resource {
name: "pet".to_string(),
description: None,
fields: vec![],
actions: vec![Action {
name: "create_pet".to_string(),
description: Some("Create a pet".to_string()),
verb: Verb::Create,
transport: Transport::Http {
method: HttpMethod::POST,
path: "/pets".to_string(),
query: vec![],
},
parameters: vec![Field {
name: "name".to_string(),
description: None,
field_type: FieldType::String,
required: true,
location: Some(ParameterLocation::Body),
default: None,
enum_values: vec![],
}],
safety: Safety::Mutating,
resource: Some("pet".to_string()),
provenance: Provenance::Declared,
metadata: Default::default(),
}],
}],
metadata: Default::default(),
};
let tools = schema_to_tools(&schema);
let response = run_agent(
&paths,
&config,
Some("mock"),
None,
"Create a pet named Rex",
&tools,
&schema,
ExecutionContext {
session_id: Uuid::new_v4().to_string(),
safety: SafetyMode {
read_only: false,
dry_run: false,
confirm: true,
strict: false,
},
},
None,
)
.await
.unwrap();
assert_eq!(response, json!("Created pet Rex"));
}