#![cfg(feature = "banco")]
use std::time::Duration;
async fn start_server() -> (String, tokio::task::JoinHandle<()>) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let base = format!("http://127.0.0.1:{port}");
let state = batuta::serve::banco::state::BancoStateInner::with_defaults();
let app = batuta::serve::banco::router::create_banco_router(state);
let handle = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
tokio::time::sleep(Duration::from_millis(100)).await;
(base, handle)
}
#[tokio::test]
async fn l2_experiment_add_runs_and_compare() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/v1/experiments"))
.json(&serde_json::json!({"name": "compare-test", "description": "L2 test"}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let exp: serde_json::Value = resp.json().await.unwrap();
let exp_id = exp["id"].as_str().unwrap().to_string();
let resp = client
.post(format!("{base}/api/v1/train/start"))
.json(&serde_json::json!({"dataset_id": "d1", "preset": "quick-lora"}))
.send()
.await
.unwrap();
let run1: serde_json::Value = resp.json().await.unwrap();
let run1_id = run1["id"].as_str().unwrap().to_string();
let resp = client
.post(format!("{base}/api/v1/train/start"))
.json(&serde_json::json!({"dataset_id": "d2", "preset": "standard-lora"}))
.send()
.await
.unwrap();
let run2: serde_json::Value = resp.json().await.unwrap();
let run2_id = run2["id"].as_str().unwrap().to_string();
let resp = client
.post(format!("{base}/api/v1/experiments/{exp_id}/runs"))
.json(&serde_json::json!({"run_id": run1_id}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let resp = client
.post(format!("{base}/api/v1/experiments/{exp_id}/runs"))
.json(&serde_json::json!({"run_id": run2_id}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let resp = reqwest::get(format!("{base}/api/v1/experiments/{exp_id}/compare")).await.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert!(json["runs"].is_array() || json["comparison"].is_object());
handle.abort();
}
#[tokio::test]
async fn l2_audio_transcription_no_file() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/v1/audio/transcriptions"))
.json(&serde_json::json!({"file": "nonexistent.wav", "model": "whisper-1"}))
.send()
.await
.unwrap();
assert!(resp.status().is_success() || resp.status().is_client_error());
handle.abort();
}
#[tokio::test]
async fn l2_file_upload_info_download() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/v1/data/upload/json"))
.json(&serde_json::json!({"name": "test.txt", "content": "File content for download test"}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let file: serde_json::Value = resp.json().await.unwrap();
let fid = file["id"].as_str().unwrap();
let resp = reqwest::get(format!("{base}/api/v1/data/files/{fid}/info")).await.unwrap();
assert_eq!(resp.status(), 200);
let info: serde_json::Value = resp.json().await.unwrap();
assert_eq!(info["name"], "test.txt");
let resp = client.delete(format!("{base}/api/v1/data/files/{fid}")).send().await.unwrap();
assert_eq!(resp.status(), 204);
handle.abort();
}
#[tokio::test]
async fn l2_rag_index_trigger() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client.post(format!("{base}/api/v1/rag/index")).send().await.unwrap();
assert_eq!(resp.status(), 200);
handle.abort();
}
#[tokio::test]
async fn l2_prompt_by_id_and_delete() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/v1/prompts"))
.json(&serde_json::json!({"name": "test-p", "content": "Be helpful."}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let prompt: serde_json::Value = resp.json().await.unwrap();
let pid = prompt["id"].as_str().unwrap().to_string();
let resp = reqwest::get(format!("{base}/api/v1/prompts/{pid}")).await.unwrap();
assert_eq!(resp.status(), 200);
let resp = client.delete(format!("{base}/api/v1/prompts/{pid}")).send().await.unwrap();
assert_eq!(resp.status(), 204);
handle.abort();
}
#[tokio::test]
async fn l2_ollama_show() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/show"))
.json(&serde_json::json!({"name": "local"}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert!(json["modelfile"].is_string());
handle.abort();
}
#[tokio::test]
async fn l2_ollama_pull() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/pull"))
.json(&serde_json::json!({"name": "llama3:8b"}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert_eq!(json["status"], "success");
handle.abort();
}
#[tokio::test]
async fn l2_ollama_delete() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.delete(format!("{base}/api/delete"))
.json(&serde_json::json!({"name": "local"}))
.send()
.await
.unwrap();
assert!(resp.status().is_success());
handle.abort();
}
#[tokio::test]
async fn l2_tool_config() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.put(format!("{base}/api/v1/tools/calculator/config"))
.json(&serde_json::json!({"enabled": true}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
handle.abort();
}
#[tokio::test]
async fn l2_batch_get_by_id() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/v1/batch"))
.json(&serde_json::json!({
"items": [{"id": "x", "messages": [{"role": "user", "content": "Hi"}]}]
}))
.send()
.await
.unwrap();
let batch: serde_json::Value = resp.json().await.unwrap();
let batch_id = batch["batch_id"].as_str().unwrap();
let resp = reqwest::get(format!("{base}/api/v1/batch/{batch_id}")).await.unwrap();
assert_eq!(resp.status(), 200);
handle.abort();
}
#[tokio::test]
async fn l2_eval_run_by_id() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/v1/eval/perplexity"))
.json(&serde_json::json!({"text": "Test text for eval"}))
.send()
.await
.unwrap();
let eval: serde_json::Value = resp.json().await.unwrap();
let eval_id = eval["eval_id"].as_str().unwrap();
let resp = reqwest::get(format!("{base}/api/v1/eval/runs/{eval_id}")).await.unwrap();
assert_eq!(resp.status(), 200);
handle.abort();
}
#[tokio::test]
async fn l2_banco_native_embeddings() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/v1/embeddings"))
.json(&serde_json::json!({"model": "local", "input": "Test"}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let json: serde_json::Value = resp.json().await.unwrap();
assert!(json["data"].is_array());
handle.abort();
}
#[tokio::test]
async fn l2_models_pull() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/v1/models/pull"))
.json(&serde_json::json!({"model_ref": "test-model:latest"}))
.send()
.await
.unwrap();
assert!(resp.status().is_success() || resp.status().is_client_error());
handle.abort();
}
#[tokio::test]
async fn l2_registry_model_by_name() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp =
client.delete(format!("{base}/api/v1/models/registry/nonexistent")).send().await.unwrap();
assert!(resp.status() == 200 || resp.status() == 404);
handle.abort();
}
#[tokio::test]
async fn l2_dataset_preview() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/v1/data/upload/json"))
.json(&serde_json::json!({"name": "p.txt", "content": "Line 1\nLine 2\nLine 3"}))
.send()
.await
.unwrap();
let file: serde_json::Value = resp.json().await.unwrap();
let fid = file["id"].as_str().unwrap().to_string();
let resp = client
.post(format!("{base}/api/v1/data/recipes"))
.json(&serde_json::json!({
"name": "prev", "source_files": [fid],
"steps": [{"type": "extract_text", "config": {}}]
}))
.send()
.await
.unwrap();
let recipe: serde_json::Value = resp.json().await.unwrap();
let rid = recipe["id"].as_str().unwrap().to_string();
let resp = client.post(format!("{base}/api/v1/data/recipes/{rid}/run")).send().await.unwrap();
let ds: serde_json::Value = resp.json().await.unwrap();
let dsid = ds["dataset_id"].as_str().unwrap();
let resp = reqwest::get(format!("{base}/api/v1/data/datasets/{dsid}/preview")).await.unwrap();
assert_eq!(resp.status(), 200);
handle.abort();
}
#[tokio::test]
async fn l2_training_metrics_sse() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/api/v1/train/start"))
.json(&serde_json::json!({"dataset_id": "test", "preset": "quick-lora"}))
.send()
.await
.unwrap();
let run: serde_json::Value = resp.json().await.unwrap();
let id = run["id"].as_str().unwrap();
let resp = reqwest::get(format!("{base}/api/v1/train/runs/{id}/metrics")).await.unwrap();
assert_eq!(resp.status(), 200);
let ct = resp.headers().get("content-type").map(|v| v.to_str().unwrap_or(""));
assert!(ct.unwrap_or("").contains("text/event-stream"), "Should return SSE content type");
handle.abort();
}
#[tokio::test]
async fn l2_static_assets() {
let (base, handle) = start_server().await;
let resp = reqwest::get(format!("{base}/assets/nonexistent.js")).await.unwrap();
assert_eq!(resp.status(), 404);
handle.abort();
}
#[tokio::test]
async fn l2_zero_js_chat_form() {
let (base, handle) = start_server().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{base}/ui/chat"))
.header("content-type", "application/x-www-form-urlencoded")
.body("message=Hello+Banco")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let html = resp.text().await.unwrap();
assert!(html.contains("Hello Banco"), "User message should appear in response");
assert!(html.contains("Banco"), "Page should still contain Banco branding");
assert!(!html.contains("<script>"), "Zero-JS: no script tags");
handle.abort();
}
#[tokio::test]
async fn l2_index_no_javascript() {
let (base, handle) = start_server().await;
let resp = reqwest::get(format!("{base}/")).await.unwrap();
assert_eq!(resp.status(), 200);
let html = resp.text().await.unwrap();
assert!(html.contains("<form"), "Should use HTML form");
assert!(!html.contains("<script>"), "Zero-JS: no inline script tags");
handle.abort();
}
#[tokio::test]
async fn l2_websocket_connect() {
let (base, handle) = start_server().await;
let ws_url = base.replace("http://", "ws://") + "/api/v1/ws";
use tokio_tungstenite::connect_async;
match connect_async(&ws_url).await {
Ok((mut ws, _)) => {
use futures_util::StreamExt;
if let Some(Ok(msg)) = ws.next().await {
let text = msg.to_text().unwrap_or("");
assert!(
text.contains("connected") || text.contains("type"),
"First WS message should be a connected event, got: {text}"
);
}
}
Err(e) => {
eprintln!("[L2] WebSocket connect failed (may be expected): {e}");
}
}
handle.abort();
}