From f79e7316f7795de4d1c4c5f456627dc670cb880b Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 5 Apr 2026 11:26:32 -0400 Subject: [PATCH] test(cli): consolidate artifact integration coverage --- .../fabro-cli/tests/it/cmd/artifact_cp.rs | 100 +--------- .../fabro-cli/tests/it/cmd/artifact_list.rs | 104 +---------- .../fabro-cli/tests/it/scenario/artifacts.rs | 176 ++++++++++++++++++ lib/crates/fabro-cli/tests/it/scenario/mod.rs | 1 + 4 files changed, 179 insertions(+), 202 deletions(-) create mode 100644 lib/crates/fabro-cli/tests/it/scenario/artifacts.rs diff --git a/lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs b/lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs index 987b31987..426ca3ac2 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/artifact_cp.rs @@ -1,6 +1,6 @@ use fabro_test::{fabro_snapshot, test_context}; -use super::support::{read_text, setup_artifact_run, setup_completed_fast_dry_run, text_tree}; +use super::support::setup_completed_fast_dry_run; #[test] fn help() { @@ -51,101 +51,3 @@ fn artifact_cp_empty_run_reports_no_artifacts() { error: No artifacts found for this run "); } - -#[test] -fn artifact_cp_specific_path_copies_single_asset() { - let context = test_context!(); - let setup = setup_artifact_run(&context); - let dest = context.temp_dir.join("artifact-one"); - let mut cmd = context.command(); - cmd.args([ - "artifact", - "cp", - &format!("{}:assets/shared/report.txt", setup.run.run_id), - dest.to_str().unwrap(), - "--node", - "create_assets", - ]); - - fabro_snapshot!(context.filters(), cmd, @" - success: true - exit_code: 0 - ----- stdout ----- - Copied assets/shared/report.txt to [TEMP_DIR]/artifact-one/report.txt - ----- stderr ----- - "); - assert_eq!(read_text(&dest.join("report.txt")), "one"); -} - -#[test] -fn artifact_cp_ambiguous_path_requires_node_or_retry() { - let context = test_context!(); - let setup = setup_artifact_run(&context); - let dest = context.temp_dir.join("artifact-one"); - let mut cmd = context.command(); - cmd.args([ - "artifact", - "cp", - &format!("{}:assets/retry/report.txt", setup.run.run_id), - dest.to_str().unwrap(), - ]); - - fabro_snapshot!(context.filters(), cmd, @" - success: false - exit_code: 1 - ----- stdout ----- - ----- stderr ----- - error: Path 'assets/retry/report.txt' matches multiple artifacts: create_colliding:retry_1, retry_assets:retry_1, retry_assets:retry_2. Use --node and/or --retry to disambiguate. - "); -} - -#[test] -fn artifact_cp_tree_preserves_structure() { - let context = test_context!(); - let setup = setup_artifact_run(&context); - let dest = context.temp_dir.join("artifact-tree"); - let mut cmd = context.command(); - cmd.args([ - "artifact", - "cp", - &setup.run.run_id, - dest.to_str().unwrap(), - "--tree", - ]); - - fabro_snapshot!(context.filters(), cmd, @" - success: true - exit_code: 0 - ----- stdout ----- - Copied 6 artifact(s) to [TEMP_DIR]/artifact-tree - ----- stderr ----- - "); - insta::assert_snapshot!( - text_tree(&dest).join("\n"), - @r" - create_assets/retry_1/assets/node_a/summary.txt = alpha - create_assets/retry_1/assets/shared/report.txt = one - create_colliding/retry_1/assets/other/summary.txt = beta - create_colliding/retry_1/assets/retry/report.txt = second - retry_assets/retry_1/assets/retry/report.txt = first - retry_assets/retry_2/assets/retry/report.txt = second - " - ); -} - -#[test] -fn artifact_cp_flat_mode_rejects_filename_collisions() { - let context = test_context!(); - let setup = setup_artifact_run(&context); - let dest = context.temp_dir.join("artifact-flat"); - let mut cmd = context.command(); - cmd.args(["artifact", "cp", &setup.run.run_id, dest.to_str().unwrap()]); - - fabro_snapshot!(context.filters(), cmd, @" - success: false - exit_code: 1 - ----- stdout ----- - ----- stderr ----- - error: Filename collision: 'summary.txt' exists in both create_assets:retry_1 and create_colliding:retry_1. Use --tree to preserve directory structure, or --node and/or --retry to filter. - "); -} diff --git a/lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs b/lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs index 3a87a6eab..db0e58b95 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/artifact_list.rs @@ -1,6 +1,6 @@ use fabro_test::{fabro_snapshot, test_context}; -use super::support::{setup_artifact_run, setup_completed_fast_dry_run}; +use super::support::setup_completed_fast_dry_run; #[test] fn help() { @@ -48,105 +48,3 @@ fn artifact_list_empty_run_reports_no_artifacts() { ----- stderr ----- "); } - -#[test] -fn artifact_list_json_outputs_entries() { - let context = test_context!(); - let setup = setup_artifact_run(&context); - let mut filters = context.filters(); - filters.push(( - r"\[STORAGE_DIR\]/runs/\d{8}-\[ULID\]".to_string(), - "[RUN_DIR]".to_string(), - )); - let mut cmd = context.command(); - cmd.args(["artifact", "list", &setup.run.run_id, "--json"]); - - fabro_snapshot!(filters, cmd, @r#" - success: true - exit_code: 0 - ----- stdout ----- - [ - { - "node_slug": "create_assets", - "retry": 1, - "relative_path": "assets/node_a/summary.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_assets/retry_1/assets/node_a/summary.txt", - "size": 5 - }, - { - "node_slug": "create_assets", - "retry": 1, - "relative_path": "assets/shared/report.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_assets/retry_1/assets/shared/report.txt", - "size": 3 - }, - { - "node_slug": "create_colliding", - "retry": 1, - "relative_path": "assets/other/summary.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_colliding/retry_1/assets/other/summary.txt", - "size": 4 - }, - { - "node_slug": "create_colliding", - "retry": 1, - "relative_path": "assets/retry/report.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_colliding/retry_1/assets/retry/report.txt", - "size": 6 - }, - { - "node_slug": "retry_assets", - "retry": 1, - "relative_path": "assets/retry/report.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/files/retry_assets/retry_1/assets/retry/report.txt", - "size": 5 - }, - { - "node_slug": "retry_assets", - "retry": 2, - "relative_path": "assets/retry/report.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/files/retry_assets/retry_2/assets/retry/report.txt", - "size": 6 - } - ] - ----- stderr ----- - "#); -} - -#[test] -fn artifact_list_filters_by_node_and_retry() { - let context = test_context!(); - let setup = setup_artifact_run(&context); - let mut filters = context.filters(); - filters.push(( - r"\[STORAGE_DIR\]/runs/\d{8}-\[ULID\]".to_string(), - "[RUN_DIR]".to_string(), - )); - let mut cmd = context.command(); - cmd.args([ - "artifact", - "list", - &setup.run.run_id, - "--node", - "retry_assets", - "--retry", - "2", - "--json", - ]); - - fabro_snapshot!(filters, cmd, @r#" - success: true - exit_code: 0 - ----- stdout ----- - [ - { - "node_slug": "retry_assets", - "retry": 2, - "relative_path": "assets/retry/report.txt", - "absolute_path": "[RUN_DIR]/cache/artifacts/files/retry_assets/retry_2/assets/retry/report.txt", - "size": 6 - } - ] - ----- stderr ----- - "#); -} diff --git a/lib/crates/fabro-cli/tests/it/scenario/artifacts.rs b/lib/crates/fabro-cli/tests/it/scenario/artifacts.rs new file mode 100644 index 000000000..ca62560e5 --- /dev/null +++ b/lib/crates/fabro-cli/tests/it/scenario/artifacts.rs @@ -0,0 +1,176 @@ +use std::time::Duration; + +use fabro_test::{fabro_snapshot, test_context}; + +use crate::cmd::support::{read_text, setup_artifact_run, text_tree}; + +fn artifact_filters(context: &fabro_test::TestContext) -> Vec<(String, String)> { + let mut filters = context.filters(); + filters.push(( + r"\[STORAGE_DIR\]/runs/\d{8}-\[ULID\]".to_string(), + "[RUN_DIR]".to_string(), + )); + filters +} + +#[test] +fn artifact_commands_share_populated_run_fixture() { + let context = test_context!(); + let setup = setup_artifact_run(&context); + let filters = artifact_filters(&context); + + let mut list_json = context.command(); + list_json.args(["artifact", "list", &setup.run.run_id, "--json"]); + fabro_snapshot!(filters.clone(), list_json, @r#" + success: true + exit_code: 0 + ----- stdout ----- + [ + { + "node_slug": "create_assets", + "retry": 1, + "relative_path": "assets/node_a/summary.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_assets/retry_1/assets/node_a/summary.txt", + "size": 5 + }, + { + "node_slug": "create_assets", + "retry": 1, + "relative_path": "assets/shared/report.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_assets/retry_1/assets/shared/report.txt", + "size": 3 + }, + { + "node_slug": "create_colliding", + "retry": 1, + "relative_path": "assets/other/summary.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_colliding/retry_1/assets/other/summary.txt", + "size": 4 + }, + { + "node_slug": "create_colliding", + "retry": 1, + "relative_path": "assets/retry/report.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/create_colliding/retry_1/assets/retry/report.txt", + "size": 6 + }, + { + "node_slug": "retry_assets", + "retry": 1, + "relative_path": "assets/retry/report.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/retry_assets/retry_1/assets/retry/report.txt", + "size": 5 + }, + { + "node_slug": "retry_assets", + "retry": 2, + "relative_path": "assets/retry/report.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/retry_assets/retry_2/assets/retry/report.txt", + "size": 6 + } + ] + ----- stderr ----- + "#); + + let mut list_filtered = context.command(); + list_filtered.args([ + "artifact", + "list", + &setup.run.run_id, + "--node", + "retry_assets", + "--retry", + "2", + "--json", + ]); + fabro_snapshot!(filters.clone(), list_filtered, @r#" + success: true + exit_code: 0 + ----- stdout ----- + [ + { + "node_slug": "retry_assets", + "retry": 2, + "relative_path": "assets/retry/report.txt", + "absolute_path": "[RUN_DIR]/cache/artifacts/files/retry_assets/retry_2/assets/retry/report.txt", + "size": 6 + } + ] + ----- stderr ----- + "#); + + let single_dest = context.temp_dir.join("artifact-one"); + let mut cp_single = context.command(); + cp_single.args([ + "artifact", + "cp", + &format!("{}:assets/shared/report.txt", setup.run.run_id), + single_dest.to_str().unwrap(), + "--node", + "create_assets", + ]); + fabro_snapshot!(context.filters(), cp_single, @" + success: true + exit_code: 0 + ----- stdout ----- + Copied assets/shared/report.txt to [TEMP_DIR]/artifact-one/report.txt + ----- stderr ----- + "); + assert_eq!(read_text(&single_dest.join("report.txt")), "one"); + + let tree_dest = context.temp_dir.join("artifact-tree"); + let mut cp_tree = context.command(); + cp_tree.args([ + "artifact", + "cp", + &setup.run.run_id, + tree_dest.to_str().unwrap(), + "--tree", + ]); + cp_tree.timeout(Duration::from_secs(30)); + fabro_snapshot!(context.filters(), cp_tree, @" + success: true + exit_code: 0 + ----- stdout ----- + Copied 6 artifact(s) to [TEMP_DIR]/artifact-tree + ----- stderr ----- + "); + insta::assert_snapshot!( + text_tree(&tree_dest).join("\n"), + @r" + create_assets/retry_1/assets/node_a/summary.txt = alpha + create_assets/retry_1/assets/shared/report.txt = one + create_colliding/retry_1/assets/other/summary.txt = beta + create_colliding/retry_1/assets/retry/report.txt = second + retry_assets/retry_1/assets/retry/report.txt = first + retry_assets/retry_2/assets/retry/report.txt = second + " + ); + + let ambiguous_dest = context.temp_dir.join("artifact-ambiguous"); + let mut cp_ambiguous = context.command(); + cp_ambiguous.args([ + "artifact", + "cp", + &format!("{}:assets/retry/report.txt", setup.run.run_id), + ambiguous_dest.to_str().unwrap(), + ]); + fabro_snapshot!(context.filters(), cp_ambiguous, @" + success: false + exit_code: 1 + ----- stdout ----- + ----- stderr ----- + error: Path 'assets/retry/report.txt' matches multiple artifacts: create_colliding:retry_1, retry_assets:retry_1, retry_assets:retry_2. Use --node and/or --retry to disambiguate. + "); + + let flat_dest = context.temp_dir.join("artifact-flat"); + let mut cp_flat = context.command(); + cp_flat.args(["artifact", "cp", &setup.run.run_id, flat_dest.to_str().unwrap()]); + fabro_snapshot!(context.filters(), cp_flat, @" + success: false + exit_code: 1 + ----- stdout ----- + ----- stderr ----- + error: Filename collision: 'summary.txt' exists in both create_assets:retry_1 and create_colliding:retry_1. Use --tree to preserve directory structure, or --node and/or --retry to filter. + "); +} diff --git a/lib/crates/fabro-cli/tests/it/scenario/mod.rs b/lib/crates/fabro-cli/tests/it/scenario/mod.rs index 18973f9c7..2605bab8e 100644 --- a/lib/crates/fabro-cli/tests/it/scenario/mod.rs +++ b/lib/crates/fabro-cli/tests/it/scenario/mod.rs @@ -1,3 +1,4 @@ +mod artifacts; mod exec; mod lifecycle; mod recovery;