diff --git a/.fabro/project.toml b/.fabro/project.toml index 24f0d8384..fdc5cea87 100644 --- a/.fabro/project.toml +++ b/.fabro/project.toml @@ -6,6 +6,7 @@ draft = false [run.sandbox] provider = "daytona" +preserve = true [run.sandbox.daytona] auto_stop_interval = 30 diff --git a/lib/crates/fabro-cli/tests/it/cmd/attach.rs b/lib/crates/fabro-cli/tests/it/cmd/attach.rs index 795ff19a2..bfd19be7b 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/attach.rs @@ -9,7 +9,7 @@ use serde_json::Value; use super::support::{ output_stdout, resolve_run, server_endpoint, wait_for_status, write_gated_workflow, }; -use crate::support::{example_fixture, fabro_json_snapshot, run_output_filters, unique_run_id}; +use crate::support::{fabro_json_snapshot, run_output_filters, unique_run_id}; const SHARED_DAEMON_TIMEOUT: Duration = Duration::from_secs(30); @@ -121,6 +121,7 @@ fn wait_for_output_signal( fn attach_replays_completed_detached_run() { let context = test_context!(); let run_id = unique_run_id(); + let workflow = context.install_fixture("simple.fabro"); context .command() @@ -132,7 +133,7 @@ fn attach_replays_completed_detached_run() { "--detach", "--run-id", run_id.as_str(), - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .assert() .success(); diff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/crates/fabro-cli/tests/it/cmd/run.rs index 71c2a1e46..519984f6f 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/run.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/run.rs @@ -7,7 +7,7 @@ use httpmock::MockServer; use serde_json::Value; use super::support::{output_stderr, wait_for_event_names}; -use crate::support::{example_fixture, fabro_json_snapshot, run_output_filters, unique_run_id}; +use crate::support::{fabro_json_snapshot, run_output_filters, unique_run_id}; fn run_status_response(run_id: &str, status: &str) -> serde_json::Value { serde_json::json!({ @@ -185,6 +185,7 @@ fn detach_uses_explicit_server_target_and_prints_remote_run_id() { .body(run_status_response(run_id.as_str(), "queued").to_string()); }); + let workflow = context.install_fixture("simple.fabro"); let output = context .run_cmd() .args([ @@ -193,7 +194,7 @@ fn detach_uses_explicit_server_target_and_prints_remote_run_id() { "--detach", "--dry-run", "--auto-approve", - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .output() .expect("command should execute"); @@ -239,13 +240,14 @@ fn detach_uses_configured_server_target_without_server_flag() { ), ); + let workflow = context.install_fixture("simple.fabro"); let output = context .run_cmd() .args([ "--detach", "--dry-run", "--auto-approve", - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .output() .expect("command should execute"); @@ -358,6 +360,7 @@ digraph VaultWorkerLlm { #[test] fn detach_rejects_storage_dir_flag() { let context = test_context!(); + let workflow = context.install_fixture("simple.fabro"); let output = context .run_cmd() .args([ @@ -367,7 +370,7 @@ fn detach_rejects_storage_dir_flag() { "--dry-run", "--auto-approve", "--no-retro", - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .output() .expect("command should execute"); @@ -417,6 +420,7 @@ fn detach_cli_server_target_overrides_configured_server_target() { ), ); + let workflow = context.install_fixture("simple.fabro"); let output = context .run_cmd() .args([ @@ -425,7 +429,7 @@ fn detach_cli_server_target_overrides_configured_server_target() { "--detach", "--dry-run", "--auto-approve", - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .output() .expect("command should execute"); @@ -521,6 +525,7 @@ fn remote_foreground_run_consumes_paginated_events_and_prints_server_backed_summ .body(remote_run_state_response().to_string()); }); + let workflow = context.install_fixture("simple.fabro"); let output = context .run_cmd() .args([ @@ -528,7 +533,7 @@ fn remote_foreground_run_consumes_paginated_events_and_prints_server_backed_summ &format!("{}/api/v1", server.base_url()), "--dry-run", "--auto-approve", - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .output() .expect("command should execute"); @@ -630,16 +635,17 @@ include = ["assets/**"] #[test] fn dry_run_simple() { let context = test_context!(); + let workflow = context.install_fixture("simple.fabro"); let mut cmd = context.run_cmd(); cmd.args(["--dry-run", "--auto-approve"]); - cmd.arg(example_fixture("simple.fabro")); + cmd.arg(&workflow); fabro_snapshot!(run_output_filters(&context), cmd, @" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Workflow: Simple (4 nodes, 3 edges) - Graph: [FIXTURES]/simple.fabro + Graph: [TEMP_DIR]/simple.fabro Goal: Run tests and report results Run: [ULID] @@ -671,10 +677,11 @@ fn dry_run_with_goal_file_reads_contents_into_goal() { let goal_path = goal_dir.path().join("goal.md"); std::fs::write(&goal_path, "Ship the rate-limiting feature end to end.\n").unwrap(); + let workflow = context.install_fixture("simple.fabro"); let mut cmd = context.run_cmd(); cmd.args(["--dry-run", "--auto-approve", "--goal-file"]); cmd.arg(&goal_path); - cmd.arg(example_fixture("simple.fabro")); + cmd.arg(&workflow); let output = cmd.output().expect("run command should execute"); assert!( @@ -698,10 +705,11 @@ fn dry_run_rejects_goal_and_goal_file_together() { let goal_path = goal_dir.path().join("goal.md"); std::fs::write(&goal_path, "never read").unwrap(); + let workflow = context.install_fixture("simple.fabro"); let mut cmd = context.run_cmd(); cmd.args(["--dry-run", "--goal", "inline override", "--goal-file"]); cmd.arg(&goal_path); - cmd.arg(example_fixture("simple.fabro")); + cmd.arg(&workflow); let output = cmd.output().expect("run command should execute"); assert!( !output.status.success(), @@ -720,6 +728,7 @@ fn dry_run_rejects_goal_and_goal_file_together() { fn dry_run_persists_event_history_in_store() { let context = test_context!(); let run_id = unique_run_id(); + let workflow = context.install_fixture("simple.fabro"); context .command() @@ -731,7 +740,7 @@ fn dry_run_persists_event_history_in_store() { "local", "--run-id", run_id.as_str(), - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .assert() .success(); @@ -818,6 +827,7 @@ fn dry_run_persists_event_history_in_store() { fn run_id_passthrough_uses_provided_ulid() { let context = test_context!(); let run_id = unique_run_id(); + let workflow = context.install_fixture("simple.fabro"); context .command() @@ -827,7 +837,7 @@ fn run_id_passthrough_uses_provided_ulid() { "--auto-approve", "--run-id", run_id.as_str(), - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .assert() .success(); @@ -902,12 +912,13 @@ fn json_run_requires_manual_input_for_human_gates_without_auto_approve() { #[test] fn detach_prints_ulid_and_exits() { let context = test_context!(); + let workflow = context.install_fixture("simple.fabro"); let mut cmd = context.run_cmd(); cmd.args([ "--detach", "--dry-run", "--auto-approve", - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]); fabro_snapshot!(context.filters(), cmd, @" success: true @@ -922,6 +933,7 @@ fn detach_prints_ulid_and_exits() { fn detach_creates_run_dir_with_detach_log() { let context = test_context!(); let run_id = unique_run_id(); + let workflow = context.install_fixture("simple.fabro"); context .run_cmd() @@ -931,7 +943,7 @@ fn detach_creates_run_dir_with_detach_log() { "--auto-approve", "--run-id", run_id.as_str(), - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .assert() .success(); diff --git a/lib/crates/fabro-cli/tests/it/cmd/start.rs b/lib/crates/fabro-cli/tests/it/cmd/start.rs index f9c4adb4a..800989341 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/start.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/start.rs @@ -1,7 +1,7 @@ use fabro_test::{fabro_snapshot, test_context}; use super::support::{output_stdout, resolve_run, wait_for_status, write_gated_workflow}; -use crate::support::{example_fixture, fabro_json_snapshot, unique_run_id}; +use crate::support::{fabro_json_snapshot, unique_run_id}; const SHARED_DAEMON_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); @@ -37,6 +37,7 @@ fn help() { fn start_by_run_id_starts_created_run() { let context = test_context!(); let run_id = unique_run_id(); + let workflow = context.install_fixture("simple.fabro"); context .command() @@ -46,7 +47,7 @@ fn start_by_run_id_starts_created_run() { "--auto-approve", "--run-id", run_id.as_str(), - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .assert() .success(); @@ -87,6 +88,7 @@ fn start_by_run_id_starts_created_run() { fn start_by_run_id_starts_created_run_without_run_json_or_status_json() { let context = test_context!(); let run_id = unique_run_id(); + let workflow = context.install_fixture("simple.fabro"); context .command() @@ -96,7 +98,7 @@ fn start_by_run_id_starts_created_run_without_run_json_or_status_json() { "--auto-approve", "--run-id", run_id.as_str(), - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .assert() .success(); diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs index d08732076..b8b814fbe 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/support.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs @@ -65,6 +65,11 @@ enum GitWorkflowKind { Noop, } +/// Returns the repo-relative path to a test fixture. +/// +/// Prefer `TestContext::install_fixture` for tests that run `fabro run`, +/// since config discovery walks from the workflow file's parent directory +/// and can find the repo's `.fabro/project.toml`. pub(crate) fn fixture(name: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join(format!("../../../test/{name}")) @@ -115,7 +120,7 @@ fn run_success_in(context: &TestContext, args: &[&str], cwd: &Path) -> Output { } pub(crate) fn setup_completed_dry_run(context: &TestContext) -> RunSetup { - let workflow = fixture("simple.fabro"); + let workflow = context.install_fixture("simple.fabro"); run_completed_dry_run(context, &workflow) } @@ -160,7 +165,7 @@ fn run_completed_dry_run(context: &TestContext, workflow: &Path) -> RunSetup { } pub(crate) fn setup_created_dry_run(context: &TestContext) -> RunSetup { - let workflow = fixture("simple.fabro"); + let workflow = context.install_fixture("simple.fabro"); run_created_dry_run(context, &workflow) } @@ -228,7 +233,7 @@ fn fast_simple_workflow(context: &TestContext) -> PathBuf { reason = "This sync integration helper polls run artifacts after spawning a detached CLI process." )] pub(crate) fn setup_detached_dry_run(context: &TestContext) -> RunSetup { - let workflow = fixture("simple.fabro"); + let workflow = context.install_fixture("simple.fabro"); let run_id = unique_run_id(); let mut cmd = context.run_cmd(); cmd.current_dir(&context.temp_dir); diff --git a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs index 1413f2487..895e23f92 100644 --- a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs +++ b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs @@ -4,7 +4,7 @@ use fabro_test::test_context; use serde_json::Value; use super::{fixture, run_state, timeout_for}; -use crate::support::{example_fixture, fabro_json_snapshot, unique_run_id}; +use crate::support::{fabro_json_snapshot, unique_run_id}; #[fabro_macros::e2e_test()] fn local_run_lifecycle() { @@ -107,6 +107,7 @@ fn local_run_lifecycle() { fn dry_run_create_start_attach_works_with_default_run_lookup() { let context = test_context!(); let run_id = unique_run_id(); + let workflow = context.install_fixture("simple.fabro"); context .command() @@ -116,7 +117,7 @@ fn dry_run_create_start_attach_works_with_default_run_lookup() { "--auto-approve", "--run-id", run_id.as_str(), - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .assert() .success(); @@ -154,6 +155,7 @@ fn dry_run_create_start_attach_works_with_default_run_lookup() { fn dry_run_detach_attach_works_with_default_run_lookup() { let context = test_context!(); let run_id = unique_run_id(); + let workflow = context.install_fixture("simple.fabro"); context .command() @@ -164,7 +166,7 @@ fn dry_run_detach_attach_works_with_default_run_lookup() { "--auto-approve", "--run-id", run_id.as_str(), - example_fixture("simple.fabro").to_str().unwrap(), + workflow.to_str().unwrap(), ]) .assert() .success(); diff --git a/lib/crates/fabro-cli/tests/it/support/mod.rs b/lib/crates/fabro-cli/tests/it/support/mod.rs index 2907f43ac..c6bc726f1 100644 --- a/lib/crates/fabro-cli/tests/it/support/mod.rs +++ b/lib/crates/fabro-cli/tests/it/support/mod.rs @@ -1,5 +1,3 @@ -use std::path::{Path, PathBuf}; - use assert_cmd::Command; use fabro_store::EventEnvelope; use fabro_test::TestContext; @@ -44,13 +42,6 @@ macro_rules! fabro_json_snapshot { pub(crate) use fabro_json_snapshot; -pub(crate) fn example_fixture(name: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join(format!("../../../test/{name}")) - .canonicalize() - .expect("fixture path should exist") -} - pub(crate) fn run_output_filters(context: &TestContext) -> Vec<(String, String)> { let mut filters = context.filters(); filters.push((r"\b\d+ms\b".to_string(), "[TIME]".to_string())); diff --git a/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs b/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs index c6c14fac5..8355667c6 100644 --- a/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs +++ b/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs @@ -1,20 +1,21 @@ use fabro_test::{fabro_snapshot, test_context}; -use crate::support::{example_fixture, run_output_filters}; +use crate::support::run_output_filters; #[test] fn dry_run_branching() { let context = test_context!(); + let workflow = context.install_fixture("branching.fabro"); let mut cmd = context.run_cmd(); cmd.args(["--dry-run", "--auto-approve"]); - cmd.arg(example_fixture("branching.fabro")); + cmd.arg(&workflow); fabro_snapshot!(run_output_filters(&context), cmd, @" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Workflow: Branch (6 nodes, 6 edges) - Graph: [FIXTURES]/branching.fabro + Graph: [TEMP_DIR]/branching.fabro Goal: Implement and validate a feature warning [node: implement]: Node 'implement' has goal_gate=true but no retry_target or fallback_retry_target (goal_gate_has_retry) @@ -41,16 +42,17 @@ fn dry_run_branching() { #[test] fn dry_run_conditions() { let context = test_context!(); + let workflow = context.install_fixture("conditions.fabro"); let mut cmd = context.run_cmd(); cmd.args(["--dry-run", "--auto-approve"]); - cmd.arg(example_fixture("conditions.fabro")); + cmd.arg(&workflow); fabro_snapshot!(run_output_filters(&context), cmd, @" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Workflow: Conditions (5 nodes, 5 edges) - Graph: [FIXTURES]/conditions.fabro + Graph: [TEMP_DIR]/conditions.fabro Goal: Test condition evaluation with OR and parentheses Run: [ULID] @@ -74,16 +76,17 @@ fn dry_run_conditions() { #[test] fn dry_run_parallel() { let context = test_context!(); + let workflow = context.install_fixture("parallel.fabro"); let mut cmd = context.run_cmd(); cmd.args(["--dry-run", "--auto-approve"]); - cmd.arg(example_fixture("parallel.fabro")); + cmd.arg(&workflow); fabro_snapshot!(run_output_filters(&context), cmd, @" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Workflow: Parallel (7 nodes, 7 edges) - Graph: [FIXTURES]/parallel.fabro + Graph: [TEMP_DIR]/parallel.fabro Goal: Test parallel and fan-in execution Run: [ULID] @@ -108,16 +111,17 @@ fn dry_run_parallel() { #[test] fn dry_run_styled() { let context = test_context!(); + let workflow = context.install_fixture("styled.fabro"); let mut cmd = context.run_cmd(); cmd.args(["--dry-run", "--auto-approve"]); - cmd.arg(example_fixture("styled.fabro")); + cmd.arg(&workflow); fabro_snapshot!(run_output_filters(&context), cmd, @" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Workflow: Styled (5 nodes, 4 edges) - Graph: [FIXTURES]/styled.fabro + Graph: [TEMP_DIR]/styled.fabro Goal: Build a styled pipeline Run: [ULID] @@ -142,16 +146,17 @@ fn dry_run_styled() { #[test] fn dry_run_legacy_tool() { let context = test_context!(); + let workflow = context.install_fixture("legacy_tool.fabro"); let mut cmd = context.run_cmd(); cmd.args(["--dry-run", "--auto-approve"]); - cmd.arg(example_fixture("legacy_tool.fabro")); + cmd.arg(&workflow); fabro_snapshot!(run_output_filters(&context), cmd, @" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Workflow: LegacyTool (3 nodes, 2 edges) - Graph: [FIXTURES]/legacy_tool.fabro + Graph: [TEMP_DIR]/legacy_tool.fabro Goal: Verify backwards compatibility with old tool naming Run: [ULID] diff --git a/lib/crates/fabro-test/src/lib.rs b/lib/crates/fabro-test/src/lib.rs index 0ddb397fc..3046c1c7b 100644 --- a/lib/crates/fabro-test/src/lib.rs +++ b/lib/crates/fabro-test/src/lib.rs @@ -1105,6 +1105,25 @@ impl TestContext { self } + /// Copy a fixture file from the repo `test/` directory into `temp_dir`, + /// isolating the test from the repo's `.fabro/project.toml`. + /// + /// Returns the path to the copied file inside `temp_dir`. + pub fn install_fixture(&self, name: &str) -> PathBuf { + let src = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../../test/{name}")); + let src = src + .canonicalize() + .unwrap_or_else(|_| panic!("fixture {name} not found at {}", src.display())); + let dest = self.temp_dir.join(name); + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent).expect("failed to create parent dirs"); + } + std::fs::copy(&src, &dest) + .unwrap_or_else(|_| panic!("failed to copy fixture {name} to {}", dest.display())); + dest + } + /// Initialize a git repository in `temp_dir`. #[expect( clippy::disallowed_methods,