use super::*;
async fn app_with(label: &str, ai_section: &str) -> (AppState, std::path::PathBuf, TempDatabase) {
let db = TempDatabase::create(label).await;
let root = temp_dir(label);
write_files(
&root,
&[(
"main.toml",
&format!(
"\n[server]\nbase_path = \"/api\"\n\n[database]\nurl = \"{}\"\n{ai_section}",
db.url
),
)],
);
let state = load_state(&root).await;
(state, root, db)
}
async fn app_with_files(
label: &str,
ai_section: &str,
files: &[(&str, &str)],
) -> (AppState, std::path::PathBuf, TempDatabase) {
let db = TempDatabase::create(label).await;
let root = temp_dir(label);
let mut all = vec![(
"main.toml",
format!(
"\n[server]\nbase_path = \"/api\"\n\n[database]\nurl = \"{}\"\n{ai_section}",
db.url
),
)];
all.extend(files.iter().map(|(path, contents)| (*path, (*contents).to_string())));
let refs = all
.iter()
.map(|(path, contents)| (*path, contents.as_str()))
.collect::<Vec<_>>();
write_files(&root, &refs);
let state = load_state(&root).await;
(state, root, db)
}
#[ntex::test]
async fn without_a_provider_there_are_no_ai_endpoints() {
let (state, root, db) = app_with("noai", "").await;
assert!(!state.ai_enabled());
let app = init_http_app!(state);
for request in [
test::TestRequest::get().uri("/api/ai/config").to_request(),
req_json("POST", "/api/ai/chat", json!({"messages":[]})),
] {
let status = test::call_service(&app, request).await.status().as_u16();
assert!(
(400..500).contains(&status),
"an app with no assistant answered {status}"
);
}
fs::remove_dir_all(root).unwrap();
db.cleanup().await;
}
#[ntex::test]
async fn a_configured_assistant_is_described_publicly_and_answers_nobody_anonymous() {
let (state, root, db) = app_with(
"withai",
"\n[ai]\nprovider = \"custom\"\nendpoint = \"http://localhost:8080\"\nmodel = \"local\"\n",
)
.await;
assert!(state.ai_enabled());
let app = init_http_app!(state);
let response =
test::call_service(&app, test::TestRequest::get().uri("/api/ai/config").to_request()).await;
assert_eq!(response.status().as_u16(), 200);
let config = read_json(response).await;
assert_eq!(config["provider"], "custom");
assert_eq!(config["model"], "local");
assert_eq!(config["access"], "authenticated");
assert!(config.get("api_key").is_none());
assert_eq!(
test::call_service(
&app,
req_json(
"POST",
"/api/ai/chat",
json!({"messages":[{"role":"user","content":"hi"}]})
)
)
.await
.status()
.as_u16(),
401
);
fs::remove_dir_all(root).unwrap();
db.cleanup().await;
}
#[ntex::test]
async fn a_public_assistant_lets_an_anonymous_caller_reach_the_provider() {
let (state, root, db) = app_with(
"publicai",
"\n[ai]\nprovider = \"custom\"\nendpoint = \"http://127.0.0.1:1\"\naccess = \"public\"\ntimeout_secs = 2\n",
)
.await;
let app = init_http_app!(state);
let response = test::call_service(
&app,
req_json(
"POST",
"/api/ai/chat",
json!({"messages":[{"role":"user","content":"hi"}]}),
),
)
.await;
assert_eq!(response.status().as_u16(), 502);
fs::remove_dir_all(root).unwrap();
db.cleanup().await;
}
#[ntex::test]
async fn a_stored_agent_persists_the_thread_even_if_the_provider_refuses() {
let (state, root, db) = app_with_files(
"agent",
"\n[ai]\nprovider = \"custom\"\nendpoint = \"http://127.0.0.1:1\"\nmodel = \"local\"\ntimeout_secs = 2\n",
&[(
"agents/coach.toml",
r#"
[agent]
name = "coach"
description = "A stored coach."
system = "Be concise."
storage.enabled = true
[permissions]
chat = "authenticated"
history = "owner"
"#,
)],
)
.await;
let app = init_http_app!(state);
let registration = read_json(
test::call_service(
&app,
req_json(
"POST",
"/api/auth/register",
json!({"email":"ann@example.test","password":"hunter2"}),
),
)
.await,
)
.await;
let token = registration["token"].as_str().unwrap().to_string();
let config = read_json(
test::call_service(&app, test::TestRequest::get().uri("/api/ai/config").to_request()).await,
)
.await;
assert_eq!(config["agents"][0]["name"], "coach");
assert_eq!(config["agents"][0]["storage"], true);
let response = test::call_service(
&app,
bearer(
test::TestRequest::post()
.uri("/api/ai/agents/coach/chat")
.header(CONTENT_TYPE, "application/json")
.set_payload(json!({"message":"hello there"}).to_string()),
&token,
)
.to_request(),
)
.await;
assert_eq!(response.status().as_u16(), 502);
let threads = read_json(
test::call_service(
&app,
bearer(test::TestRequest::get().uri("/api/ai_coach_thread"), &token).to_request(),
)
.await,
)
.await;
let threads = threads.as_array().unwrap();
assert_eq!(threads.len(), 1);
let messages = read_json(
test::call_service(
&app,
bearer(test::TestRequest::get().uri("/api/ai_coach_message"), &token).to_request(),
)
.await,
)
.await;
let messages = messages.as_array().unwrap();
assert_eq!(messages.len(), 1);
assert_eq!(messages[0]["role"], "user");
assert_eq!(messages[0]["content"], "hello there");
fs::remove_dir_all(root).unwrap();
db.cleanup().await;
}