Files
MyAiDesk/nomifun-tauri/crates/backend/nomifun-app/tests/common/mod.rs
T
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

271 lines
10 KiB
Rust

//! Shared test helpers for nomifun-app E2E tests.
#![allow(dead_code)]
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use http_body_util::BodyExt;
use tower::ServiceExt;
use wiremock::MockServer;
use nomifun_ai_agent::{AgentInstance, IAgentTask, IMockAgent, WorkerTaskManagerImpl};
use nomifun_app::{AppConfig, AppServices, build_module_states, create_router, create_router_with_states};
use nomifun_extension::{ExternalPathsManager, SkillPaths, SkillRouterState};
use nomifun_file::FileService;
use nomifun_system::VersionCheckService;
pub async fn build_app() -> (axum::Router, AppServices) {
let db = nomifun_db::init_database_memory().await.unwrap();
let services = AppServices::from_config(db, &AppConfig::default()).await.unwrap();
let router = create_router(&services).await;
(router, services)
}
/// Build an app whose skill router reads from the given temp directories.
///
/// Use for HTTP integration tests that need deterministic on-disk layouts
/// (E1 `/api/skills`, E2 `/api/skills/builtin-auto`, E3/E4 built-in reads,
/// E5 `/api/skills/info`). Returns the router, services, and the
/// `SkillPaths` so the test can seed fixtures at known locations.
#[allow(dead_code)]
pub async fn build_app_with_skill_paths(root: &std::path::Path) -> (axum::Router, AppServices, SkillPaths) {
let db = nomifun_db::init_database_memory().await.unwrap();
let services = AppServices::from_config(db, &AppConfig::default()).await.unwrap();
let (mut states, _) = build_module_states(&services).await;
let builtin_dir = root.join("builtin-skills");
let paths = SkillPaths {
data_dir: root.to_path_buf(),
user_skills_dir: root.join("skills"),
cron_skills_dir: root.join("cron").join("skills"),
builtin_skills_dir: builtin_dir.clone(),
builtin_rules_dir: root.join("builtin-rules"),
assistant_rules_dir: root.join("assistant-rules"),
assistant_skills_dir: root.join("assistant-skills"),
};
for dir in [
&paths.user_skills_dir,
&builtin_dir,
&paths.builtin_rules_dir,
&paths.assistant_rules_dir,
&paths.assistant_skills_dir,
] {
std::fs::create_dir_all(dir).unwrap();
}
let ext_paths_mgr = std::sync::Arc::new(ExternalPathsManager::with_file(root.join("paths.json")).await);
states.skill = SkillRouterState {
skill_paths: paths.clone(),
external_paths_manager: ext_paths_mgr,
assistant_dispatcher: states.skill.assistant_dispatcher.clone(),
skill_tag_repo: std::sync::Arc::new(nomifun_db::SqliteSkillTagRepository::new(
services.database.pool().clone(),
)),
builtin_skill_tags: std::sync::Arc::new(std::collections::HashMap::new()),
};
let router = create_router_with_states(&services, states);
(router, services, paths)
}
pub async fn build_app_with_noop_opener() -> (axum::Router, AppServices) {
let db = nomifun_db::init_database_memory().await.unwrap();
let services = AppServices::from_config(db, &AppConfig::default()).await.unwrap();
let (mut states, _) = build_module_states(&services).await;
states.shell.shell_service = std::sync::Arc::new(nomifun_shell::ShellService::new(std::sync::Arc::new(
nomifun_shell::NoopSystemOpener,
)));
let router = create_router_with_states(&services, states);
(router, services)
}
pub async fn build_app_with_file_roots(allowed_roots: Vec<std::path::PathBuf>) -> (axum::Router, AppServices) {
let db = nomifun_db::init_database_memory().await.unwrap();
let services = AppServices::from_config(db, &AppConfig::default()).await.unwrap();
let (mut states, _) = build_module_states(&services).await;
states.file.file_service = std::sync::Arc::new(FileService::new(services.event_bus.clone(), allowed_roots));
let router = create_router_with_states(&services, states);
(router, services)
}
pub async fn build_app_with_mock_version(
current_version: &str,
mock_server: &MockServer,
) -> (axum::Router, AppServices) {
let db = nomifun_db::init_database_memory().await.unwrap();
let services = AppServices::from_config(db, &AppConfig::default()).await.unwrap();
let (mut states, _) = build_module_states(&services).await;
states.system.version_check_service =
VersionCheckService::with_api_base(reqwest::Client::new(), current_version.to_owned(), mock_server.uri());
let router = create_router_with_states(&services, states);
(router, services)
}
/// Build app with a mock worker task manager that returns noop agents.
///
/// Use for tests that exercise session/warmup paths (team ensure_session,
/// send_message) where spawning a real CLI process is not feasible.
pub async fn build_app_with_mock_agents() -> (axum::Router, AppServices) {
let db = nomifun_db::init_database_memory().await.unwrap();
let factory: std::sync::Arc<
dyn Fn(
nomifun_ai_agent::types::BuildTaskOptions,
) -> futures_util::future::BoxFuture<'static, Result<AgentInstance, nomifun_common::AppError>>
+ Send
+ Sync,
> = std::sync::Arc::new(|opts| {
Box::pin(async move {
Ok(AgentInstance::Mock(std::sync::Arc::new(NoopMockAgent {
conversation_id: opts.conversation_id,
})))
})
});
let wtm: std::sync::Arc<dyn nomifun_ai_agent::IWorkerTaskManager> =
std::sync::Arc::new(WorkerTaskManagerImpl::new(factory));
let services = AppServices::from_config(db, &AppConfig::default())
.await
.unwrap()
.with_worker_task_manager(wtm);
let router = create_router(&services).await;
(router, services)
}
struct NoopMockAgent {
conversation_id: String,
}
#[async_trait::async_trait]
impl IAgentTask for NoopMockAgent {
fn agent_type(&self) -> nomifun_common::AgentType {
nomifun_common::AgentType::Acp
}
fn conversation_id(&self) -> &str {
&self.conversation_id
}
fn workspace(&self) -> &str {
"/tmp/test"
}
fn status(&self) -> Option<nomifun_common::ConversationStatus> {
None
}
fn last_activity_at(&self) -> nomifun_common::TimestampMs {
nomifun_common::now_ms()
}
fn subscribe(&self) -> tokio::sync::broadcast::Receiver<nomifun_ai_agent::AgentStreamEvent> {
let (tx, _) = tokio::sync::broadcast::channel(1);
tx.subscribe()
}
async fn send_message(
&self,
_data: nomifun_ai_agent::types::SendMessageData,
) -> Result<(), nomifun_ai_agent::AgentSendError> {
Ok(())
}
async fn cancel(&self) -> Result<(), nomifun_common::AppError> {
Ok(())
}
fn kill(&self, _reason: Option<nomifun_common::AgentKillReason>) -> Result<(), nomifun_common::AppError> {
Ok(())
}
}
#[async_trait::async_trait]
impl IMockAgent for NoopMockAgent {}
pub async fn body_json(resp: axum::response::Response) -> serde_json::Value {
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
serde_json::from_slice(&bytes).unwrap()
}
pub fn extract_csrf_token(resp: &axum::response::Response) -> Option<String> {
resp.headers()
.get_all(header::SET_COOKIE)
.iter()
.filter_map(|v| v.to_str().ok())
.find(|s| s.starts_with("nomifun-csrf-token="))
.map(|s| {
s.strip_prefix("nomifun-csrf-token=")
.unwrap()
.split(';')
.next()
.unwrap()
.to_owned()
})
}
pub fn get_request(uri: &str) -> Request<Body> {
Request::builder().method("GET").uri(uri).body(Body::empty()).unwrap()
}
pub fn get_with_token(uri: &str, token: &str) -> Request<Body> {
Request::builder()
.method("GET")
.uri(uri)
.header("authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap()
}
pub fn json_with_token(method_str: &str, uri: &str, body: serde_json::Value, token: &str, csrf: &str) -> Request<Body> {
Request::builder()
.method(method_str)
.uri(uri)
.header("content-type", "application/json")
.header("authorization", format!("Bearer {token}"))
.header("x-csrf-token", csrf)
.header("cookie", format!("nomifun-csrf-token={csrf}"))
.body(Body::from(serde_json::to_vec(&body).unwrap()))
.unwrap()
}
pub fn delete_with_token(uri: &str, token: &str, csrf: &str) -> Request<Body> {
Request::builder()
.method("DELETE")
.uri(uri)
.header("authorization", format!("Bearer {token}"))
.header("x-csrf-token", csrf)
.header("cookie", format!("nomifun-csrf-token={csrf}"))
.body(Body::empty())
.unwrap()
}
/// Set up a user and login, returning (session_token, csrf_token).
///
/// The seeded `system_default_user` row already uses `username = "admin"`; if
/// the test asks for that username, overwrite the seed row's empty credentials
/// in place instead of trying to INSERT a duplicate.
pub async fn setup_and_login(
app: &mut axum::Router,
services: &AppServices,
username: &str,
password: &str,
) -> (String, String) {
let hash = nomifun_auth::hash_password(password).unwrap();
if username == "admin" {
services
.user_repo
.set_system_user_credentials(username, &hash)
.await
.unwrap();
} else {
services.user_repo.create_user(username, &hash).await.unwrap();
}
let resp = app.clone().oneshot(get_request("/api/auth/status")).await.unwrap();
let csrf = extract_csrf_token(&resp).expect("CSRF cookie should be set");
let body = format!(r#"{{"username":"{username}","password":"{password}"}}"#);
let req = Request::builder()
.method("POST")
.uri("/login")
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK, "login should succeed");
let json = body_json(resp).await;
let token = json["token"].as_str().unwrap().to_owned();
(token, csrf)
}