//! P3 surface: skills library/install and the file-drive listings. use std::sync::Arc; use cm_api::AppState; use cm_auth::AuthService; use cm_domain::{FileDrive, FileNode, Role, User, UserId, Workspace, WorkspaceId}; use cm_llm::ScriptedProvider; use cm_runtime::{Runtime, RuntimeConfig}; use serde_json::{json, Value}; use uuid::Uuid; struct TestServer { base: String, client: reqwest::Client, } async fn serve(pool: sqlx::PgPool) -> TestServer { let runtime = Runtime::new( pool.clone(), Arc::new(ScriptedProvider::from_toml("").unwrap()), RuntimeConfig::basic("scripted", 1024), ); let app = cm_api::router(AppState::new(pool, runtime)); 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_and_login(pool: &sqlx::PgPool, server: &TestServer) -> (String, String) { let ws = Workspace { id: WorkspaceId::new(), name: "Acme".into(), plan: "team".into(), }; cm_db::repo::workspaces::insert(pool, &ws).await.unwrap(); let owner = User { id: UserId::new(), workspace_id: ws.id, email: format!("{}@acme.test", UserId::new()), role: Role::Owner, display_name: "Owner".into(), created_at: time::OffsetDateTime::UNIX_EPOCH, }; cm_db::repo::users::insert(pool, &owner).await.unwrap(); AuthService::new(pool.clone()) .set_password(owner.id, "pw") .await .unwrap(); let token = server .client .post(format!("{}/api/auth/login", server.base)) .json(&json!({"email": owner.email, "password": "pw"})) .send() .await .unwrap() .json::() .await .unwrap()["token"] .as_str() .unwrap() .to_owned(); let claw: Value = server .client .post(format!("{}/api/claws", server.base)) .bearer_auth(&token) .json(&json!({"name": "Scout", "job_title": "Analyst"})) .send() .await .unwrap() .json() .await .unwrap(); (token, claw["id"].as_str().unwrap().to_owned()) } #[tokio::test] async fn skill_library_install_and_uninstall_round_trip() { let pool = cm_testkit::test_pool().await; let server = serve(pool.clone()).await; let (token, claw_id) = seed_and_login(&pool, &server).await; let skill = cm_db::repo::skills::create( &pool, None, "Daily briefing", "Clawmates", "Summarize the day each morning.", "Each morning, compile...", ) .await .unwrap(); // Library lists the catalog skill. let library: Value = server .client .get(format!("{}/api/skills", server.base)) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(library[0]["title"], "Daily briefing"); assert_eq!(library[0]["installs"], 0); // Nothing installed yet. let installed: Value = server .client .get(format!("{}/api/skills?clawId={claw_id}", server.base)) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); assert!(installed.as_array().unwrap().is_empty()); // Install (twice — idempotent, single count). for _ in 0..2 { let res = server .client .post(format!("{}/api/skills/install", server.base)) .bearer_auth(&token) .json(&json!({"clawId": claw_id, "skillId": skill.id})) .send() .await .unwrap(); assert_eq!(res.status(), 204); } let installed: Value = server .client .get(format!("{}/api/skills?clawId={claw_id}", server.base)) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(installed.as_array().unwrap().len(), 1); let library: Value = server .client .get(format!("{}/api/skills", server.base)) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(library[0]["installs"], 1); // Uninstall. let res = server .client .post(format!("{}/api/skills/uninstall", server.base)) .bearer_auth(&token) .json(&json!({"clawId": claw_id, "skillId": skill.id})) .send() .await .unwrap(); assert_eq!(res.status(), 204); } #[tokio::test] async fn file_listings_are_drive_and_agent_scoped() { let pool = cm_testkit::test_pool().await; let server = serve(pool.clone()).await; let (token, claw_id) = seed_and_login(&pool, &server).await; let agent_uuid: Uuid = claw_id.parse().unwrap(); let workspace_id = cm_db::repo::agents::get(&pool, agent_uuid.into()) .await .unwrap() .workspace_id; for (drive, agent, path) in [ (FileDrive::Documents, Some(agent_uuid), "doc.md"), (FileDrive::Received, Some(agent_uuid), "incoming.csv"), (FileDrive::Shared, None, "team-handbook.md"), ] { cm_db::repo::files::upsert( &pool, &FileNode { id: Uuid::now_v7(), workspace_id, agent_id: agent.map(Into::into), drive, path: path.into(), size: 10, blob_ref: format!("k/{path}"), }, ) .await .unwrap(); } let documents: Value = server .client .get(format!( "{}/api/openclaw/files?clawId={claw_id}&drive=documents", server.base )) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(documents.as_array().unwrap().len(), 1); assert_eq!(documents[0]["path"], "doc.md"); let received: Value = server .client .get(format!( "{}/api/openclaw/files?clawId={claw_id}&drive=received", server.base )) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(received[0]["path"], "incoming.csv"); let shared: Value = server .client .get(format!( "{}/api/shared-drive/files?clawId={claw_id}", server.base )) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(shared[0]["path"], "team-handbook.md"); }