diff --git a/server/src/chat/transport/matrix/new_project.rs b/server/src/chat/transport/matrix/new_project.rs index 0ac1810c..cb711472 100644 --- a/server/src/chat/transport/matrix/new_project.rs +++ b/server/src/chat/transport/matrix/new_project.rs @@ -347,6 +347,11 @@ async fn verify_push_credentials(git_url: &str, git_token: Option<&str>) -> Resu /// Calls `ssh-keygen -t ed25519 -N "" -f ` with no passphrase. /// Returns the public key string (trimmed) on success. async fn generate_ssh_keypair(key_path: &std::path::Path) -> Result { + if let Some(parent) = key_path.parent() { + tokio::fs::create_dir_all(parent) + .await + .map_err(|e| format!("Cannot create key directory {}: {e}", parent.display()))?; + } let out = tokio::process::Command::new("ssh-keygen") .args(["-t", "ed25519", "-N", ""]) .arg("-f") @@ -1185,6 +1190,35 @@ mod tests { assert_eq!(pub_contents.trim(), pubkey); } + #[tokio::test] + async fn generate_ssh_keypair_creates_missing_parent_dirs() { + // Skip if ssh-keygen is not available in this environment. + if tokio::process::Command::new("ssh-keygen") + .arg("-V") + .output() + .await + .is_err() + { + return; + } + + let base = tempfile::tempdir().unwrap(); + // Use a nested path whose intermediate directories do not yet exist. + let key_path = base.path().join("a").join("b").join("c").join("id_ed25519"); + assert!( + !key_path.parent().unwrap().exists(), + "precondition: parent should not exist" + ); + + let pubkey = generate_ssh_keypair(&key_path).await.unwrap(); + + assert!(key_path.exists(), "private key file not created"); + assert!( + pubkey.starts_with("ssh-ed25519"), + "public key should start with ssh-ed25519, got: {pubkey}" + ); + } + #[test] fn find_free_port_returns_bindable_port() { let port = find_free_port(2200).expect("expected Some(port) in range 2200..2300");