mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
refactor(async): lint std::process::Command across all targets
Move async subprocess paths to Tokio or spawn_blocking, document the intentional synchronous std::process::Command callsites, and make CI run Clippy with --all-targets so the guardrail applies to test code too.
This commit is contained in:
parent
708c37aed1
commit
4d925d5d5d
45 changed files with 406 additions and 200 deletions
2
.github/workflows/rust.yml
vendored
2
.github/workflows/rust.yml
vendored
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- run: cargo clippy --workspace -- -D warnings
|
||||
- run: cargo clippy --workspace --all-targets -- -D warnings
|
||||
|
||||
test:
|
||||
name: Test (Linux)
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ disallowed-methods = [
|
|||
{ path = "std::thread::sleep", reason = "Prefer tokio::time::sleep on Tokio paths; document intentional blocking sleeps with #[expect(clippy::disallowed_methods, reason = \"...\")]", replacement = "tokio::time::sleep" },
|
||||
{ path = "std::thread::spawn", reason = "Prefer Tokio task APIs on async paths; document intentional dedicated OS threads with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
|
||||
{ path = "std::thread::Builder::spawn", reason = "Prefer Tokio task APIs on async paths; document intentional dedicated OS threads with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
|
||||
{ path = "std::process::Command::new", reason = "Prefer tokio::process::Command on Tokio paths; document intentional synchronous subprocesses with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -179,6 +179,11 @@ impl MetadataStore {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "These unit tests use the real git CLI to validate metadata branch behavior."
|
||||
)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "Build scripts run outside Tokio and need a synchronous git probe for the embedded build SHA."
|
||||
)]
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=../../../.git/HEAD");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
|
|
@ -17,6 +16,7 @@ use fabro_util::terminal::Styles;
|
|||
use fabro_util::version::FABRO_VERSION;
|
||||
use regex::Regex;
|
||||
use semver::Version;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
|
||||
use crate::args::{DoctorArgs, GlobalArgs};
|
||||
use crate::command_context::CommandContext;
|
||||
|
|
@ -68,28 +68,29 @@ fn parse_version(re: &Regex, output: &str) -> Option<Version> {
|
|||
))
|
||||
}
|
||||
|
||||
pub(crate) fn probe_system_deps() -> Vec<ProbeOutcome> {
|
||||
DEP_SPECS
|
||||
.iter()
|
||||
.map(|spec| {
|
||||
let result = Command::new(spec.command[0])
|
||||
.args(&spec.command[1..])
|
||||
.output()
|
||||
.ok();
|
||||
pub(crate) async fn probe_system_deps() -> Vec<ProbeOutcome> {
|
||||
let mut outcomes = Vec::with_capacity(DEP_SPECS.len());
|
||||
for spec in DEP_SPECS {
|
||||
let result = TokioCommand::new(spec.command[0])
|
||||
.args(&spec.command[1..])
|
||||
.output()
|
||||
.await
|
||||
.ok();
|
||||
|
||||
match result {
|
||||
None => ProbeOutcome::NotFound,
|
||||
Some(output) if !output.status.success() => ProbeOutcome::Failed,
|
||||
Some(output) => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let version = parse_version(spec.pattern, &stdout)
|
||||
.or_else(|| parse_version(spec.pattern, &stderr));
|
||||
ProbeOutcome::Ok { version }
|
||||
}
|
||||
let outcome = match result {
|
||||
None => ProbeOutcome::NotFound,
|
||||
Some(output) if !output.status.success() => ProbeOutcome::Failed,
|
||||
Some(output) => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let version = parse_version(spec.pattern, &stdout)
|
||||
.or_else(|| parse_version(spec.pattern, &stderr));
|
||||
ProbeOutcome::Ok { version }
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
outcomes.push(outcome);
|
||||
}
|
||||
outcomes
|
||||
}
|
||||
|
||||
fn dep_issue(name: &str, issue: &str, required: bool) -> (CheckStatus, String) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use std::io::Write as _;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::process::Stdio;
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use axum::extract::Query;
|
||||
|
|
@ -20,7 +19,9 @@ use fabro_server::secret_store::SecretStore;
|
|||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use rand::Rng;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
|
|
@ -38,10 +39,11 @@ use crate::{server_client, user_config};
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Run an openssl subcommand and return stdout on success.
|
||||
fn run_openssl(args: &[&str], description: &str) -> Result<Vec<u8>> {
|
||||
let output = Command::new("openssl")
|
||||
async fn run_openssl(args: &[&str], description: &str) -> Result<Vec<u8>> {
|
||||
let output = TokioCommand::new("openssl")
|
||||
.args(args)
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("failed to run openssl for: {description}"))?;
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
|
|
@ -53,22 +55,30 @@ fn run_openssl(args: &[&str], description: &str) -> Result<Vec<u8>> {
|
|||
}
|
||||
|
||||
/// Run an openssl subcommand that reads key material from stdin.
|
||||
fn run_openssl_with_stdin(args: &[&str], stdin_data: &[u8], description: &str) -> Result<Vec<u8>> {
|
||||
let mut child = Command::new("openssl")
|
||||
async fn run_openssl_with_stdin(
|
||||
args: &[&str],
|
||||
stdin_data: &[u8],
|
||||
description: &str,
|
||||
) -> Result<Vec<u8>> {
|
||||
let mut child = TokioCommand::new("openssl")
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to spawn openssl for: {description}"))?;
|
||||
child
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.context("openssl process missing stdin")?
|
||||
.context("openssl process missing stdin")?;
|
||||
stdin
|
||||
.write_all(stdin_data)
|
||||
.await
|
||||
.with_context(|| format!("failed to write to openssl stdin for: {description}"))?;
|
||||
drop(stdin);
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.await
|
||||
.with_context(|| format!("failed to read openssl output for: {description}"))?;
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
|
|
@ -93,10 +103,11 @@ fn generate_session_secret() -> String {
|
|||
// JWT keypair generation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn generate_jwt_keypair() -> Result<(String, String)> {
|
||||
let private_pem = run_openssl(&["genpkey", "-algorithm", "Ed25519"], "generate keypair")?;
|
||||
async fn generate_jwt_keypair() -> Result<(String, String)> {
|
||||
let private_pem =
|
||||
run_openssl(&["genpkey", "-algorithm", "Ed25519"], "generate keypair").await?;
|
||||
let public_pem =
|
||||
run_openssl_with_stdin(&["pkey", "-pubout"], &private_pem, "extract public key")?;
|
||||
run_openssl_with_stdin(&["pkey", "-pubout"], &private_pem, "extract public key").await?;
|
||||
|
||||
let private_str = String::from_utf8(private_pem).context("private key is not valid UTF-8")?;
|
||||
let public_str = String::from_utf8(public_pem).context("public key is not valid UTF-8")?;
|
||||
|
|
@ -107,11 +118,11 @@ fn generate_jwt_keypair() -> Result<(String, String)> {
|
|||
// mTLS certificate generation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn generate_mtls_certs(dir: &Path) -> Result<()> {
|
||||
async fn generate_mtls_certs(dir: &Path) -> Result<()> {
|
||||
std::fs::create_dir_all(dir).context("failed to create certs directory")?;
|
||||
|
||||
// 1. CA key + self-signed cert
|
||||
let ca_key = run_openssl(&["genpkey", "-algorithm", "Ed25519"], "generate CA key")?;
|
||||
let ca_key = run_openssl(&["genpkey", "-algorithm", "Ed25519"], "generate CA key").await?;
|
||||
let ca_key_path = dir.join("ca.key");
|
||||
std::fs::write(&ca_key_path, &ca_key)?;
|
||||
|
||||
|
|
@ -130,12 +141,14 @@ fn generate_mtls_certs(dir: &Path) -> Result<()> {
|
|||
"/CN=Fabro CA",
|
||||
],
|
||||
"generate CA cert",
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
let ca_cert_path = dir.join("ca.crt");
|
||||
std::fs::write(&ca_cert_path, &ca_cert)?;
|
||||
|
||||
// 2. Server key + CSR signed by CA
|
||||
let server_key = run_openssl(&["genpkey", "-algorithm", "Ed25519"], "generate server key")?;
|
||||
let server_key =
|
||||
run_openssl(&["genpkey", "-algorithm", "Ed25519"], "generate server key").await?;
|
||||
let server_key_path = dir.join("server.key");
|
||||
std::fs::write(&server_key_path, &server_key)?;
|
||||
|
||||
|
|
@ -151,7 +164,8 @@ fn generate_mtls_certs(dir: &Path) -> Result<()> {
|
|||
"/CN=localhost",
|
||||
],
|
||||
"generate server CSR",
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
let csr_path = dir.join("server.csr");
|
||||
std::fs::write(&csr_path, &csr)?;
|
||||
|
|
@ -175,7 +189,8 @@ fn generate_mtls_certs(dir: &Path) -> Result<()> {
|
|||
"3650",
|
||||
],
|
||||
"sign server cert",
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
std::fs::write(dir.join("server.crt"), &server_cert)?;
|
||||
|
||||
// Clean up temporary files
|
||||
|
|
@ -316,12 +331,13 @@ fn format_config_toml(username: &str) -> String {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Check if a binary exists on PATH using the doctor.rs pattern.
|
||||
fn detect_binary_on_path(binary: &str) -> bool {
|
||||
Command::new(binary)
|
||||
async fn detect_binary_on_path(binary: &str) -> bool {
|
||||
TokioCommand::new(binary)
|
||||
.arg("--version")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.await
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
|
@ -756,7 +772,7 @@ pub(crate) async fn run_install(
|
|||
" {}",
|
||||
s.dim.apply_to("[Pre-flight] System dependency checks")
|
||||
);
|
||||
let dep_outcomes = doctor::probe_system_deps();
|
||||
let dep_outcomes = doctor::probe_system_deps().await;
|
||||
let dep_check = doctor::check_system_deps(doctor::DEP_SPECS, &dep_outcomes);
|
||||
|
||||
if dep_check.status == doctor::CheckStatus::Error {
|
||||
|
|
@ -777,9 +793,10 @@ pub(crate) async fn run_install(
|
|||
.await??;
|
||||
|
||||
if install {
|
||||
let status = Command::new("brew")
|
||||
let status = TokioCommand::new("brew")
|
||||
.args(["install", "graphviz"])
|
||||
.status()
|
||||
.await
|
||||
.context("failed to run brew install graphviz")?;
|
||||
if !status.success() {
|
||||
fabro_util::printerr!(printer, " Warning: brew install graphviz failed");
|
||||
|
|
@ -802,7 +819,7 @@ pub(crate) async fn run_install(
|
|||
let mut secret_pairs: Vec<(String, String)> = Vec::new();
|
||||
let mut configured_providers: Vec<Provider> = Vec::new();
|
||||
|
||||
let codex_detected = detect_binary_on_path("codex");
|
||||
let codex_detected = detect_binary_on_path("codex").await;
|
||||
let mut openai_via_oauth = false;
|
||||
|
||||
if codex_detected {
|
||||
|
|
@ -897,7 +914,7 @@ pub(crate) async fn run_install(
|
|||
|
||||
match strategy {
|
||||
0 => {
|
||||
let token = fabro_github::gh_auth_token().map_err(|err| {
|
||||
let token = fabro_github::gh_auth_token().await.map_err(|err| {
|
||||
anyhow!("{err}. Run `gh auth login` and rerun `fabro install`.")
|
||||
})?;
|
||||
let user_toml_path = fabro_dir.join(SETTINGS_CONFIG_FILENAME);
|
||||
|
|
@ -1007,7 +1024,7 @@ pub(crate) async fn run_install(
|
|||
s.green.apply_to("✔")
|
||||
);
|
||||
|
||||
let (jwt_private_pem, jwt_public_pem) = generate_jwt_keypair()?;
|
||||
let (jwt_private_pem, jwt_public_pem) = generate_jwt_keypair().await?;
|
||||
fabro_util::printerr!(
|
||||
printer,
|
||||
" {} Ed25519 JWT keypair generated",
|
||||
|
|
@ -1015,7 +1032,7 @@ pub(crate) async fn run_install(
|
|||
);
|
||||
|
||||
let certs_dir = fabro_dir.join("certs");
|
||||
generate_mtls_certs(&certs_dir)?;
|
||||
generate_mtls_certs(&certs_dir).await?;
|
||||
fabro_util::printerr!(
|
||||
printer,
|
||||
" {} mTLS CA + server certificates generated",
|
||||
|
|
@ -1105,14 +1122,14 @@ mod tests {
|
|||
|
||||
// -- Binary detection --
|
||||
|
||||
#[test]
|
||||
fn detect_binary_finds_existing_command() {
|
||||
assert!(detect_binary_on_path("git"));
|
||||
#[tokio::test]
|
||||
async fn detect_binary_finds_existing_command() {
|
||||
assert!(detect_binary_on_path("git").await);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_binary_returns_false_for_nonexistent() {
|
||||
assert!(!detect_binary_on_path("arc_nonexistent_xyz"));
|
||||
#[tokio::test]
|
||||
async fn detect_binary_returns_false_for_nonexistent() {
|
||||
assert!(!detect_binary_on_path("arc_nonexistent_xyz").await);
|
||||
}
|
||||
|
||||
// -- Session secret --
|
||||
|
|
@ -1137,37 +1154,37 @@ mod tests {
|
|||
|
||||
// -- JWT keypair --
|
||||
|
||||
#[test]
|
||||
fn jwt_keypair_private_pem_header() {
|
||||
let (private, _) = generate_jwt_keypair().unwrap();
|
||||
#[tokio::test]
|
||||
async fn jwt_keypair_private_pem_header() {
|
||||
let (private, _) = generate_jwt_keypair().await.unwrap();
|
||||
assert!(
|
||||
private.starts_with("-----BEGIN PRIVATE KEY-----"),
|
||||
"private PEM: {private}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jwt_keypair_public_pem_header() {
|
||||
let (_, public) = generate_jwt_keypair().unwrap();
|
||||
#[tokio::test]
|
||||
async fn jwt_keypair_public_pem_header() {
|
||||
let (_, public) = generate_jwt_keypair().await.unwrap();
|
||||
assert!(
|
||||
public.starts_with("-----BEGIN PUBLIC KEY-----"),
|
||||
"public PEM: {public}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jwt_keypair_public_parses() {
|
||||
let (_, public) = generate_jwt_keypair().unwrap();
|
||||
#[tokio::test]
|
||||
async fn jwt_keypair_public_parses() {
|
||||
let (_, public) = generate_jwt_keypair().await.unwrap();
|
||||
jsonwebtoken::DecodingKey::from_ed_pem(public.as_bytes()).expect("public key should parse");
|
||||
}
|
||||
|
||||
// -- mTLS cert generation --
|
||||
|
||||
#[test]
|
||||
fn mtls_certs_creates_files() {
|
||||
#[tokio::test]
|
||||
async fn mtls_certs_creates_files() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let certs_dir = dir.path().join("certs");
|
||||
generate_mtls_certs(&certs_dir).unwrap();
|
||||
generate_mtls_certs(&certs_dir).await.unwrap();
|
||||
|
||||
assert!(certs_dir.join("ca.key").exists());
|
||||
assert!(certs_dir.join("ca.crt").exists());
|
||||
|
|
@ -1175,11 +1192,11 @@ mod tests {
|
|||
assert!(certs_dir.join("server.crt").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mtls_ca_cert_is_pem() {
|
||||
#[tokio::test]
|
||||
async fn mtls_ca_cert_is_pem() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let certs_dir = dir.path().join("certs");
|
||||
generate_mtls_certs(&certs_dir).unwrap();
|
||||
generate_mtls_certs(&certs_dir).await.unwrap();
|
||||
|
||||
let ca_crt = std::fs::read_to_string(certs_dir.join("ca.crt")).unwrap();
|
||||
assert!(
|
||||
|
|
@ -1188,11 +1205,11 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mtls_server_cert_is_pem() {
|
||||
#[tokio::test]
|
||||
async fn mtls_server_cert_is_pem() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let certs_dir = dir.path().join("certs");
|
||||
generate_mtls_certs(&certs_dir).unwrap();
|
||||
generate_mtls_certs(&certs_dir).await.unwrap();
|
||||
|
||||
let server_crt = std::fs::read_to_string(certs_dir.join("server.crt")).unwrap();
|
||||
assert!(
|
||||
|
|
@ -1201,11 +1218,11 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mtls_certs_parse_via_rustls() {
|
||||
#[tokio::test]
|
||||
async fn mtls_certs_parse_via_rustls() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let certs_dir = dir.path().join("certs");
|
||||
generate_mtls_certs(&certs_dir).unwrap();
|
||||
generate_mtls_certs(&certs_dir).await.unwrap();
|
||||
|
||||
let ca_pem = std::fs::read(certs_dir.join("ca.crt")).unwrap();
|
||||
let mut reader = std::io::Cursor::new(&ca_pem);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pub(super) async fn close_command(
|
|||
) -> Result<()> {
|
||||
let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?;
|
||||
|
||||
let creds = super::load_github_credentials_required(printer)?;
|
||||
let creds = super::load_github_credentials_required(printer).await?;
|
||||
|
||||
fabro_github::close_pull_request(
|
||||
&creds,
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ pub(super) async fn create_command(
|
|||
let (owner, repo) = fabro_github::parse_github_owner_repo(&https_url)
|
||||
.map_err(|err| anyhow::anyhow!("{err}"))?;
|
||||
|
||||
let creds = super::load_github_credentials_required(printer)?;
|
||||
let creds = super::load_github_credentials_required(printer).await?;
|
||||
|
||||
let branch_found = fabro_github::branch_exists(
|
||||
&creds,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ pub(super) async fn list_command(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let creds = super::load_github_credentials_required(printer)?;
|
||||
let creds = super::load_github_credentials_required(printer).await?;
|
||||
|
||||
let futures: Vec<_> = entries
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pub(super) async fn merge_command(
|
|||
) -> Result<()> {
|
||||
let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?;
|
||||
|
||||
let creds = super::load_github_credentials_required(printer)?;
|
||||
let creds = super::load_github_credentials_required(printer).await?;
|
||||
|
||||
fabro_github::merge_pull_request(
|
||||
&creds,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ pub(crate) async fn dispatch(
|
|||
}
|
||||
}
|
||||
|
||||
fn load_github_credentials_required(printer: Printer) -> Result<GitHubCredentials> {
|
||||
async fn load_github_credentials_required(printer: Printer) -> Result<GitHubCredentials> {
|
||||
let ctx = CommandContext::base(printer)?;
|
||||
let server_settings =
|
||||
fabro_config::resolve_server_from_file(ctx.machine_settings()).map_err(|errors| {
|
||||
|
|
@ -54,6 +54,7 @@ fn load_github_credentials_required(printer: Printer) -> Result<GitHubCredential
|
|||
.map(InterpString::as_source)
|
||||
.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| anyhow!(GITHUB_CREDENTIALS_REQUIRED))?;
|
||||
creds.context(GITHUB_CREDENTIALS_REQUIRED)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pub(super) async fn view_command(
|
|||
) -> Result<()> {
|
||||
let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?;
|
||||
|
||||
let creds = super::load_github_credentials_required(printer)?;
|
||||
let creds = super::load_github_credentials_required(printer).await?;
|
||||
|
||||
let detail = fabro_github::get_pull_request(
|
||||
&creds,
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@ use std::path::PathBuf;
|
|||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_util::printer::Printer;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
use crate::args::{GlobalArgs, RepoInitArgs, ServerTargetArgs};
|
||||
use crate::command_context::CommandContext;
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This is a shared synchronous git helper used by repo deinit; async callers should use spawn_blocking."
|
||||
)]
|
||||
pub(super) fn git_repo_root() -> Result<PathBuf> {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["rev-parse", "--show-toplevel"])
|
||||
|
|
@ -27,7 +32,9 @@ pub(crate) async fn run_init(
|
|||
globals: &GlobalArgs,
|
||||
printer: Printer,
|
||||
) -> Result<Vec<String>> {
|
||||
let repo_root = git_repo_root()?;
|
||||
let repo_root = spawn_blocking(git_repo_root)
|
||||
.await
|
||||
.context("git repo root task panicked")??;
|
||||
let mut created = Vec::new();
|
||||
|
||||
let fabro_dir = repo_root.join(".fabro");
|
||||
|
|
@ -144,9 +151,10 @@ draft = true
|
|||
|
||||
async fn check_github_app_installation(target: &ServerTargetArgs, printer: Printer) {
|
||||
// Get the git remote origin URL
|
||||
let output = match std::process::Command::new("git")
|
||||
let output = match TokioCommand::new("git")
|
||||
.args(["remote", "get-url", "origin"])
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
Ok(o) if o.status.success() => o,
|
||||
_ => {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,11 @@ pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs, printer: Printe
|
|||
}
|
||||
|
||||
if args.open && !globals.json {
|
||||
std::process::Command::new("open")
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "Preview URL opening is a fire-and-forget OS integration, not a Tokio-managed child process."
|
||||
)]
|
||||
let _browser = std::process::Command::new("open")
|
||||
.arg(&response.url)
|
||||
.spawn()
|
||||
.context("Failed to open browser")?;
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ pub(crate) async fn execute(
|
|||
spawn_worker_control_stream(Arc::clone(&interviewer), Arc::clone(&cancel_token))?;
|
||||
let run_control = RunControlState::new();
|
||||
install_signal_handlers(Arc::clone(&run_control), Arc::clone(&cancel_token))?;
|
||||
let github_app = maybe_build_github_credentials(&run_record.settings)?;
|
||||
let github_app = maybe_build_github_credentials(&run_record.settings).await?;
|
||||
let services = StartServices {
|
||||
run_id,
|
||||
cancel_token: Some(Arc::clone(&cancel_token)),
|
||||
|
|
@ -470,7 +470,7 @@ fn update_worker_title_from_event(event: &RunEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
fn maybe_build_github_credentials(
|
||||
async fn maybe_build_github_credentials(
|
||||
settings: &SettingsLayer,
|
||||
) -> Result<Option<fabro_github::GitHubCredentials>> {
|
||||
let resolved_run = fabro_config::resolve_run_from_file(settings).ok();
|
||||
|
|
@ -493,11 +493,12 @@ fn maybe_build_github_credentials(
|
|||
.map(InterpString::as_source);
|
||||
|
||||
if required_github_credentials {
|
||||
return build_github_credentials(strategy, app_id.as_deref());
|
||||
return build_github_credentials(strategy, app_id.as_deref()).await;
|
||||
}
|
||||
|
||||
if pull_request_enabled {
|
||||
return Ok(build_github_credentials(strategy, app_id.as_deref())
|
||||
.await
|
||||
.ok()
|
||||
.flatten());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ fn format_output(ssh_command: &str) -> String {
|
|||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This path replaces the current process via CommandExt::exec; Tokio child APIs are not a substitute."
|
||||
)]
|
||||
fn exec_ssh(ssh_cmd: &str) -> Result<()> {
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@ pub(crate) fn active_server_record(storage_dir: &Path) -> Option<ServerRecord> {
|
|||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous process identity probe is shared by async server start and sync server status flows."
|
||||
)]
|
||||
fn server_process_matches(record: &ServerRecord) -> bool {
|
||||
let output = match std::process::Command::new("ps")
|
||||
.args(["-ww", "-o", "command=", "-p", &record.pid.to_string()])
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use fabro_server::serve;
|
|||
use fabro_server::serve::{DEFAULT_TCP_PORT, ServeArgs};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
use tokio::time;
|
||||
|
||||
use super::record;
|
||||
|
|
@ -205,7 +206,7 @@ async fn execute_daemon(
|
|||
let stdout_log = log_file.try_clone()?;
|
||||
let exe = std::env::current_exe()?;
|
||||
|
||||
let mut cmd = std::process::Command::new(&exe);
|
||||
let mut cmd = TokioCommand::new(&exe);
|
||||
cmd.args(["server", "__serve"])
|
||||
.arg("--record-path")
|
||||
.arg(&record_path)
|
||||
|
|
@ -248,7 +249,7 @@ async fn execute_daemon(
|
|||
.stdin(std::process::Stdio::null());
|
||||
|
||||
#[cfg(unix)]
|
||||
fabro_proc::pre_exec_setsid(&mut cmd);
|
||||
fabro_proc::pre_exec_setsid(cmd.as_std_mut());
|
||||
|
||||
let mut child = cmd.spawn()?;
|
||||
|
||||
|
|
@ -269,11 +270,12 @@ async fn execute_daemon(
|
|||
if let Some(record) = record::read_server_record(&record_path) {
|
||||
if try_connect(&record.bind) {
|
||||
if announce {
|
||||
let pid = child.id().unwrap_or_default();
|
||||
maybe_warn_host_port_fallback(bind, &record.bind, printer);
|
||||
fabro_util::printerr!(
|
||||
printer,
|
||||
"Server started (pid {}) on {}",
|
||||
child.id(),
|
||||
pid,
|
||||
record.bind
|
||||
);
|
||||
}
|
||||
|
|
@ -295,8 +297,8 @@ async fn execute_daemon(
|
|||
}
|
||||
|
||||
record::remove_server_record(&record_path);
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
let _ = child.kill().await;
|
||||
let _ = child.wait().await;
|
||||
let tail = read_log_tail(&log_path, 20);
|
||||
if !tail.is_empty() {
|
||||
fabro_util::printerr!(printer, "{tail}");
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ pub(crate) async fn run_upgrade(
|
|||
debug!("SHA256 checksum verified");
|
||||
|
||||
// Extract tarball
|
||||
let status = std::process::Command::new("tar")
|
||||
let status = TokioCommand::new("tar")
|
||||
.args([
|
||||
"xzf",
|
||||
&tarball_path.to_string_lossy(),
|
||||
|
|
@ -330,6 +330,7 @@ pub(crate) async fn run_upgrade(
|
|||
&tmp_dir.path().to_string_lossy(),
|
||||
])
|
||||
.status()
|
||||
.await
|
||||
.context("failed to run tar")?;
|
||||
if !status.success() {
|
||||
bail!("tar extraction failed");
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use anyhow::anyhow;
|
|||
use fabro_github::GitHubCredentials;
|
||||
use fabro_types::settings::server::GithubIntegrationStrategy;
|
||||
|
||||
pub(crate) fn build_github_credentials(
|
||||
pub(crate) async fn build_github_credentials(
|
||||
strategy: GithubIntegrationStrategy,
|
||||
app_id: Option<&str>,
|
||||
) -> anyhow::Result<Option<GitHubCredentials>> {
|
||||
|
|
@ -11,6 +11,7 @@ pub(crate) fn build_github_credentials(
|
|||
GitHubCredentials::from_env(app_id).map_err(|err| anyhow!(err))
|
||||
}
|
||||
GithubIntegrationStrategy::GhCli => fabro_github::gh_auth_token()
|
||||
.await
|
||||
.map(|token| Some(GitHubCredentials::Token(token)))
|
||||
.map_err(|err| anyhow!(err)),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ impl LinuxSleepInhibitor {
|
|||
cmd.spawn()
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "Sleep inhibitor ownership is tied to a std::process::Child dropped synchronously with pre-exec hooks."
|
||||
)]
|
||||
fn try_systemd_inhibit() -> Option<Self> {
|
||||
let mut cmd = Command::new("systemd-inhibit");
|
||||
cmd.args([
|
||||
|
|
@ -52,6 +56,10 @@ impl LinuxSleepInhibitor {
|
|||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "Sleep inhibitor ownership is tied to a std::process::Child dropped synchronously with pre-exec hooks."
|
||||
)]
|
||||
fn try_gnome_inhibit() -> Option<Self> {
|
||||
let mut cmd = Command::new("gnome-session-inhibit");
|
||||
cmd.args([
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "These CLI integration tests synchronously probe for dot before exercising JSON output paths."
|
||||
)]
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use fabro_test::test_context;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "These CLI integration tests spawn real fabro worker subprocesses and observe their lifecycle."
|
||||
)]
|
||||
|
||||
use std::io::Read;
|
||||
use std::process::{Child, ExitStatus, Output, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
|
@ -640,11 +645,11 @@ fn worker_exits_with_retro_enabled_even_when_stdin_stays_open() {
|
|||
|
||||
context.write_temp(
|
||||
".fabro/project.toml",
|
||||
r#"_version = 1
|
||||
r"_version = 1
|
||||
|
||||
[run.execution]
|
||||
retros = true
|
||||
"#,
|
||||
",
|
||||
);
|
||||
context.write_temp(
|
||||
"retro-success.fabro",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
clippy::manual_assert,
|
||||
clippy::redundant_closure_for_method_calls
|
||||
)]
|
||||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "These CLI integration test helpers shell out to real git and fabro binaries while constructing fixtures."
|
||||
)]
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This recovery scenario test uses the real git CLI to set up repository history for end-to-end assertions."
|
||||
)]
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
|
|
|
|||
|
|
@ -165,12 +165,12 @@ strategy = "app"
|
|||
#[test]
|
||||
fn defaults_github_integration_strategy_to_gh_cli() {
|
||||
let file = parse(
|
||||
r#"
|
||||
r"
|
||||
_version = 1
|
||||
|
||||
[server.integrations.github]
|
||||
enabled = true
|
||||
"#,
|
||||
",
|
||||
);
|
||||
|
||||
let settings =
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::process::Command;
|
||||
|
||||
pub const GITHUB_API_BASE_URL: &str = "https://api.github.com";
|
||||
|
||||
|
|
@ -120,10 +121,11 @@ impl GitHubCredentials {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn gh_auth_token() -> Result<String, String> {
|
||||
let output = std::process::Command::new("gh")
|
||||
pub async fn gh_auth_token() -> Result<String, String> {
|
||||
let output = Command::new("gh")
|
||||
.args(["auth", "token"])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|err| format!("Failed to run `gh auth token`: {err}"))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
|
|
|
|||
|
|
@ -89,6 +89,10 @@ pub fn postprocess_svg(raw: Vec<u8>) -> Vec<u8> {
|
|||
}
|
||||
|
||||
/// Render styled DOT source into the given format via the `dot` command.
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous rendering helper is intentionally called behind spawn_blocking from async server code."
|
||||
)]
|
||||
pub fn render_dot(source: &str, format: GraphFormat) -> anyhow::Result<Vec<u8>> {
|
||||
let styled_source = inject_dot_style_defaults(source);
|
||||
let mut child = match Command::new("dot")
|
||||
|
|
@ -127,6 +131,10 @@ pub fn render_dot(source: &str, format: GraphFormat) -> anyhow::Result<Vec<u8>>
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous test probe checks whether dot is installed before running render assertions."
|
||||
)]
|
||||
fn dot_is_available() -> bool {
|
||||
Command::new("dot")
|
||||
.arg("-V")
|
||||
|
|
|
|||
|
|
@ -72,6 +72,43 @@ impl LocalSandbox {
|
|||
self.working_directory.join(p)
|
||||
}
|
||||
}
|
||||
|
||||
fn binary_on_path(binary: &str) -> bool {
|
||||
let Some(paths) = std::env::var_os("PATH") else {
|
||||
return false;
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let extensions: Vec<String> = std::env::var_os("PATHEXT")
|
||||
.map(|value| {
|
||||
value
|
||||
.to_string_lossy()
|
||||
.split(';')
|
||||
.map(|ext| ext.to_ascii_lowercase())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec![".exe".to_string(), ".cmd".to_string(), ".bat".to_string()]);
|
||||
|
||||
for dir in std::env::split_paths(&paths) {
|
||||
let candidate = dir.join(binary);
|
||||
if candidate.is_file() {
|
||||
return true;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if candidate.extension().is_none() {
|
||||
for ext in &extensions {
|
||||
if dir.join(format!("{binary}{ext}")).is_file() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
@ -267,15 +304,7 @@ impl Sandbox for LocalSandbox {
|
|||
let full_path = self.resolve_path(path);
|
||||
|
||||
// Try rg (ripgrep) first, fall back to grep
|
||||
let use_rg = *self.rg_available.get_or_init(|| {
|
||||
std::process::Command::new("rg")
|
||||
.arg("--version")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
});
|
||||
let use_rg = *self.rg_available.get_or_init(|| Self::binary_on_path("rg"));
|
||||
|
||||
let output = if use_rg {
|
||||
let mut args = vec!["-n".to_string()];
|
||||
|
|
@ -293,9 +322,10 @@ impl Sandbox for LocalSandbox {
|
|||
args.push(pattern.into());
|
||||
args.push(full_path.to_string_lossy().into_owned());
|
||||
|
||||
std::process::Command::new("rg")
|
||||
Command::new("rg")
|
||||
.args(&args)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to run rg: {e}"))?
|
||||
} else {
|
||||
let mut args = vec!["-rn".to_string()];
|
||||
|
|
@ -313,9 +343,10 @@ impl Sandbox for LocalSandbox {
|
|||
args.push(pattern.into());
|
||||
args.push(full_path.to_string_lossy().into_owned());
|
||||
|
||||
std::process::Command::new("grep")
|
||||
Command::new("grep")
|
||||
.args(&args)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to run grep: {e}"))?
|
||||
};
|
||||
|
||||
|
|
@ -454,6 +485,10 @@ impl Sandbox for LocalSandbox {
|
|||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous host metadata probe only runs uname once while building the sandbox platform string."
|
||||
)]
|
||||
fn os_version(&self) -> String {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
@ -15,6 +14,7 @@ use fabro_util::version::FABRO_VERSION;
|
|||
use regex::Regex;
|
||||
use semver::Version;
|
||||
use serde::Serialize;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::server::AppState;
|
||||
|
|
@ -44,8 +44,8 @@ fn parse_version(re: &Regex, output: &str) -> Option<Version> {
|
|||
))
|
||||
}
|
||||
|
||||
fn probe_dot() -> ProbeOutcome {
|
||||
let result = Command::new("dot").arg("-V").output().ok();
|
||||
async fn probe_dot() -> ProbeOutcome {
|
||||
let result = Command::new("dot").arg("-V").output().await.ok();
|
||||
match result {
|
||||
None => ProbeOutcome::NotFound,
|
||||
Some(output) if !output.status.success() => ProbeOutcome::Failed,
|
||||
|
|
@ -59,8 +59,8 @@ fn probe_dot() -> ProbeOutcome {
|
|||
}
|
||||
}
|
||||
|
||||
fn check_dot() -> CheckResult {
|
||||
let outcome = probe_dot();
|
||||
async fn check_dot() -> CheckResult {
|
||||
let outcome = probe_dot().await;
|
||||
let (status, summary, remediation) = match &outcome {
|
||||
ProbeOutcome::NotFound => (
|
||||
CheckStatus::Warning,
|
||||
|
|
@ -154,10 +154,11 @@ fn validate_session_secret(value: &str) -> Result<(), String> {
|
|||
}
|
||||
|
||||
pub async fn run_all(state: &AppState) -> DiagnosticsReport {
|
||||
let (llm, github, brave) = tokio::join!(
|
||||
let (llm, github, brave, dot) = tokio::join!(
|
||||
check_llm_providers(state),
|
||||
check_github_app(state),
|
||||
check_brave_search(state),
|
||||
check_dot(),
|
||||
);
|
||||
let sandbox = check_sandbox(state);
|
||||
let crypto = check_crypto(state);
|
||||
|
|
@ -171,7 +172,7 @@ pub async fn run_all(state: &AppState) -> DiagnosticsReport {
|
|||
},
|
||||
CheckSection {
|
||||
title: "System".to_string(),
|
||||
checks: vec![check_dot()],
|
||||
checks: vec![dot],
|
||||
},
|
||||
CheckSection {
|
||||
title: "Configuration".to_string(),
|
||||
|
|
|
|||
|
|
@ -447,6 +447,11 @@ impl<S: Send + Sync> FromRequestParts<S> for AuthenticatedSubject {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "These unit tests use the host openssl CLI to generate certificate fixtures for auth validation."
|
||||
)]
|
||||
|
||||
use axum::body::{Body, to_bytes};
|
||||
use axum::http::{Request, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
|
|
|
|||
|
|
@ -1061,12 +1061,12 @@ app_id = "snapshotted-app-id"
|
|||
manifest.configs.push(types::ManifestConfig {
|
||||
path: Some("/tmp/project/.fabro/project.toml".to_string()),
|
||||
source: Some(
|
||||
r#"
|
||||
r"
|
||||
_version = 1
|
||||
|
||||
[run.pull_request]
|
||||
enabled = true
|
||||
"#
|
||||
"
|
||||
.to_string(),
|
||||
),
|
||||
type_: types::ManifestConfigType::Project,
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ fn spawn_detached_unix(args: &[&str], env: &[(&str, &str)], env_remove: &[&str])
|
|||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "Detached Windows subprocess creation requires std::process::Command creation_flags support."
|
||||
)]
|
||||
fn spawn_detached_windows(args: &[&str], env: &[(&str, &str)], env_remove: &[&str]) {
|
||||
use std::os::windows::process::CommandExt;
|
||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ mod tests {
|
|||
use std::collections::HashMap;
|
||||
|
||||
use fabro_util::env::TestEnv;
|
||||
use toml::map::Map;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
@ -178,7 +179,7 @@ mod tests {
|
|||
fn renders_nested_input_variable() {
|
||||
let ctx = TemplateContext::new().with_inputs(HashMap::from([(
|
||||
"repo".to_string(),
|
||||
toml::Value::Table(toml::map::Map::from_iter([(
|
||||
toml::Value::Table(Map::from_iter([(
|
||||
"name".to_string(),
|
||||
toml::Value::String("fabro".to_string()),
|
||||
)])),
|
||||
|
|
|
|||
|
|
@ -150,6 +150,10 @@ fn session_refs() -> &'static Mutex<HashMap<PathBuf, usize>> {
|
|||
SESSION_REFS.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous test-support helper uses uuidgen when available to create stable unique case IDs."
|
||||
)]
|
||||
fn test_case_id() -> String {
|
||||
let ulid = std::process::Command::new("uuidgen")
|
||||
.arg("-r")
|
||||
|
|
@ -598,6 +602,10 @@ fn wait_for_server_running(server: &ServerPaths) {
|
|||
);
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous test-support helper launches the real fabro CLI server before reqwest clients connect to it."
|
||||
)]
|
||||
fn ensure_server_running(fabro_bin: &Path, server: &ServerPaths, config_path: &Path) {
|
||||
if server_running(server) {
|
||||
return;
|
||||
|
|
@ -1093,6 +1101,10 @@ impl TestContext {
|
|||
}
|
||||
|
||||
/// Initialize a git repository in `temp_dir`.
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous test-support helper initializes fixture repositories with the real git CLI."
|
||||
)]
|
||||
pub fn git_init(&self) -> &Self {
|
||||
std::process::Command::new("git")
|
||||
.args(["init"])
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ fn git_error(msg: impl Into<String>) -> Error {
|
|||
}
|
||||
|
||||
/// Return a pre-configured `git` command with auto-maintenance disabled.
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This shared synchronous git helper layer is used by sync code; async callers must wrap it in spawn_blocking."
|
||||
)]
|
||||
fn git_cmd(dir: &Path) -> Command {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.args(["-c", "maintenance.auto=0", "-c", "gc.auto=0"])
|
||||
|
|
@ -323,6 +327,10 @@ mod tests {
|
|||
use crate::run_dump::RunDump;
|
||||
|
||||
/// Create a temporary git repo with an initial commit.
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous test helper shells out to git while constructing fixture repositories."
|
||||
)]
|
||||
fn init_repo(dir: &Path) {
|
||||
Command::new("git")
|
||||
.args(["init"])
|
||||
|
|
@ -386,6 +394,10 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous test verifies git branch listing against the real git CLI."
|
||||
)]
|
||||
fn create_branch_and_list() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
init_repo(dir.path());
|
||||
|
|
|
|||
|
|
@ -85,11 +85,15 @@ async fn resolve_worktree_plan(options: &mut InitOptions) -> Result<Option<Workt
|
|||
.host_repo_path
|
||||
.clone()
|
||||
.or_else(|| options.sandbox.host_repo_path());
|
||||
let git_status = host_repo_path
|
||||
.as_ref()
|
||||
.map_or(GitSyncStatus::Dirty, |path| {
|
||||
git::sync_status(path, "origin", options.run_options.base_branch.as_deref())
|
||||
});
|
||||
let git_status = if let Some(path) = host_repo_path.as_ref() {
|
||||
let path = path.clone();
|
||||
let base_branch = options.run_options.base_branch.clone();
|
||||
spawn_blocking(move || git::sync_status(&path, "origin", base_branch.as_deref()))
|
||||
.await
|
||||
.unwrap_or(GitSyncStatus::Dirty)
|
||||
} else {
|
||||
GitSyncStatus::Dirty
|
||||
};
|
||||
let strategy = options.sandbox.workdir_strategy(
|
||||
worktree_mode,
|
||||
git_status.is_clean(),
|
||||
|
|
@ -166,7 +170,10 @@ async fn resolve_worktree_plan(options: &mut InitOptions) -> Result<Option<Workt
|
|||
options.run_options.display_base_sha = None;
|
||||
return Ok(None);
|
||||
};
|
||||
match git::head_sha(&repo_path) {
|
||||
match spawn_blocking(move || git::head_sha(&repo_path))
|
||||
.await
|
||||
.unwrap_or_else(|_| Err(Error::engine("git head_sha task panicked")))
|
||||
{
|
||||
Ok(base_sha) => {
|
||||
options.run_options.display_base_sha = Some(base_sha.clone());
|
||||
Ok(Some(WorktreePlan {
|
||||
|
|
@ -189,9 +196,15 @@ async fn resolve_worktree_plan(options: &mut InitOptions) -> Result<Option<Workt
|
|||
}
|
||||
}
|
||||
WorkdirStrategy::Cloud => {
|
||||
options.run_options.display_base_sha = host_repo_path
|
||||
.as_ref()
|
||||
.and_then(|path| git::head_sha(path).ok());
|
||||
options.run_options.display_base_sha = if let Some(path) = host_repo_path.as_ref() {
|
||||
let path = path.clone();
|
||||
spawn_blocking(move || git::head_sha(&path))
|
||||
.await
|
||||
.ok()
|
||||
.and_then(std::result::Result::ok)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
WorkdirStrategy::LocalDirectory => {
|
||||
|
|
|
|||
|
|
@ -238,6 +238,11 @@ pub async fn git_replace_worktree(sandbox: &dyn Sandbox, path: &str, branch: &st
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "These unit tests use the real git CLI to construct sandbox-git fixture repositories."
|
||||
)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
|
|
@ -68,6 +68,11 @@ impl Transform for FileInliningTransform {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "These unit tests use the real git CLI to build repositories for file-inlining transform coverage."
|
||||
)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use fabro_graphviz::graph::{AttrValue, Graph, Node};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
clippy::items_after_statements,
|
||||
clippy::print_stderr
|
||||
)]
|
||||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "These Daytona integration tests use the real git CLI to prepare remote-repo fixtures for workflow runs."
|
||||
)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "These git integration tests intentionally exercise the real git CLI to validate repository helper behavior."
|
||||
)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
|
|
@ -18,7 +23,7 @@ use fabro_workflow::handler::start::StartHandler;
|
|||
use fabro_workflow::run_options::{GitCheckpointOptions, RunOptions};
|
||||
use fabro_workflow::test_support::run_graph;
|
||||
|
||||
fn assert_success(output: Output, context: &str) {
|
||||
fn assert_success(output: &Output, context: &str) {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{context} failed: {}",
|
||||
|
|
@ -28,86 +33,74 @@ fn assert_success(output: Output, context: &str) {
|
|||
|
||||
fn init_repo(dir: &Path) {
|
||||
std::fs::create_dir_all(dir).unwrap();
|
||||
assert_success(
|
||||
Command::new("git")
|
||||
.args(["init"])
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.unwrap(),
|
||||
"git init",
|
||||
);
|
||||
assert_success(
|
||||
Command::new("git")
|
||||
.args([
|
||||
"-c",
|
||||
"user.name=test",
|
||||
"-c",
|
||||
"user.email=test@test",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
"init",
|
||||
])
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.unwrap(),
|
||||
"git commit --allow-empty",
|
||||
);
|
||||
let init = Command::new("git")
|
||||
.args(["init"])
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_success(&init, "git init");
|
||||
let commit = Command::new("git")
|
||||
.args([
|
||||
"-c",
|
||||
"user.name=test",
|
||||
"-c",
|
||||
"user.email=test@test",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
"init",
|
||||
])
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_success(&commit, "git commit --allow-empty");
|
||||
}
|
||||
|
||||
fn init_bare_remote(dir: &Path) {
|
||||
std::fs::create_dir_all(dir.parent().unwrap()).unwrap();
|
||||
assert_success(
|
||||
Command::new("git")
|
||||
.args(["init", "--bare"])
|
||||
.arg(dir)
|
||||
.output()
|
||||
.unwrap(),
|
||||
"git init --bare",
|
||||
);
|
||||
let init = Command::new("git")
|
||||
.args(["init", "--bare"])
|
||||
.arg(dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_success(&init, "git init --bare");
|
||||
}
|
||||
|
||||
fn add_origin(repo_dir: &Path, remote_dir: &Path) {
|
||||
assert_success(
|
||||
Command::new("git")
|
||||
.args(["remote", "add", "origin"])
|
||||
.arg(remote_dir)
|
||||
.current_dir(repo_dir)
|
||||
.output()
|
||||
.unwrap(),
|
||||
"git remote add origin",
|
||||
);
|
||||
let output = Command::new("git")
|
||||
.args(["remote", "add", "origin"])
|
||||
.arg(remote_dir)
|
||||
.current_dir(repo_dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_success(&output, "git remote add origin");
|
||||
}
|
||||
|
||||
fn rename_branch(repo_dir: &Path, branch: &str) {
|
||||
assert_success(
|
||||
Command::new("git")
|
||||
.args(["branch", "-M", branch])
|
||||
.current_dir(repo_dir)
|
||||
.output()
|
||||
.unwrap(),
|
||||
"git branch -M",
|
||||
);
|
||||
let output = Command::new("git")
|
||||
.args(["branch", "-M", branch])
|
||||
.current_dir(repo_dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_success(&output, "git branch -M");
|
||||
}
|
||||
|
||||
fn empty_commit(repo_dir: &Path, message: &str) {
|
||||
assert_success(
|
||||
Command::new("git")
|
||||
.args([
|
||||
"-c",
|
||||
"user.name=test",
|
||||
"-c",
|
||||
"user.email=test@test",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
message,
|
||||
])
|
||||
.current_dir(repo_dir)
|
||||
.output()
|
||||
.unwrap(),
|
||||
"git commit --allow-empty",
|
||||
);
|
||||
let output = Command::new("git")
|
||||
.args([
|
||||
"-c",
|
||||
"user.name=test",
|
||||
"-c",
|
||||
"user.email=test@test",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
message,
|
||||
])
|
||||
.current_dir(repo_dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_success(&output, "git commit --allow-empty");
|
||||
}
|
||||
|
||||
fn list_branch(repo_dir: &Path, branch: &str) -> String {
|
||||
|
|
@ -116,7 +109,7 @@ fn list_branch(repo_dir: &Path, branch: &str) -> String {
|
|||
.current_dir(repo_dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_success(output.clone(), "git branch --list");
|
||||
assert_success(&output, "git branch --list");
|
||||
String::from_utf8(output.stdout).unwrap()
|
||||
}
|
||||
|
||||
|
|
@ -293,13 +286,13 @@ async fn git_checkpoint_skips_start_node() {
|
|||
});
|
||||
run_options.host_repo_path = Some(PathBuf::from(repo));
|
||||
|
||||
run_graph(
|
||||
Box::pin(run_graph(
|
||||
make_registry(),
|
||||
Arc::new(emitter),
|
||||
local_env(repo),
|
||||
&g,
|
||||
&run_options,
|
||||
)
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
clippy::unnecessary_literal_bound,
|
||||
clippy::unreadable_literal
|
||||
)]
|
||||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "These end-to-end workflow integration tests use the real git CLI to verify checkpoint and branch behavior."
|
||||
)]
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ use crate::server::SharedState;
|
|||
use crate::state::{PermissionLevel, TokenPermission};
|
||||
|
||||
/// Find the git-http-backend binary by querying `git --exec-path`.
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous test harness helper resolves the git-http-backend path before launching the CGI subprocess."
|
||||
)]
|
||||
fn find_git_http_backend() -> Result<PathBuf, String> {
|
||||
let output = std::process::Command::new("git")
|
||||
.arg("--exec-path")
|
||||
|
|
|
|||
|
|
@ -258,6 +258,10 @@ pub fn derive_public_key_pem(private_key_pem: &str) -> String {
|
|||
return TEST_RSA_PUBLIC_PEM.to_string();
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous test harness helper derives an RSA public key with the host openssl CLI."
|
||||
)]
|
||||
let mut child = Command::new("openssl")
|
||||
.args(["rsa", "-pubout"])
|
||||
.stdin(Stdio::piped())
|
||||
|
|
@ -391,6 +395,10 @@ pub fn init_bare_repo(
|
|||
}
|
||||
std::fs::create_dir_all(&repo_dir)
|
||||
.map_err(|e| format!("failed to create git dir {}: {e}", repo_dir.display()))?;
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous test harness helper initializes fixture git repositories with the real git CLI."
|
||||
)]
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["init", "--bare"])
|
||||
.arg(&repo_dir)
|
||||
|
|
@ -404,6 +412,10 @@ pub fn init_bare_repo(
|
|||
}
|
||||
|
||||
// Enable http.receivepack so push works via git-http-backend
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "This synchronous test harness helper configures fixture git repositories with the real git CLI."
|
||||
)]
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["config", "http.receivepack", "true"])
|
||||
.current_dir(&repo_dir)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
#![expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "These browser-debug integration tests synchronously probe for Chrome binaries before launching external tooling."
|
||||
)]
|
||||
|
||||
mod common;
|
||||
|
||||
use serde_json::json;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue