f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
843 lines
29 KiB
Rust
843 lines
29 KiB
Rust
mod common;
|
|
|
|
use axum::http::StatusCode;
|
|
use serde_json::json;
|
|
use tower::ServiceExt;
|
|
|
|
use common::{
|
|
body_json, build_app, build_app_with_mock_agents, delete_with_token, get_with_token, json_with_token,
|
|
setup_and_login,
|
|
};
|
|
|
|
fn two_agent_body() -> serde_json::Value {
|
|
json!({
|
|
"name": "Alpha",
|
|
"agents": [
|
|
{ "name": "Lead", "role": "lead", "backend": "acp", "model": "claude" },
|
|
{ "name": "Worker", "role": "teammate", "backend": "acp", "model": "claude" }
|
|
]
|
|
})
|
|
}
|
|
|
|
async fn create_team(app: &mut axum::Router, token: &str, csrf: &str) -> serde_json::Value {
|
|
let req = json_with_token("POST", "/api/teams", two_agent_body(), token, csrf);
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
let json = body_json(resp).await;
|
|
assert!(json["success"].as_bool().unwrap());
|
|
json["data"].clone()
|
|
}
|
|
|
|
// ===========================================================================
|
|
// §1 Team CRUD (TC-*, TL-*, TG-*, TD-*, TR-*)
|
|
// ===========================================================================
|
|
|
|
// TC-1: Create team with multiple agents
|
|
#[tokio::test]
|
|
async fn tc1_create_team_with_multiple_agents() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
assert_eq!(data["name"], "Alpha");
|
|
assert_eq!(data["agents"].as_array().unwrap().len(), 2);
|
|
assert_eq!(data["agents"][0]["role"], "lead");
|
|
assert_eq!(data["agents"][1]["role"], "teammate");
|
|
assert!(data["lead_agent_id"].is_string());
|
|
assert_eq!(data["lead_agent_id"], data["agents"][0]["slot_id"]);
|
|
}
|
|
|
|
// TC-2: Create single agent team
|
|
#[tokio::test]
|
|
async fn tc2_create_single_agent_team() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let body = json!({
|
|
"name": "Solo",
|
|
"agents": [{ "name": "Lead", "role": "lead", "backend": "acp", "model": "claude" }]
|
|
});
|
|
let req = json_with_token("POST", "/api/teams", body, &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"]["agents"].as_array().unwrap().len(), 1);
|
|
}
|
|
|
|
// TC-3: Each agent has a conversation
|
|
#[tokio::test]
|
|
async fn tc3_each_agent_has_conversation_id() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
for agent in data["agents"].as_array().unwrap() {
|
|
assert!(agent["conversation_id"].is_i64());
|
|
assert!(agent["conversation_id"].as_i64().unwrap() > 0);
|
|
}
|
|
assert_ne!(
|
|
data["agents"][0]["conversation_id"],
|
|
data["agents"][1]["conversation_id"]
|
|
);
|
|
}
|
|
|
|
// TC-4: First agent defaults to lead
|
|
#[tokio::test]
|
|
async fn tc4_first_agent_is_lead() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let body = json!({
|
|
"name": "T",
|
|
"agents": [
|
|
{ "name": "A", "role": "teammate", "backend": "acp", "model": "claude" },
|
|
{ "name": "B", "role": "teammate", "backend": "acp", "model": "claude" }
|
|
]
|
|
});
|
|
let req = json_with_token("POST", "/api/teams", body, &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"]["agents"][0]["role"], "lead");
|
|
assert_eq!(json["data"]["lead_agent_id"], json["data"]["agents"][0]["slot_id"]);
|
|
}
|
|
|
|
// TC-5: Empty agents returns 400
|
|
#[tokio::test]
|
|
async fn tc5_empty_agents_returns_error() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let body = json!({ "name": "Empty", "agents": [] });
|
|
let req = json_with_token("POST", "/api/teams", body, &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
|
}
|
|
|
|
// TC-6: Missing name returns 400
|
|
#[tokio::test]
|
|
async fn tc6_missing_name_returns_error() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let body = json!({ "agents": [{ "name": "L", "role": "lead", "backend": "acp", "model": "c" }] });
|
|
let req = json_with_token("POST", "/api/teams", body, &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn tc6b_workspace_with_edge_whitespace_segment_returns_specific_code() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let body = json!({
|
|
"name": "Alpha",
|
|
"workspace": "/Users/zhoukai/Documents/Archive ",
|
|
"agents": [{ "name": "Lead", "role": "lead", "backend": "acp", "model": "claude" }]
|
|
});
|
|
let req = json_with_token("POST", "/api/teams", body, &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["code"], "WORKSPACE_PATH_EDGE_WHITESPACE_UNSUPPORTED");
|
|
assert!(
|
|
json["error"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("begins or ends with whitespace")
|
|
);
|
|
}
|
|
|
|
// TC-7: Unauthenticated returns 403
|
|
#[tokio::test]
|
|
async fn tc7_unauthenticated_returns_403() {
|
|
let (app, _services) = build_app().await;
|
|
|
|
let req = axum::http::Request::builder()
|
|
.method("GET")
|
|
.uri("/api/teams")
|
|
.body(axum::body::Body::empty())
|
|
.unwrap();
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
|
}
|
|
|
|
// TL-1: Empty team list
|
|
#[tokio::test]
|
|
async fn tl1_empty_team_list() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, _csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let req = get_with_token("/api/teams", &token);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
let json = body_json(resp).await;
|
|
assert!(json["data"].as_array().unwrap().is_empty());
|
|
}
|
|
|
|
// TL-2: List multiple teams
|
|
#[tokio::test]
|
|
async fn tl2_list_multiple_teams() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
create_team(&mut app, &token, &csrf).await;
|
|
|
|
let body = json!({
|
|
"name": "Beta",
|
|
"agents": [{ "name": "Lead", "role": "lead", "backend": "acp", "model": "claude" }]
|
|
});
|
|
let req = json_with_token("POST", "/api/teams", body, &token, &csrf);
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
|
|
let req = get_with_token("/api/teams", &token);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"].as_array().unwrap().len(), 2);
|
|
}
|
|
|
|
// TL-3: Each team contains full agents info
|
|
#[tokio::test]
|
|
async fn tl3_teams_contain_full_agent_info() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
create_team(&mut app, &token, &csrf).await;
|
|
|
|
let req = get_with_token("/api/teams", &token);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
let json = body_json(resp).await;
|
|
let teams = json["data"].as_array().unwrap();
|
|
let agent = &teams[0]["agents"][0];
|
|
assert!(agent["slot_id"].is_string());
|
|
assert!(agent["name"].is_string());
|
|
assert!(agent["role"].is_string());
|
|
assert!(agent["conversation_id"].is_i64());
|
|
assert!(agent["backend"].is_string());
|
|
assert!(agent["model"].is_string());
|
|
}
|
|
|
|
// TG-1: Get existing team
|
|
#[tokio::test]
|
|
async fn tg1_get_existing_team() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = get_with_token(&format!("/api/teams/{team_id}"), &token);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"]["id"], team_id);
|
|
assert_eq!(json["data"]["name"], "Alpha");
|
|
}
|
|
|
|
// TG-2: Get nonexistent team returns 404
|
|
#[tokio::test]
|
|
async fn tg2_get_nonexistent_returns_404() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, _csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let req = get_with_token("/api/teams/nonexistent", &token);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
// TD-1: Delete existing team
|
|
#[tokio::test]
|
|
async fn td1_delete_existing_team() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = delete_with_token(&format!("/api/teams/{team_id}"), &token, &csrf);
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
// TD-2: Delete then list confirms removal
|
|
#[tokio::test]
|
|
async fn td2_delete_then_list_empty() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = delete_with_token(&format!("/api/teams/{team_id}"), &token, &csrf);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
let req = get_with_token("/api/teams", &token);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
let json = body_json(resp).await;
|
|
assert!(json["data"].as_array().unwrap().is_empty());
|
|
}
|
|
|
|
// TD-6: Delete nonexistent team returns 404
|
|
#[tokio::test]
|
|
async fn td6_delete_nonexistent_returns_404() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let req = delete_with_token("/api/teams/nonexistent", &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
// TR-1: Rename existing team
|
|
#[tokio::test]
|
|
async fn tr1_rename_existing_team() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = json_with_token(
|
|
"PATCH",
|
|
&format!("/api/teams/{team_id}/name"),
|
|
json!({ "name": "New Name" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
// TR-2: Rename then get confirms new name
|
|
#[tokio::test]
|
|
async fn tr2_rename_then_get_confirms_new_name() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = json_with_token(
|
|
"PATCH",
|
|
&format!("/api/teams/{team_id}/name"),
|
|
json!({ "name": "New Name" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
let req = get_with_token(&format!("/api/teams/{team_id}"), &token);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"]["name"], "New Name");
|
|
}
|
|
|
|
// TR-4: Rename nonexistent team returns 404
|
|
#[tokio::test]
|
|
async fn tr4_rename_nonexistent_returns_404() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let req = json_with_token(
|
|
"PATCH",
|
|
"/api/teams/nonexistent/name",
|
|
json!({ "name": "X" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// §2 Agent Management (AA-*, AR-*, AN-*)
|
|
// ===========================================================================
|
|
|
|
// AA-1: Add agent to team
|
|
#[tokio::test]
|
|
async fn aa1_add_agent_to_team() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let body = json!({
|
|
"name": "New Agent",
|
|
"role": "teammate",
|
|
"backend": "acp",
|
|
"model": "claude"
|
|
});
|
|
let req = json_with_token("POST", &format!("/api/teams/{team_id}/agents"), body, &token, &csrf);
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"]["name"], "New Agent");
|
|
assert!(json["data"]["conversation_id"].is_i64());
|
|
}
|
|
|
|
// AA-2: After adding, agent count increases
|
|
#[tokio::test]
|
|
async fn aa2_add_agent_increases_count() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let body = json!({ "name": "X", "role": "teammate", "backend": "acp", "model": "claude" });
|
|
let req = json_with_token("POST", &format!("/api/teams/{team_id}/agents"), body, &token, &csrf);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
let req = get_with_token(&format!("/api/teams/{team_id}"), &token);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"]["agents"].as_array().unwrap().len(), 3);
|
|
}
|
|
|
|
// AA-4: Add agent to nonexistent team returns 404
|
|
#[tokio::test]
|
|
async fn aa4_add_agent_nonexistent_team() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let body = json!({ "name": "X", "role": "teammate", "backend": "acp", "model": "claude" });
|
|
let req = json_with_token("POST", "/api/teams/nonexistent/agents", body, &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
// AA-5: Missing required fields returns 400
|
|
#[tokio::test]
|
|
async fn aa5_add_agent_missing_fields() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let body = json!({ "role": "teammate", "backend": "acp" });
|
|
let req = json_with_token("POST", &format!("/api/teams/{team_id}/agents"), body, &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
|
}
|
|
|
|
// AR-1: Remove agent from team
|
|
#[tokio::test]
|
|
async fn ar1_remove_agent_from_team() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
let slot_id = data["agents"][1]["slot_id"].as_str().unwrap();
|
|
|
|
let req = delete_with_token(&format!("/api/teams/{team_id}/agents/{slot_id}"), &token, &csrf);
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
// AR-2: After removal, agent not in team
|
|
#[tokio::test]
|
|
async fn ar2_after_removal_agent_gone() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
let slot_id = data["agents"][1]["slot_id"].as_str().unwrap();
|
|
|
|
let req = delete_with_token(&format!("/api/teams/{team_id}/agents/{slot_id}"), &token, &csrf);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
let req = get_with_token(&format!("/api/teams/{team_id}"), &token);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
let json = body_json(resp).await;
|
|
let agents = json["data"]["agents"].as_array().unwrap();
|
|
assert_eq!(agents.len(), 1);
|
|
assert!(agents.iter().all(|a| a["slot_id"] != slot_id));
|
|
}
|
|
|
|
// AR-4: Remove nonexistent agent returns 404
|
|
#[tokio::test]
|
|
async fn ar4_remove_nonexistent_agent() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = delete_with_token(&format!("/api/teams/{team_id}/agents/nonexistent"), &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
// AN-1: Rename agent
|
|
#[tokio::test]
|
|
async fn an1_rename_agent() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
let slot_id = data["agents"][1]["slot_id"].as_str().unwrap();
|
|
|
|
let req = json_with_token(
|
|
"PATCH",
|
|
&format!("/api/teams/{team_id}/agents/{slot_id}/name"),
|
|
json!({ "name": "Senior Worker" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
// AN-2: Rename then get confirms new name
|
|
#[tokio::test]
|
|
async fn an2_rename_then_get_confirms_name() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
let slot_id = data["agents"][1]["slot_id"].as_str().unwrap();
|
|
|
|
let req = json_with_token(
|
|
"PATCH",
|
|
&format!("/api/teams/{team_id}/agents/{slot_id}/name"),
|
|
json!({ "name": "Senior Worker" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
let req = get_with_token(&format!("/api/teams/{team_id}"), &token);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
let json = body_json(resp).await;
|
|
let agents = json["data"]["agents"].as_array().unwrap();
|
|
let agent = agents.iter().find(|a| a["slot_id"] == slot_id).unwrap();
|
|
assert_eq!(agent["name"], "Senior Worker");
|
|
}
|
|
|
|
// AN-3: Rename nonexistent agent returns 404
|
|
#[tokio::test]
|
|
async fn an3_rename_nonexistent_agent() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = json_with_token(
|
|
"PATCH",
|
|
&format!("/api/teams/{team_id}/agents/nonexistent/name"),
|
|
json!({ "name": "X" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// §3 Session Management (ES-*, SS-*)
|
|
// ===========================================================================
|
|
|
|
// ES-1: Ensure session
|
|
#[tokio::test]
|
|
async fn es1_ensure_session() {
|
|
let (mut app, services) = build_app_with_mock_agents().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = json_with_token(
|
|
"POST",
|
|
&format!("/api/teams/{team_id}/session"),
|
|
json!({}),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
// ES-2: Ensure session is idempotent
|
|
#[tokio::test]
|
|
async fn es2_ensure_session_idempotent() {
|
|
let (mut app, services) = build_app_with_mock_agents().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = json_with_token(
|
|
"POST",
|
|
&format!("/api/teams/{team_id}/session"),
|
|
json!({}),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
|
|
let req = json_with_token(
|
|
"POST",
|
|
&format!("/api/teams/{team_id}/session"),
|
|
json!({}),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
// ES-3: Ensure session for nonexistent team returns 404
|
|
#[tokio::test]
|
|
async fn es3_ensure_session_nonexistent() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let req = json_with_token("POST", "/api/teams/nonexistent/session", json!({}), &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
// SS-1: Stop session
|
|
#[tokio::test]
|
|
async fn ss1_stop_session() {
|
|
let (mut app, services) = build_app_with_mock_agents().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = json_with_token(
|
|
"POST",
|
|
&format!("/api/teams/{team_id}/session"),
|
|
json!({}),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
let req = delete_with_token(&format!("/api/teams/{team_id}/session"), &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
// SS-3: Stop session without active is noop
|
|
#[tokio::test]
|
|
async fn ss3_stop_session_noop() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = delete_with_token(&format!("/api/teams/{team_id}/session"), &token, &csrf);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// §4 Message sending (SM-*, SA-*)
|
|
// ===========================================================================
|
|
|
|
// SM-1: Send message with active session
|
|
#[tokio::test]
|
|
async fn sm1_send_message_with_session() {
|
|
let (mut app, services) = build_app_with_mock_agents().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
// Start session first
|
|
let req = json_with_token(
|
|
"POST",
|
|
&format!("/api/teams/{team_id}/session"),
|
|
json!({}),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
let req = json_with_token(
|
|
"POST",
|
|
&format!("/api/teams/{team_id}/messages"),
|
|
json!({ "content": "Hello team" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
// SM-4: Send message without session returns 404
|
|
#[tokio::test]
|
|
async fn sm4_send_message_no_session() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let req = json_with_token(
|
|
"POST",
|
|
"/api/teams/nonexistent/messages",
|
|
json!({ "content": "Hello" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
// SM-5: Missing content returns 400
|
|
#[tokio::test]
|
|
async fn sm5_send_message_missing_content() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
|
|
let req = json_with_token(
|
|
"POST",
|
|
&format!("/api/teams/{team_id}/messages"),
|
|
json!({}),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
|
}
|
|
|
|
// SA-1: Send message to specific agent
|
|
#[tokio::test]
|
|
async fn sa1_send_message_to_agent() {
|
|
let (mut app, services) = build_app_with_mock_agents().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
let slot_id = data["agents"][1]["slot_id"].as_str().unwrap();
|
|
|
|
// Start session first
|
|
let req = json_with_token(
|
|
"POST",
|
|
&format!("/api/teams/{team_id}/session"),
|
|
json!({}),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
let req = json_with_token(
|
|
"POST",
|
|
&format!("/api/teams/{team_id}/agents/{slot_id}/messages"),
|
|
json!({ "content": "Do this" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// §5 Full lifecycle
|
|
// ===========================================================================
|
|
|
|
// Full CRUD lifecycle
|
|
#[tokio::test]
|
|
async fn full_team_lifecycle() {
|
|
let (mut app, services) = build_app_with_mock_agents().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
// Create
|
|
let data = create_team(&mut app, &token, &csrf).await;
|
|
let team_id = data["id"].as_str().unwrap();
|
|
assert_eq!(data["agents"].as_array().unwrap().len(), 2);
|
|
|
|
// Add agent
|
|
let body = json!({ "name": "Helper", "role": "teammate", "backend": "acp", "model": "claude" });
|
|
let req = json_with_token("POST", &format!("/api/teams/{team_id}/agents"), body, &token, &csrf);
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
let added = body_json(resp).await;
|
|
let new_slot = added["data"]["slot_id"].as_str().unwrap().to_owned();
|
|
|
|
// Verify 3 agents
|
|
let req = get_with_token(&format!("/api/teams/{team_id}"), &token);
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"]["agents"].as_array().unwrap().len(), 3);
|
|
|
|
// Rename team
|
|
let req = json_with_token(
|
|
"PATCH",
|
|
&format!("/api/teams/{team_id}/name"),
|
|
json!({ "name": "Renamed" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
// Rename agent
|
|
let req = json_with_token(
|
|
"PATCH",
|
|
&format!("/api/teams/{team_id}/agents/{new_slot}/name"),
|
|
json!({ "name": "Senior Helper" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
// Ensure session
|
|
let req = json_with_token(
|
|
"POST",
|
|
&format!("/api/teams/{team_id}/session"),
|
|
json!({}),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
// Send message
|
|
let req = json_with_token(
|
|
"POST",
|
|
&format!("/api/teams/{team_id}/messages"),
|
|
json!({ "content": "Hello" }),
|
|
&token,
|
|
&csrf,
|
|
);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
// Stop session
|
|
let req = delete_with_token(&format!("/api/teams/{team_id}/session"), &token, &csrf);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
// Remove added agent
|
|
let req = delete_with_token(&format!("/api/teams/{team_id}/agents/{new_slot}"), &token, &csrf);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
// Verify 2 agents remain
|
|
let req = get_with_token(&format!("/api/teams/{team_id}"), &token);
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"]["agents"].as_array().unwrap().len(), 2);
|
|
assert_eq!(json["data"]["name"], "Renamed");
|
|
|
|
// Delete team
|
|
let req = delete_with_token(&format!("/api/teams/{team_id}"), &token, &csrf);
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
// Verify empty
|
|
let req = get_with_token("/api/teams", &token);
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
let json = body_json(resp).await;
|
|
assert!(json["data"].as_array().unwrap().is_empty());
|
|
}
|