use cm_api::AppState; use cm_auth::AuthService; use cm_domain::{Role, User, UserId, Workspace, WorkspaceId}; use serde_json::{json, Value}; struct TestServer { base: String, client: reqwest::Client, } fn test_runtime(pool: sqlx::PgPool) -> cm_runtime::Runtime { cm_runtime::Runtime::new( pool, std::sync::Arc::new(cm_llm::ScriptedProvider::from_toml("").unwrap()), cm_runtime::RuntimeConfig::basic("scripted", 1024), ) } async fn serve(pool: sqlx::PgPool) -> TestServer { let app = cm_api::router(AppState::new(pool.clone(), test_runtime(pool.clone()))); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); TestServer { base: format!("http://{addr}"), client: reqwest::Client::new(), } } async fn seed_user(pool: &sqlx::PgPool, ws: &Workspace, email: &str, role: Role) -> User { let user = User { id: UserId::new(), workspace_id: ws.id, email: email.into(), role, display_name: email.split('@').next().unwrap().into(), created_at: time::OffsetDateTime::UNIX_EPOCH, }; cm_db::repo::users::insert(pool, &user).await.unwrap(); AuthService::new(pool.clone()) .set_password(user.id, "pw") .await .unwrap(); user } async fn seed_workspace(pool: &sqlx::PgPool) -> Workspace { let ws = Workspace { id: WorkspaceId::new(), name: "Acme".into(), plan: "team".into(), }; cm_db::repo::workspaces::insert(pool, &ws).await.unwrap(); ws } async fn login(server: &TestServer, email: &str) -> String { let res = server .client .post(format!("{}/api/auth/login", server.base)) .json(&json!({"email": email, "password": "pw"})) .send() .await .unwrap(); assert_eq!(res.status(), 200); res.json::().await.unwrap()["token"] .as_str() .unwrap() .to_owned() } async fn create_claw(server: &TestServer, token: &str, name: &str) -> Value { let res = server .client .post(format!("{}/api/claws", server.base)) .bearer_auth(token) .json(&json!({"name": name, "job_title": "Research Analyst"})) .send() .await .unwrap(); assert_eq!(res.status(), 201); res.json().await.unwrap() } #[tokio::test] async fn create_returns_live_agent_with_default_policy_and_audit() { let pool = cm_testkit::test_pool().await; let ws = seed_workspace(&pool).await; let owner = seed_user(&pool, &ws, "owner@acme.test", Role::Owner).await; let server = serve(pool.clone()).await; let token = login(&server, "owner@acme.test").await; let claw = create_claw(&server, &token, "Scout").await; assert_eq!(claw["name"], "Scout"); assert_eq!(claw["status"], "online"); assert_eq!(claw["managed_by"], owner.id.to_string()); let settings: Value = server .client .get(format!( "{}/api/claws/settings/full?clawId={}", server.base, claw["id"].as_str().unwrap() )) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(settings["agent"]["name"], "Scout"); assert_eq!(settings["access_policy"]["humans"]["mode"], "entire_team"); assert_eq!(settings["access_policy"]["agents"]["mode"], "any"); let audited = sqlx::query_scalar::<_, i64>( "SELECT count(*) FROM audit_log WHERE event_type = 'agent.created'", ) .fetch_one(&pool) .await .unwrap(); assert_eq!(audited, 1); } #[tokio::test] async fn patch_updates_profile_and_system_prompt() { let pool = cm_testkit::test_pool().await; let ws = seed_workspace(&pool).await; seed_user(&pool, &ws, "owner@acme.test", Role::Owner).await; let server = serve(pool).await; let token = login(&server, "owner@acme.test").await; let claw = create_claw(&server, &token, "Scout").await; let id = claw["id"].as_str().unwrap(); let res = server .client .patch(format!("{}/api/claws/{id}", server.base)) .bearer_auth(&token) .json(&json!({ "job_title": "Senior Analyst", "system_prompt": "Think in bullet points." })) .send() .await .unwrap(); assert_eq!(res.status(), 200); let updated: Value = res.json().await.unwrap(); assert_eq!(updated["job_title"], "Senior Analyst"); assert_eq!(updated["system_prompt"], "Think in bullet points."); // Untouched fields persist. assert_eq!(updated["name"], "Scout"); } #[tokio::test] async fn delete_requires_owner_or_manager() { let pool = cm_testkit::test_pool().await; let ws = seed_workspace(&pool).await; seed_user(&pool, &ws, "owner@acme.test", Role::Owner).await; seed_user(&pool, &ws, "member@acme.test", Role::Member).await; let server = serve(pool).await; let owner_token = login(&server, "owner@acme.test").await; let member_token = login(&server, "member@acme.test").await; let claw = create_claw(&server, &owner_token, "Scout").await; let id = claw["id"].as_str().unwrap(); // A plain member who doesn't manage the claw cannot delete it. let forbidden = server .client .delete(format!("{}/api/claws/{id}", server.base)) .bearer_auth(&member_token) .send() .await .unwrap(); assert_eq!(forbidden.status(), 403); let deleted = server .client .delete(format!("{}/api/claws/{id}", server.base)) .bearer_auth(&owner_token) .send() .await .unwrap(); assert_eq!(deleted.status(), 204); // Gone from the roster. let claws: Value = server .client .get(format!("{}/api/team/claws", server.base)) .bearer_auth(&owner_token) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(claws.as_array().unwrap().len(), 0); } #[tokio::test] async fn members_can_create_and_manage_their_own_claws() { let pool = cm_testkit::test_pool().await; let ws = seed_workspace(&pool).await; seed_user(&pool, &ws, "member@acme.test", Role::Member).await; let server = serve(pool).await; let token = login(&server, "member@acme.test").await; // Builders (plain members) can create claws (ยง1 personas)... let claw = create_claw(&server, &token, "Drafter").await; let id = claw["id"].as_str().unwrap(); // ...and delete the ones they manage. let deleted = server .client .delete(format!("{}/api/claws/{id}", server.base)) .bearer_auth(&token) .send() .await .unwrap(); assert_eq!(deleted.status(), 204); } #[tokio::test] async fn cross_workspace_access_is_not_found() { let pool = cm_testkit::test_pool().await; let ws_a = seed_workspace(&pool).await; let ws_b = seed_workspace(&pool).await; seed_user(&pool, &ws_a, "a@acme.test", Role::Owner).await; seed_user(&pool, &ws_b, "b@other.test", Role::Owner).await; let server = serve(pool).await; let token_a = login(&server, "a@acme.test").await; let token_b = login(&server, "b@other.test").await; let claw = create_claw(&server, &token_a, "Scout").await; let id = claw["id"].as_str().unwrap(); // Tenant isolation: the other workspace can't even see it exists. let other = server .client .get(format!( "{}/api/claws/settings/full?clawId={id}", server.base )) .bearer_auth(&token_b) .send() .await .unwrap(); assert_eq!(other.status(), 404); }