From b5e5b91d510f5484c7a6e84205c76cb3c9227dfc Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Tue, 31 Mar 2026 17:51:09 -0400 Subject: [PATCH] Default TestContext::command() working directory to temp_dir Tests were flaky because command() inherited the real repo as the working directory. When the repo was clean, detached runs attempted git worktree creation against it, sometimes failing and injecting extra warning lines into snapshots. Now command() defaults to the non-git temp_dir, eliminating this class of flakiness. Tests needing a specific directory override with .current_dir(). Also canonicalizes fixture paths and adds a [FIXTURES] snapshot filter via test_context!() macro. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/crates/fabro-cli/tests/it/cmd/attach.rs | 3 -- lib/crates/fabro-cli/tests/it/cmd/detached.rs | 1 - lib/crates/fabro-cli/tests/it/cmd/exec.rs | 7 ---- lib/crates/fabro-cli/tests/it/cmd/fork.rs | 1 - lib/crates/fabro-cli/tests/it/cmd/parse.rs | 2 - .../fabro-cli/tests/it/cmd/preflight.rs | 2 +- lib/crates/fabro-cli/tests/it/cmd/repo.rs | 15 +------ .../fabro-cli/tests/it/cmd/repo_init.rs | 3 -- lib/crates/fabro-cli/tests/it/cmd/rewind.rs | 1 - lib/crates/fabro-cli/tests/it/cmd/run.rs | 13 +++--- lib/crates/fabro-cli/tests/it/cmd/start.rs | 4 -- lib/crates/fabro-cli/tests/it/cmd/support.rs | 5 ++- lib/crates/fabro-cli/tests/it/cmd/validate.rs | 19 +++++---- lib/crates/fabro-cli/tests/it/cmd/workflow.rs | 1 - .../fabro-cli/tests/it/cmd/workflow_create.rs | 1 - .../fabro-cli/tests/it/cmd/workflow_list.rs | 1 - .../fabro-cli/tests/it/scenario/exec.rs | 1 - .../fabro-cli/tests/it/scenario/lifecycle.rs | 6 +-- lib/crates/fabro-cli/tests/it/support/mod.rs | 5 ++- .../tests/it/workflow/dry_run_examples.rs | 10 ++--- lib/crates/fabro-test/src/lib.rs | 42 +++++++++++++++++-- 21 files changed, 73 insertions(+), 70 deletions(-) diff --git a/lib/crates/fabro-cli/tests/it/cmd/attach.rs b/lib/crates/fabro-cli/tests/it/cmd/attach.rs index 633868d66..0bf4777c7 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/attach.rs @@ -103,7 +103,6 @@ fn attach_before_completion_streams_to_finished_state() { let gate = write_gated_workflow(&context.temp_dir.join("slow.fabro"), "slow", "Run slowly"); let mut run_cmd = context.command(); - run_cmd.current_dir(&context.temp_dir); run_cmd.env("OPENAI_API_KEY", "test"); run_cmd.args([ "run", @@ -136,7 +135,6 @@ fn attach_before_completion_streams_to_finished_state() { gate.release(); }); let mut attach_cmd = context.command(); - attach_cmd.current_dir(&context.temp_dir); attach_cmd.args(["attach", &run_id]); let (snapshot, _output) = run_and_format(&mut attach_cmd, &filters); release_gate.join().expect("gate releaser should join"); @@ -176,7 +174,6 @@ fn attach_json_errors_without_prompting_for_human_input() { let run_output = context .command() - .current_dir(&context.temp_dir) .env("OPENAI_API_KEY", "test") .args([ "run", diff --git a/lib/crates/fabro-cli/tests/it/cmd/detached.rs b/lib/crates/fabro-cli/tests/it/cmd/detached.rs index a71bf51b2..e201f5912 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/detached.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/detached.rs @@ -187,7 +187,6 @@ digraph Test { let run = context .command() - .current_dir(&context.temp_dir) .args([ "run", "--dry-run", diff --git a/lib/crates/fabro-cli/tests/it/cmd/exec.rs b/lib/crates/fabro-cli/tests/it/cmd/exec.rs index 67404a174..19146ce82 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/exec.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/exec.rs @@ -75,7 +75,6 @@ fn exec_missing_api_key_exits_with_error() { cmd.arg("test prompt"); cmd.env_clear(); cmd.env("HOME", &context.home_dir); - cmd.current_dir(&context.temp_dir); fabro_snapshot!(context.filters(), cmd, @" success: false exit_code: 1 @@ -99,7 +98,6 @@ fn exec_uses_user_config_defaults() { cmd.env("HOME", &context.home_dir); cmd.env("FABRO_STORAGE_DIR", &context.storage_dir); cmd.env("FABRO_NO_UPGRADE_CHECK", "true"); - cmd.current_dir(&context.temp_dir); fabro_snapshot!(context.filters(), cmd, @" success: false @@ -125,7 +123,6 @@ fn exec_creates_file() { "claude-haiku-4-5", "Create a file called hello.txt containing exactly 'Hello'", ]) - .current_dir(&context.temp_dir) .timeout(std::time::Duration::from_secs(120)) .assert() .success(); @@ -153,7 +150,6 @@ fn exec_shell_command() { "claude-haiku-4-5", "Run the shell command `echo arc_test_marker_42` and tell me what it printed", ]) - .current_dir(&context.temp_dir) .timeout(std::time::Duration::from_secs(120)) .assert() .success(); @@ -174,7 +170,6 @@ fn exec_read_only_blocks_write() { "claude-haiku-4-5", "Create a file called forbidden.txt containing 'should not exist'", ]) - .current_dir(&context.temp_dir) .timeout(std::time::Duration::from_secs(120)) .assert() .success(); @@ -201,7 +196,6 @@ fn exec_json_output_format() { "claude-haiku-4-5", "Create a file called test.txt containing 'test'", ]) - .current_dir(&context.temp_dir) .timeout(std::time::Duration::from_secs(120)) .assert() .success() @@ -237,7 +231,6 @@ fn exec_read_and_edit() { "claude-haiku-4-5", "Read data.txt then replace its entire content with 'new content'", ]) - .current_dir(&context.temp_dir) .timeout(std::time::Duration::from_secs(120)) .assert() .success(); diff --git a/lib/crates/fabro-cli/tests/it/cmd/fork.rs b/lib/crates/fabro-cli/tests/it/cmd/fork.rs index 4947aab15..989492c6d 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/fork.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/fork.rs @@ -42,7 +42,6 @@ fn help() { fn fork_outside_git_repo_errors() { let context = test_context!(); let mut cmd = context.command(); - cmd.current_dir(&context.temp_dir); cmd.args(["fork", "01ARZ3NDEKTSV4RRFFQ69G5FAW"]); fabro_snapshot!(context.filters(), cmd, @" diff --git a/lib/crates/fabro-cli/tests/it/cmd/parse.rs b/lib/crates/fabro-cli/tests/it/cmd/parse.rs index 5fcfdf7fa..ea720e917 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/parse.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/parse.rs @@ -36,7 +36,6 @@ fn parse_valid_workflow_prints_ast_json() { "digraph Tiny {\n graph [goal=\"Parse a tiny workflow\"]\n start [shape=Mdiamond]\n exit [shape=Msquare]\n main [label=\"Main\", prompt=\"Do the thing\"]\n start -> main -> exit\n}\n", ); let mut cmd = context.command(); - cmd.current_dir(&context.temp_dir); cmd.args(["parse", "tiny.fabro"]); fabro_snapshot!(context.filters(), cmd, @r###" @@ -125,7 +124,6 @@ fn parse_invalid_dot_fails_cleanly() { "digraph Bad {\n start [shape=Mdiamond]\n exit [shape=Msquare]\n start -> exit\n", ); let mut cmd = context.command(); - cmd.current_dir(&context.temp_dir); cmd.args(["parse", "bad.fabro"]); fabro_snapshot!(context.filters(), cmd, @" diff --git a/lib/crates/fabro-cli/tests/it/cmd/preflight.rs b/lib/crates/fabro-cli/tests/it/cmd/preflight.rs index a3878f3c9..01a07f9ce 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/preflight.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/preflight.rs @@ -49,7 +49,7 @@ fn preflight_invalid_workflow_fails_with_validation_output() { ----- stdout ----- ----- stderr ----- Workflow: Invalid (2 nodes, 1 edges) - Graph: ../../../test/invalid.fabro + Graph: [FIXTURES]/invalid.fabro error: Pipeline must have exactly one start node (shape=Mdiamond or id start/Start) (start_node) error [node: exit]: Exit node 'exit' has 1 outgoing edge(s) but must have none (exit_no_outgoing) error: Validation failed diff --git a/lib/crates/fabro-cli/tests/it/cmd/repo.rs b/lib/crates/fabro-cli/tests/it/cmd/repo.rs index 8ab8a2625..1f0074fed 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/repo.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/repo.rs @@ -46,12 +46,7 @@ fn test_repo_deinit_removes_fabro_toml_and_dir() { assert!(context.temp_dir.join("fabro.toml").exists()); assert!(context.temp_dir.join("fabro").exists()); - context - .repo() - .arg("deinit") - .current_dir(&context.temp_dir) - .assert() - .success(); + context.repo().arg("deinit").assert().success(); assert!( !context.temp_dir.join("fabro.toml").exists(), @@ -70,7 +65,6 @@ fn test_repo_deinit_fails_when_not_initialized() { let mut cmd = context.repo(); cmd.arg("deinit"); - cmd.current_dir(&context.temp_dir); fabro_snapshot!(context.filters(), cmd, @" success: false exit_code: 1 @@ -85,12 +79,7 @@ fn test_repo_init_skill_installs_skill_files() { let context = test_context!(); context.git_init(); - context - .repo() - .args(["init", "--skill"]) - .current_dir(&context.temp_dir) - .assert() - .success(); + context.repo().args(["init", "--skill"]).assert().success(); // Skill files should be installed under .claude/skills/fabro-create-workflow/ let skill_dir = context diff --git a/lib/crates/fabro-cli/tests/it/cmd/repo_init.rs b/lib/crates/fabro-cli/tests/it/cmd/repo_init.rs index 789e6dd13..d6110dbea 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/repo_init.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/repo_init.rs @@ -33,7 +33,6 @@ fn repo_init_creates_fabro_toml_and_hello_workflow() { context.git_init(); let mut cmd = context.command(); - cmd.current_dir(&context.temp_dir); cmd.args(["repo", "init"]); fabro_snapshot!(context.filters(), cmd, @" @@ -111,7 +110,6 @@ fn repo_init_rejects_already_initialized_repo() { std::fs::write(context.temp_dir.join("fabro.toml"), "version = 1\n").unwrap(); let mut cmd = context.command(); - cmd.current_dir(&context.temp_dir); cmd.args(["repo", "init"]); fabro_snapshot!(context.filters(), cmd, @" @@ -127,7 +125,6 @@ fn repo_init_rejects_already_initialized_repo() { fn repo_init_errors_outside_git_repo() { let context = test_context!(); let mut cmd = context.command(); - cmd.current_dir(&context.temp_dir); cmd.args(["repo", "init"]); fabro_snapshot!(context.filters(), cmd, @" diff --git a/lib/crates/fabro-cli/tests/it/cmd/rewind.rs b/lib/crates/fabro-cli/tests/it/cmd/rewind.rs index db15f0cfe..2e1f186c3 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/rewind.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/rewind.rs @@ -42,7 +42,6 @@ fn help() { fn rewind_outside_git_repo_errors() { let context = test_context!(); let mut cmd = context.command(); - cmd.current_dir(&context.temp_dir); cmd.args(["rewind", "01ARZ3NDEKTSV4RRFFQ69G5FAW", "--list"]); fabro_snapshot!(context.filters(), cmd, @" diff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/crates/fabro-cli/tests/it/cmd/run.rs index acd2f6662..60fb64374 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/run.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/run.rs @@ -56,7 +56,7 @@ fn dry_run_simple() { ----- stdout ----- ----- stderr ----- Workflow: Simple (4 nodes, 3 edges) - Graph: ../../../test/simple.fabro + Graph: [FIXTURES]/simple.fabro Goal: Run tests and report results Sandbox: local (ready in [TIME]) @@ -80,11 +80,9 @@ fn dry_run_simple() { fn dry_run_writes_jsonl_and_live_json() { let context = test_context!(); let run_id = "01ARZ3NDEKTSV4RRFFQ69G5FB8"; - let workflow = example_fixture("simple.fabro"); context .command() - .current_dir(&context.temp_dir) .args([ "run", "--dry-run", @@ -93,7 +91,7 @@ fn dry_run_writes_jsonl_and_live_json() { "local", "--run-id", run_id, - workflow.to_str().unwrap(), + example_fixture("simple.fabro").to_str().unwrap(), ]) .assert() .success(); @@ -427,7 +425,7 @@ fn run_id_passthrough_uses_provided_ulid() { "--auto-approve", "--run-id", run_id, - "../../../test/simple.fabro", + example_fixture("simple.fabro").to_str().unwrap(), ]) .assert() .success(); @@ -461,7 +459,6 @@ fn json_run_implies_auto_approve_for_human_gates() { let output = context .command() - .current_dir(&context.temp_dir) .args([ "--json", "run", @@ -811,7 +808,7 @@ fn detach_prints_ulid_and_exits() { "--detach", "--dry-run", "--auto-approve", - "../../../test/simple.fabro", + example_fixture("simple.fabro").to_str().unwrap(), ]); fabro_snapshot!(context.filters(), cmd, @" success: true @@ -835,7 +832,7 @@ fn detach_creates_run_dir_with_detach_log() { "--auto-approve", "--run-id", run_id, - "../../../test/simple.fabro", + example_fixture("simple.fabro").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 7f7a85503..f0e967068 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/start.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/start.rs @@ -163,7 +163,6 @@ fn start_rejects_already_active_or_completed_run() { let gate = write_gated_workflow(&context.temp_dir.join("slow.fabro"), "slow", "Run slowly"); let mut create_cmd = context.command(); - create_cmd.current_dir(&context.temp_dir); create_cmd.env("OPENAI_API_KEY", "test"); create_cmd.args([ "create", @@ -185,7 +184,6 @@ fn start_rejects_already_active_or_completed_run() { let run = resolve_run(&context, &run_id); let mut start_cmd = context.command(); - start_cmd.current_dir(&context.temp_dir); start_cmd.env("OPENAI_API_KEY", "test"); start_cmd.args(["start", &run_id]); start_cmd.assert().success(); @@ -193,7 +191,6 @@ fn start_rejects_already_active_or_completed_run() { wait_for_status(&run.run_dir, &["running"]); let mut active_cmd = context.command(); - active_cmd.current_dir(&context.temp_dir); active_cmd.args(["start", &run_id]); fabro_snapshot!(context.filters(), active_cmd, @" success: false @@ -207,7 +204,6 @@ fn start_rejects_already_active_or_completed_run() { wait_for_status(&run.run_dir, &["succeeded"]); let mut completed_cmd = context.command(); - completed_cmd.current_dir(&context.temp_dir); completed_cmd.args(["start", &run_id]); fabro_snapshot!(context.filters(), completed_cmd, @" success: false diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs index fad14c496..0a6d5ab36 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/support.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs @@ -41,7 +41,10 @@ enum GitWorkflowKind { } pub(crate) fn fixture(name: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../../test/{name}")) + Path::new(env!("CARGO_MANIFEST_DIR")) + .join(format!("../../../test/{name}")) + .canonicalize() + .expect("fixture path should exist") } pub(crate) fn output_stderr(output: &Output) -> String { diff --git a/lib/crates/fabro-cli/tests/it/cmd/validate.rs b/lib/crates/fabro-cli/tests/it/cmd/validate.rs index f11e18d8d..1b2ae519c 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/validate.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/validate.rs @@ -1,7 +1,10 @@ use fabro_test::{fabro_snapshot, test_context}; fn fixture(name: &str) -> std::path::PathBuf { - std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../../test/{name}")) + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join(format!("../../../test/{name}")) + .canonicalize() + .expect("fixture path should exist") } #[test] @@ -43,7 +46,7 @@ fn simple() { ----- stdout ----- ----- stderr ----- Workflow: Simple (4 nodes, 3 edges) - Graph: ../../../test/simple.fabro + Graph: [FIXTURES]/simple.fabro Validation: OK "); } @@ -59,7 +62,7 @@ fn branching() { ----- stdout ----- ----- stderr ----- Workflow: Branch (6 nodes, 6 edges) - Graph: ../../../test/branching.fabro + Graph: [FIXTURES]/branching.fabro warning [node: implement]: Node 'implement' has goal_gate=true but no retry_target or fallback_retry_target (goal_gate_has_retry) Validation: OK "); @@ -76,7 +79,7 @@ fn conditions() { ----- stdout ----- ----- stderr ----- Workflow: Conditions (5 nodes, 5 edges) - Graph: ../../../test/conditions.fabro + Graph: [FIXTURES]/conditions.fabro Validation: OK "); } @@ -92,7 +95,7 @@ fn parallel() { ----- stdout ----- ----- stderr ----- Workflow: Parallel (7 nodes, 7 edges) - Graph: ../../../test/parallel.fabro + Graph: [FIXTURES]/parallel.fabro Validation: OK "); } @@ -108,7 +111,7 @@ fn styled() { ----- stdout ----- ----- stderr ----- Workflow: Styled (5 nodes, 4 edges) - Graph: ../../../test/styled.fabro + Graph: [FIXTURES]/styled.fabro Validation: OK "); } @@ -124,7 +127,7 @@ fn legacy_tool() { ----- stdout ----- ----- stderr ----- Workflow: LegacyTool (3 nodes, 2 edges) - Graph: ../../../test/legacy_tool.fabro + Graph: [FIXTURES]/legacy_tool.fabro Validation: OK "); } @@ -140,7 +143,7 @@ fn invalid() { ----- stdout ----- ----- stderr ----- Workflow: Invalid (2 nodes, 1 edges) - Graph: ../../../test/invalid.fabro + Graph: [FIXTURES]/invalid.fabro error: Pipeline must have exactly one start node (shape=Mdiamond or id start/Start) (start_node) error [node: exit]: Exit node 'exit' has 1 outgoing edge(s) but must have none (exit_no_outgoing) error: Validation failed diff --git a/lib/crates/fabro-cli/tests/it/cmd/workflow.rs b/lib/crates/fabro-cli/tests/it/cmd/workflow.rs index 3199e1a7a..bf1cd1a05 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/workflow.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/workflow.rs @@ -43,7 +43,6 @@ fn list() { let mut cmd = context.command(); cmd.args(["workflow", "list"]); - cmd.current_dir(&context.temp_dir); fabro_snapshot!(context.filters(), cmd, @" success: true exit_code: 0 diff --git a/lib/crates/fabro-cli/tests/it/cmd/workflow_create.rs b/lib/crates/fabro-cli/tests/it/cmd/workflow_create.rs index 2a494ebfc..9857b4cd9 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/workflow_create.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/workflow_create.rs @@ -147,7 +147,6 @@ fn workflow_create_rejects_existing_workflow() { fn workflow_create_errors_without_project_config() { let context = test_context!(); let mut cmd = context.command(); - cmd.current_dir(&context.temp_dir); cmd.args(["workflow", "create", "hello-world"]); fabro_snapshot!(context.filters(), cmd, @" diff --git a/lib/crates/fabro-cli/tests/it/cmd/workflow_list.rs b/lib/crates/fabro-cli/tests/it/cmd/workflow_list.rs index 8edeb73b6..00f4f84d0 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/workflow_list.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/workflow_list.rs @@ -31,7 +31,6 @@ fn help() { fn workflow_list_errors_without_project_config() { let context = test_context!(); let mut cmd = context.command(); - cmd.current_dir(&context.temp_dir); cmd.args(["workflow", "list"]); fabro_snapshot!(context.filters(), cmd, @" diff --git a/lib/crates/fabro-cli/tests/it/scenario/exec.rs b/lib/crates/fabro-cli/tests/it/scenario/exec.rs index d12e43ae0..fba7ca76c 100644 --- a/lib/crates/fabro-cli/tests/it/scenario/exec.rs +++ b/lib/crates/fabro-cli/tests/it/scenario/exec.rs @@ -17,7 +17,6 @@ fn test_exec_creates_file() { "claude-haiku-4-5", "Create a file called hello.txt containing exactly 'Hello from exec scenario'", ]); - cmd.current_dir(&context.temp_dir); cmd.timeout(Duration::from_secs(120)); cmd.assert().success(); diff --git a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs index 17a11aac6..f00e8c41f 100644 --- a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs +++ b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs @@ -2,7 +2,7 @@ use fabro_test::test_context; use serde_json::Value; use super::{fixture, read_json, timeout_for}; -use crate::support::fabro_json_snapshot; +use crate::support::{example_fixture, fabro_json_snapshot}; #[fabro_macros::e2e_test()] fn local_run_lifecycle() { @@ -112,7 +112,7 @@ fn dry_run_create_start_attach_works_with_default_run_lookup() { "--auto-approve", "--run-id", run_id, - "../../../test/simple.fabro", + example_fixture("simple.fabro").to_str().unwrap(), ]) .assert() .success(); @@ -155,7 +155,7 @@ fn dry_run_detach_attach_works_with_default_run_lookup() { "--auto-approve", "--run-id", run_id, - "../../../test/simple.fabro", + example_fixture("simple.fabro").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 797759ba0..067afb449 100644 --- a/lib/crates/fabro-cli/tests/it/support/mod.rs +++ b/lib/crates/fabro-cli/tests/it/support/mod.rs @@ -32,7 +32,10 @@ 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}")) + Path::new(env!("CARGO_MANIFEST_DIR")) + .join(format!("../../../test/{name}")) + .canonicalize() + .expect("fixture path should exist") } pub(crate) fn read_json(path: impl AsRef) -> Value { 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 d3fa94488..98b0ac3e1 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 @@ -14,7 +14,7 @@ fn dry_run_branching() { ----- stdout ----- ----- stderr ----- Workflow: Branch (6 nodes, 6 edges) - Graph: ../../../test/branching.fabro + Graph: [FIXTURES]/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) @@ -49,7 +49,7 @@ fn dry_run_conditions() { ----- stdout ----- ----- stderr ----- Workflow: Conditions (5 nodes, 5 edges) - Graph: ../../../test/conditions.fabro + Graph: [FIXTURES]/conditions.fabro Goal: Test condition evaluation with OR and parentheses Sandbox: local (ready in [TIME]) @@ -81,7 +81,7 @@ fn dry_run_parallel() { ----- stdout ----- ----- stderr ----- Workflow: Parallel (7 nodes, 7 edges) - Graph: ../../../test/parallel.fabro + Graph: [FIXTURES]/parallel.fabro Goal: Test parallel and fan-in execution Sandbox: local (ready in [TIME]) @@ -114,7 +114,7 @@ fn dry_run_styled() { ----- stdout ----- ----- stderr ----- Workflow: Styled (5 nodes, 4 edges) - Graph: ../../../test/styled.fabro + Graph: [FIXTURES]/styled.fabro Goal: Build a styled pipeline Sandbox: local (ready in [TIME]) @@ -147,7 +147,7 @@ fn dry_run_legacy_tool() { ----- stdout ----- ----- stderr ----- Workflow: LegacyTool (3 nodes, 2 edges) - Graph: ../../../test/legacy_tool.fabro + Graph: [FIXTURES]/legacy_tool.fabro Goal: Verify backwards compatibility with old tool naming Sandbox: local (ready in [TIME]) diff --git a/lib/crates/fabro-test/src/lib.rs b/lib/crates/fabro-test/src/lib.rs index 7656f253d..f2429198e 100644 --- a/lib/crates/fabro-test/src/lib.rs +++ b/lib/crates/fabro-test/src/lib.rs @@ -1,9 +1,21 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Output; use assert_cmd::Command; use regex::Regex; +/// Walk up from `start` to find the repo-level `test/` fixtures directory. +pub fn find_test_fixtures_dir(start: &Path) -> Option { + let mut dir = start; + loop { + let candidate = dir.join("test"); + if candidate.is_dir() { + return candidate.canonicalize().ok(); + } + dir = dir.parent()?; + } +} + /// Static filters applied to every snapshot. static INSTA_FILTERS: &[(&str, &str)] = &[ (r"fabro \d+\.\d+\.\d+", "fabro [VERSION]"), @@ -127,6 +139,12 @@ impl TestContext { } } + /// Register a custom filter (regex pattern → replacement). + pub fn add_filter(&mut self, pattern: &str, replacement: &str) { + self.filters + .push((regex::escape(pattern), replacement.to_string())); + } + /// Returns the combined static + context-specific filters. pub fn filters(&self) -> Vec<(String, String)> { let mut filters = self.filters.clone(); @@ -139,8 +157,14 @@ impl TestContext { } /// Build a base `Command` with all isolation env vars set. + /// + /// The working directory defaults to `self.temp_dir` (a non-git temp + /// directory) so tests never accidentally interact with the real repo. + /// Tests that need a specific working directory can override this with + /// a subsequent `.current_dir(path)` call. pub fn command(&self) -> Command { let mut cmd = Command::new(&self.fabro_bin); + cmd.current_dir(&self.temp_dir); cmd.env("NO_COLOR", "1"); cmd.env("HOME", &self.home_dir); cmd.env("FABRO_NO_UPGRADE_CHECK", "true"); @@ -399,11 +423,21 @@ pub fn apply_filters(snapshot: &str, filters: &[(String, String)]) -> String { } /// Create a `TestContext` using the `fabro` binary built by cargo. +/// +/// Automatically registers a `[FIXTURES]` snapshot filter for the `test/` +/// directory at the repository root (found by walking up from `CARGO_MANIFEST_DIR`). #[macro_export] macro_rules! test_context { - () => { - $crate::TestContext::new(std::path::PathBuf::from(env!("CARGO_BIN_EXE_fabro"))) - }; + () => {{ + let mut ctx = + $crate::TestContext::new(std::path::PathBuf::from(env!("CARGO_BIN_EXE_fabro"))); + if let Some(fixtures_dir) = + $crate::find_test_fixtures_dir(std::path::Path::new(env!("CARGO_MANIFEST_DIR"))) + { + ctx.add_filter(fixtures_dir.to_str().unwrap(), "[FIXTURES]"); + } + ctx + }}; } /// Snapshot test macro that runs a command and compares output using insta.