From 1520b873e67349712734f252f34cb589cabb0c17 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Tue, 29 Sep 2026 13:08:16 -0700 Subject: [PATCH 1/2] fix(daemon): resolve test-emu project_dir against the caller's cwd (#1415) --- .../src/handlers/emulator/mod.rs | 2 + .../src/handlers/emulator/select.rs | 13 ++++- .../handlers/emulator/tests_project_dir.rs | 58 +++++++++++++++++++ .../src/handlers/operations/mod.rs | 2 +- 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 crates/fbuild-daemon/src/handlers/emulator/tests_project_dir.rs diff --git a/crates/fbuild-daemon/src/handlers/emulator/mod.rs b/crates/fbuild-daemon/src/handlers/emulator/mod.rs index 46785a50f..611c823d8 100644 --- a/crates/fbuild-daemon/src/handlers/emulator/mod.rs +++ b/crates/fbuild-daemon/src/handlers/emulator/mod.rs @@ -23,6 +23,8 @@ mod tests_outcome; #[cfg(test)] mod tests_process; #[cfg(test)] +mod tests_project_dir; +#[cfg(test)] mod tests_select_runner; // --- Public API re-exports (preserve `handlers::emulator::*` paths) --- diff --git a/crates/fbuild-daemon/src/handlers/emulator/select.rs b/crates/fbuild-daemon/src/handlers/emulator/select.rs index ff05ee576..3e6656742 100644 --- a/crates/fbuild-daemon/src/handlers/emulator/select.rs +++ b/crates/fbuild-daemon/src/handlers/emulator/select.rs @@ -146,7 +146,11 @@ pub async fn test_emu( .request_id .clone() .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); - let project_dir = PathBuf::from(&req.project_dir); + // Resolve against the caller's cwd, not the daemon's (FastLED/fbuild#1415). + let project_dir = crate::handlers::operations::resolve_request_project_dir( + &req.project_dir, + req.caller_cwd.as_deref(), + ); // Mark the daemon as busy for the full build + emulate lifecycle. // Without this guard the 30 s self-eviction loop sees an "empty" @@ -157,7 +161,7 @@ pub async fn test_emu( let _op_guard = crate::handlers::operations::OperationGuard::new( &ctx, fbuild_core::DaemonState::Building, - Some(format!("test-emu {}", req.project_dir)), + Some(format!("test-emu {}", project_dir.display())), ); if !project_dir.exists() { @@ -165,7 +169,10 @@ pub async fn test_emu( StatusCode::BAD_REQUEST, Json(OperationResponse::fail( request_id, - format!("project directory does not exist: {}", req.project_dir), + format!( + "project directory does not exist: {}", + project_dir.display() + ), )), ); } diff --git a/crates/fbuild-daemon/src/handlers/emulator/tests_project_dir.rs b/crates/fbuild-daemon/src/handlers/emulator/tests_project_dir.rs new file mode 100644 index 000000000..e571bfd20 --- /dev/null +++ b/crates/fbuild-daemon/src/handlers/emulator/tests_project_dir.rs @@ -0,0 +1,58 @@ +//! `POST /api/test-emu` must read a relative `project_dir` against the +//! caller's cwd, not the daemon's (FastLED/fbuild#1415). + +use super::select::test_emu; +use crate::context::DaemonContext; +use crate::models::TestEmuRequest; +use axum::Json; +use axum::extract::State; +use std::sync::Arc; + +fn request(project_dir: &str, caller_cwd: &std::path::Path) -> TestEmuRequest { + serde_json::from_value(serde_json::json!({ + "project_dir": project_dir, + "caller_cwd": caller_cwd.to_str().unwrap(), + })) + .unwrap() +} + +#[tokio::test] +async fn relative_project_dir_resolves_against_caller_cwd() { + let (shutdown_tx, _rx) = tokio::sync::watch::channel(false); + let ctx = Arc::new(DaemonContext::new(0, shutdown_tx, "test".to_string())); + let caller = tempfile::TempDir::new().unwrap(); + // A platform no runner knows: reaching this error proves the handler read + // the caller's platformio.ini rather than the daemon cwd's (which has none). + std::fs::write( + caller.path().join("platformio.ini"), + "[env:x]\nplatform = bogus-platform-1415\nboard = x\n", + ) + .unwrap(); + + let (_, Json(resp)) = test_emu(State(ctx), Json(request(".", caller.path()))).await; + + assert!(!resp.success); + assert!( + resp.message.contains("bogus-platform-1415"), + "handler did not read the caller's project: {}", + resp.message + ); +} + +#[tokio::test] +async fn missing_relative_project_dir_reports_the_resolved_path() { + let (shutdown_tx, _rx) = tokio::sync::watch::channel(false); + let ctx = Arc::new(DaemonContext::new(0, shutdown_tx, "test".to_string())); + let caller = tempfile::TempDir::new().unwrap(); + + let (_, Json(resp)) = test_emu(State(ctx), Json(request("nope", caller.path()))).await; + + assert!(!resp.success); + let expected = caller.path().join("nope"); + assert!( + resp.message.contains(&expected.display().to_string()), + "message should name the resolved path {}: {}", + expected.display(), + resp.message + ); +} diff --git a/crates/fbuild-daemon/src/handlers/operations/mod.rs b/crates/fbuild-daemon/src/handlers/operations/mod.rs index 347cc610c..2f956bef6 100644 --- a/crates/fbuild-daemon/src/handlers/operations/mod.rs +++ b/crates/fbuild-daemon/src/handlers/operations/mod.rs @@ -27,5 +27,5 @@ pub use reset::reset; // `pub(crate)` re-exports for sibling handler modules // (`handlers::emulator` consumes these). -pub(crate) use common::{OperationGuard, qemu_extra_build_flags}; +pub(crate) use common::{OperationGuard, qemu_extra_build_flags, resolve_request_project_dir}; pub(crate) use monitor::{MonitorOutcome, MonitorState}; From 31ea47f677ed58107ab86f0b37203235defd450f Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Tue, 29 Sep 2026 13:27:37 -0700 Subject: [PATCH 2/2] fix(test): root test-emu project_dir tempdirs under fbuild temp root (dylint) --- .../fbuild-daemon/src/handlers/emulator/tests_project_dir.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/fbuild-daemon/src/handlers/emulator/tests_project_dir.rs b/crates/fbuild-daemon/src/handlers/emulator/tests_project_dir.rs index e571bfd20..f186e773a 100644 --- a/crates/fbuild-daemon/src/handlers/emulator/tests_project_dir.rs +++ b/crates/fbuild-daemon/src/handlers/emulator/tests_project_dir.rs @@ -20,7 +20,7 @@ fn request(project_dir: &str, caller_cwd: &std::path::Path) -> TestEmuRequest { async fn relative_project_dir_resolves_against_caller_cwd() { let (shutdown_tx, _rx) = tokio::sync::watch::channel(false); let ctx = Arc::new(DaemonContext::new(0, shutdown_tx, "test".to_string())); - let caller = tempfile::TempDir::new().unwrap(); + let caller = tempfile::tempdir_in(fbuild_paths::temp_subdir("daemon-tests")).unwrap(); // A platform no runner knows: reaching this error proves the handler read // the caller's platformio.ini rather than the daemon cwd's (which has none). std::fs::write( @@ -43,7 +43,7 @@ async fn relative_project_dir_resolves_against_caller_cwd() { async fn missing_relative_project_dir_reports_the_resolved_path() { let (shutdown_tx, _rx) = tokio::sync::watch::channel(false); let ctx = Arc::new(DaemonContext::new(0, shutdown_tx, "test".to_string())); - let caller = tempfile::TempDir::new().unwrap(); + let caller = tempfile::tempdir_in(fbuild_paths::temp_subdir("daemon-tests")).unwrap(); let (_, Json(resp)) = test_emu(State(ctx), Json(request("nope", caller.path()))).await;