Update: 将子项目从 submodule 转为完整内容

- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
This commit is contained in:
freedak
2026-07-04 19:20:46 +08:00
parent 54d6465fa7
commit f7a720204a
3360 changed files with 802660 additions and 3 deletions
@@ -0,0 +1,842 @@
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());
}