diff --git a/src/presentation/cli/input/cli/args.rs b/src/presentation/cli/input/cli/args.rs index 65c2c467..9702f8a7 100644 --- a/src/presentation/cli/input/cli/args.rs +++ b/src/presentation/cli/input/cli/args.rs @@ -72,20 +72,22 @@ pub struct GlobalArgs { #[arg(long, default_value = ".", global = true)] pub working_dir: PathBuf, - /// Output format for command results (default: text) + /// Output format for command results (default: json) /// - /// Controls the format of user-facing output (stdout channel). - /// - text: Human-readable formatted output with tables and sections (default) - /// - json: Machine-readable JSON for automation, scripts, and AI agents + /// Controls the format of result data written to stdout. Progress messages, + /// warnings, and status updates are always written to stderr regardless of + /// this setting. + /// - json: Machine-readable JSON for automation, scripts, and AI agents (default) + /// - text: Human-readable formatted output with tables and sections /// /// This is independent of logging format (--log-file-format, --log-stderr-format) /// which controls stderr/file output. /// /// Examples: - /// - Default: Text format for human consumption - /// - Automation: JSON format for programmatic parsing + /// - Default: JSON format for automation and AI agents + /// - Human output: Pass --output-format text for human-readable display /// - CI/CD: JSON piped to jq for field extraction - #[arg(long, value_enum, default_value = "text", global = true)] + #[arg(long, value_enum, default_value = "json", global = true)] pub output_format: OutputFormat, /// Increase verbosity of user-facing output diff --git a/src/presentation/cli/input/cli/output_format.rs b/src/presentation/cli/input/cli/output_format.rs index 92b0757b..a2f3fd6c 100644 --- a/src/presentation/cli/input/cli/output_format.rs +++ b/src/presentation/cli/input/cli/output_format.rs @@ -14,16 +14,16 @@ /// ```rust /// use torrust_tracker_deployer_lib::presentation::cli::input::cli::OutputFormat; /// -/// // Default is text format +/// // Default is JSON format /// let format = OutputFormat::default(); -/// assert!(matches!(format, OutputFormat::Text)); +/// assert!(matches!(format, OutputFormat::Json)); /// -/// // JSON format for automation -/// let json_format = OutputFormat::Json; +/// // Text format for human-readable output +/// let text_format = OutputFormat::Text; /// ``` #[derive(Clone, Copy, Debug, Default, clap::ValueEnum)] pub enum OutputFormat { - /// Human-readable text output (default) + /// Human-readable text output /// /// Produces formatted text with tables, sections, and visual elements /// optimized for terminal display and human consumption. @@ -38,10 +38,9 @@ pub enum OutputFormat { /// 3. Data directory: ./data/my-env /// 4. Build directory: ./build/my-env /// ``` - #[default] Text, - /// JSON output for automation and programmatic parsing + /// JSON output for automation and programmatic parsing (default) /// /// Produces machine-readable JSON objects that can be parsed by tools /// like jq, scripts, and AI agents for programmatic extraction of data. @@ -56,5 +55,6 @@ pub enum OutputFormat { /// "created_at": "2026-02-16T14:30:00Z" /// } /// ``` + #[default] Json, } diff --git a/src/presentation/cli/views/commands/configure/views/json_view.rs b/src/presentation/cli/views/commands/configure/views/json_view.rs index 4fdbc54f..d0d8a074 100644 --- a/src/presentation/cli/views/commands/configure/views/json_view.rs +++ b/src/presentation/cli/views/commands/configure/views/json_view.rs @@ -92,12 +92,16 @@ impl JsonView { #[must_use] pub fn render(data: &ConfigureDetailsData) -> String { serde_json::to_string_pretty(data).unwrap_or_else(|e| { - format!( - r#"{{ - "error": "Failed to serialize configure details", - "message": "{e}" -}}"# - ) + serde_json::to_string_pretty(&serde_json::json!({ + "error": "Failed to serialize configure details", + "message": e.to_string(), + })) + .unwrap_or_else(|_| { + r#"{ + "error": "Failed to serialize error message" +}"# + .to_string() + }) }) } } diff --git a/src/presentation/cli/views/commands/destroy/views/json_view.rs b/src/presentation/cli/views/commands/destroy/views/json_view.rs index bbbc0a09..a8469162 100644 --- a/src/presentation/cli/views/commands/destroy/views/json_view.rs +++ b/src/presentation/cli/views/commands/destroy/views/json_view.rs @@ -92,12 +92,16 @@ impl JsonView { #[must_use] pub fn render(data: &DestroyDetailsData) -> String { serde_json::to_string_pretty(data).unwrap_or_else(|e| { - format!( - r#"{{ - "error": "Failed to serialize destroy details", - "message": "{e}" -}}"# - ) + serde_json::to_string_pretty(&serde_json::json!({ + "error": "Failed to serialize destroy details", + "message": e.to_string(), + })) + .unwrap_or_else(|_| { + r#"{ + "error": "Failed to serialize error message" +}"# + .to_string() + }) }) } } diff --git a/src/presentation/cli/views/commands/list/views/json_view.rs b/src/presentation/cli/views/commands/list/views/json_view.rs index 337f8022..547d2e58 100644 --- a/src/presentation/cli/views/commands/list/views/json_view.rs +++ b/src/presentation/cli/views/commands/list/views/json_view.rs @@ -96,12 +96,16 @@ impl JsonView { #[must_use] pub fn render(list: &EnvironmentList) -> String { serde_json::to_string_pretty(list).unwrap_or_else(|e| { - format!( - r#"{{ - "error": "Failed to serialize environment list", - "message": "{e}" -}}"# - ) + serde_json::to_string_pretty(&serde_json::json!({ + "error": "Failed to serialize environment list", + "message": e.to_string(), + })) + .unwrap_or_else(|_| { + r#"{ + "error": "Failed to serialize error message" +}"# + .to_string() + }) }) } } diff --git a/src/presentation/cli/views/commands/purge/views/json_view.rs b/src/presentation/cli/views/commands/purge/views/json_view.rs index c28886e1..665f44a4 100644 --- a/src/presentation/cli/views/commands/purge/views/json_view.rs +++ b/src/presentation/cli/views/commands/purge/views/json_view.rs @@ -70,12 +70,16 @@ impl JsonView { #[must_use] pub fn render(data: &PurgeDetailsData) -> String { serde_json::to_string_pretty(data).unwrap_or_else(|e| { - format!( - r#"{{ - "error": "Failed to serialize purge details", - "message": "{e}" -}}"# - ) + serde_json::to_string_pretty(&serde_json::json!({ + "error": "Failed to serialize purge details", + "message": e.to_string(), + })) + .unwrap_or_else(|_| { + r#"{ + "error": "Failed to serialize error message" +}"# + .to_string() + }) }) } } diff --git a/src/presentation/cli/views/commands/register/views/json_view.rs b/src/presentation/cli/views/commands/register/views/json_view.rs index 1d2560e1..e0a17d3f 100644 --- a/src/presentation/cli/views/commands/register/views/json_view.rs +++ b/src/presentation/cli/views/commands/register/views/json_view.rs @@ -87,12 +87,16 @@ impl JsonView { #[must_use] pub fn render(data: &RegisterDetailsData) -> String { serde_json::to_string_pretty(data).unwrap_or_else(|e| { - format!( - r#"{{ - "error": "Failed to serialize register details", - "message": "{e}" -}}"# - ) + serde_json::to_string_pretty(&serde_json::json!({ + "error": "Failed to serialize register details", + "message": e.to_string(), + })) + .unwrap_or_else(|_| { + r#"{ + "error": "Failed to serialize error message" +}"# + .to_string() + }) }) } } diff --git a/src/presentation/cli/views/commands/release/views/json_view.rs b/src/presentation/cli/views/commands/release/views/json_view.rs index 107c188c..c6dcf68a 100644 --- a/src/presentation/cli/views/commands/release/views/json_view.rs +++ b/src/presentation/cli/views/commands/release/views/json_view.rs @@ -92,12 +92,16 @@ impl JsonView { #[must_use] pub fn render(data: &ReleaseDetailsData) -> String { serde_json::to_string_pretty(data).unwrap_or_else(|e| { - format!( - r#"{{ - "error": "Failed to serialize release details", - "message": "{e}" -}}"# - ) + serde_json::to_string_pretty(&serde_json::json!({ + "error": "Failed to serialize release details", + "message": e.to_string(), + })) + .unwrap_or_else(|_| { + r#"{ + "error": "Failed to serialize error message" +}"# + .to_string() + }) }) } } diff --git a/src/presentation/cli/views/commands/render/views/json_view.rs b/src/presentation/cli/views/commands/render/views/json_view.rs index 2ccc38af..db7776e3 100644 --- a/src/presentation/cli/views/commands/render/views/json_view.rs +++ b/src/presentation/cli/views/commands/render/views/json_view.rs @@ -83,12 +83,16 @@ impl JsonView { #[must_use] pub fn render(data: &RenderDetailsData) -> String { serde_json::to_string_pretty(data).unwrap_or_else(|e| { - format!( - r#"{{ - "error": "Failed to serialize render details", - "message": "{e}" -}}"# - ) + serde_json::to_string_pretty(&serde_json::json!({ + "error": "Failed to serialize render details", + "message": e.to_string(), + })) + .unwrap_or_else(|_| { + r#"{ + "error": "Failed to serialize error message" +}"# + .to_string() + }) }) } } diff --git a/src/presentation/cli/views/commands/run/json_view.rs b/src/presentation/cli/views/commands/run/json_view.rs index 77065a48..a370003a 100644 --- a/src/presentation/cli/views/commands/run/json_view.rs +++ b/src/presentation/cli/views/commands/run/json_view.rs @@ -122,8 +122,18 @@ impl JsonView { grafana, }; - serde_json::to_string_pretty(&output) - .unwrap_or_else(|e| format!(r#"{{"error": "Failed to serialize: {e}"}}"#)) + serde_json::to_string_pretty(&output).unwrap_or_else(|e| { + serde_json::to_string_pretty(&serde_json::json!({ + "error": "Failed to serialize run details", + "message": e.to_string(), + })) + .unwrap_or_else(|_| { + r#"{ + "error": "Failed to serialize error message" +}"# + .to_string() + }) + }) } } diff --git a/src/presentation/cli/views/commands/show/views/json_view.rs b/src/presentation/cli/views/commands/show/views/json_view.rs index 256bd0a1..d90e6a88 100644 --- a/src/presentation/cli/views/commands/show/views/json_view.rs +++ b/src/presentation/cli/views/commands/show/views/json_view.rs @@ -90,8 +90,18 @@ impl JsonView { /// ``` #[must_use] pub fn render(info: &EnvironmentInfo) -> String { - serde_json::to_string_pretty(info) - .unwrap_or_else(|e| format!(r#"{{"error": "Failed to serialize: {e}"}}"#)) + serde_json::to_string_pretty(info).unwrap_or_else(|e| { + serde_json::to_string_pretty(&serde_json::json!({ + "error": "Failed to serialize show details", + "message": e.to_string(), + })) + .unwrap_or_else(|_| { + r#"{ + "error": "Failed to serialize error message" +}"# + .to_string() + }) + }) } } diff --git a/src/presentation/cli/views/commands/test/views/json_view.rs b/src/presentation/cli/views/commands/test/views/json_view.rs index 61e308f4..605066e8 100644 --- a/src/presentation/cli/views/commands/test/views/json_view.rs +++ b/src/presentation/cli/views/commands/test/views/json_view.rs @@ -62,12 +62,16 @@ impl JsonView { #[must_use] pub fn render(data: &TestResultData) -> String { serde_json::to_string_pretty(data).unwrap_or_else(|e| { - format!( - r#"{{ - "error": "Failed to serialize test results", - "message": "{e}" -}}"# - ) + serde_json::to_string_pretty(&serde_json::json!({ + "error": "Failed to serialize test results", + "message": e.to_string(), + })) + .unwrap_or_else(|_| { + r#"{ + "error": "Failed to serialize error message" +}"# + .to_string() + }) }) } } diff --git a/src/presentation/cli/views/commands/validate/views/json_view.rs b/src/presentation/cli/views/commands/validate/views/json_view.rs index 8c4650a2..f76b4dc9 100644 --- a/src/presentation/cli/views/commands/validate/views/json_view.rs +++ b/src/presentation/cli/views/commands/validate/views/json_view.rs @@ -95,12 +95,16 @@ impl JsonView { #[must_use] pub fn render(data: &ValidateDetailsData) -> String { serde_json::to_string_pretty(data).unwrap_or_else(|e| { - format!( - r#"{{ - "error": "Failed to serialize validate details", - "message": "{e}" -}}"# - ) + serde_json::to_string_pretty(&serde_json::json!({ + "error": "Failed to serialize validate details", + "message": e.to_string(), + })) + .unwrap_or_else(|_| { + r#"{ + "error": "Failed to serialize error message" +}"# + .to_string() + }) }) } } diff --git a/tests/e2e/create_command.rs b/tests/e2e/create_command.rs index d5827116..aba53651 100644 --- a/tests/e2e/create_command.rs +++ b/tests/e2e/create_command.rs @@ -197,3 +197,42 @@ fn it_should_fail_when_environment_already_exists() { "Error message should mention environment already exists, got: {stderr}" ); } + +#[test] +fn it_should_produce_json_by_default() { + // Verify dependencies before running tests + verify_required_dependencies().expect("Dependency verification failed"); + + // Arrange: Create a valid environment config file in a temp workspace + let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace"); + let config = create_test_environment_config("test-create-json-default"); + temp_workspace + .write_config_file("environment.json", &config) + .expect("Failed to write config file"); + let config_path = temp_workspace.path().join("environment.json"); + + // Act: Run create command without --output-format + let result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_create_command(config_path.to_str().unwrap()) + .expect("Failed to run create command"); + + // Assert: Command succeeds + assert!( + result.success(), + "Create command should succeed with a valid config, stderr: {}", + result.stderr() + ); + + // Assert: stdout is valid JSON + let stdout = result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Create command default output must be valid JSON"); + + // Assert: Expected field is present + assert!( + json.get("environment_name").is_some(), + "Expected `environment_name` field in create JSON output, got: {stdout}" + ); +} diff --git a/tests/e2e/destroy_command.rs b/tests/e2e/destroy_command.rs index 678cf7b4..1c434344 100644 --- a/tests/e2e/destroy_command.rs +++ b/tests/e2e/destroy_command.rs @@ -232,3 +232,54 @@ fn it_should_complete_full_lifecycle_with_custom_working_directory() { env_assertions.assert_environment_exists("test-lifecycle"); env_assertions.assert_environment_state_is("test-lifecycle", "Destroyed"); } + +#[test] +fn it_should_produce_json_by_default() { + // Verify dependencies before running tests + verify_required_dependencies().expect("Dependency verification failed"); + + // Arrange: Create environment first so destroy has something to remove + let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace"); + let config = create_test_environment_config("test-destroy-json-default"); + temp_workspace + .write_config_file("environment.json", &config) + .expect("Failed to write config file"); + let config_path = temp_workspace.path().join("environment.json"); + + let create_result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_create_command(config_path.to_str().unwrap()) + .expect("Failed to run create command"); + + assert!( + create_result.success(), + "Pre-condition: create must succeed, stderr: {}", + create_result.stderr() + ); + + // Act: Run destroy command without --output-format + let result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_destroy_command("test-destroy-json-default") + .expect("Failed to run destroy command"); + + // Assert: Command succeeds + assert!( + result.success(), + "Destroy command should succeed for an existing environment, stderr: {}", + result.stderr() + ); + + // Assert: stdout is valid JSON + let stdout = result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Destroy command default output must be valid JSON"); + + // Assert: Expected field is present + assert!( + json.get("environment_name").is_some(), + "Expected `environment_name` field in destroy JSON output, got: {stdout}" + ); +} diff --git a/tests/e2e/list_command.rs b/tests/e2e/list_command.rs index 8d20c91d..a7ed5d30 100644 --- a/tests/e2e/list_command.rs +++ b/tests/e2e/list_command.rs @@ -203,3 +203,55 @@ fn it_should_list_multiple_environments() { "Expected environment name 'test-list-second' in output, got: {stdout}" ); } + +#[test] +fn it_should_produce_json_by_default() { + // Verify dependencies before running tests + verify_required_dependencies().expect("Dependency verification failed"); + + // Arrange: Create an environment first so the data directory exists and + // `list` can succeed (it fails on empty workspaces with no data directory). + let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace"); + let config_json = create_test_environment_config("test-list-json-default"); + temp_workspace + .write_config_file("environment.json", &config_json) + .expect("Failed to write config file"); + let config_path = temp_workspace.path().join("environment.json"); + + let create_result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_create_command(config_path.to_str().unwrap()) + .expect("Failed to run create command"); + + assert!( + create_result.success(), + "Pre-condition: create must succeed, stderr: {}", + create_result.stderr() + ); + + // Act: Run list command without --output-format + let result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_list_command() + .expect("Failed to run list command"); + + // Assert: Command succeeds + assert!( + result.success(), + "List command should succeed when at least one environment exists, stderr: {}", + result.stderr() + ); + + // Assert: stdout is valid JSON + let stdout = result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("List command default output must be valid JSON"); + + // Assert: Expected top-level key is present + assert!( + json.get("total_count").is_some(), + "Expected `total_count` field in list JSON output, got: {stdout}" + ); +} diff --git a/tests/e2e/purge_command.rs b/tests/e2e/purge_command.rs index 52e8b96f..7db56599 100644 --- a/tests/e2e/purge_command.rs +++ b/tests/e2e/purge_command.rs @@ -123,11 +123,13 @@ fn it_should_purge_destroyed_environment_successfully() { env_assertions.assert_data_directory_not_exists("test-purge-destroyed"); env_assertions.assert_build_directory_not_exists("test-purge-destroyed"); - // Assert: Verify success message in output (check both stdout and stderr) - let output = format!("{}{}", purge_result.stdout(), purge_result.stderr()); - assert!( - output.contains("purged successfully"), - "Output should contain success message. Combined output: {output}" + // Assert: Verify the JSON result is on stdout (not stderr) + let stdout = purge_result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Purge output must be valid JSON on stdout"); + assert_eq!( + json["purged"], true, + "Output should contain JSON success field. Got stdout: {stdout}" ); } @@ -288,11 +290,13 @@ fn it_should_complete_full_lifecycle_from_create_to_purge() { env_assertions.assert_data_directory_not_exists("test-full-lifecycle-purge"); env_assertions.assert_build_directory_not_exists("test-full-lifecycle-purge"); - // Verify purge output indicates success (check both stdout and stderr) - let output = format!("{}{}", purge_result.stdout(), purge_result.stderr()); - assert!( - output.contains("purged successfully"), - "Output should contain success message. Combined output: {output}" + // Verify the JSON result is on stdout (not stderr) + let stdout = purge_result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Purge output must be valid JSON on stdout"); + assert_eq!( + json["purged"], true, + "Output should contain JSON success field. Got stdout: {stdout}" ); } @@ -376,3 +380,54 @@ fn it_should_remove_only_specified_environment_data() { env_assertions.assert_environment_exists("test-purge-env2"); env_assertions.assert_environment_state_is("test-purge-env2", "Destroyed"); } + +#[test] +fn it_should_produce_json_by_default() { + // Verify dependencies before running tests + verify_required_dependencies().expect("Dependency verification failed"); + + // Arrange: Create environment first so purge has something to remove + let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace"); + let config = create_test_environment_config("test-purge-json-default"); + temp_workspace + .write_config_file("environment.json", &config) + .expect("Failed to write config file"); + let config_path = temp_workspace.path().join("environment.json"); + + let create_result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_create_command(config_path.to_str().unwrap()) + .expect("Failed to run create command"); + + assert!( + create_result.success(), + "Pre-condition: create must succeed, stderr: {}", + create_result.stderr() + ); + + // Act: Run purge command without --output-format + let result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_purge_command("test-purge-json-default") + .expect("Failed to run purge command"); + + // Assert: Command succeeds + assert!( + result.success(), + "Purge command should succeed for an existing environment, stderr: {}", + result.stderr() + ); + + // Assert: stdout is valid JSON + let stdout = result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Purge command default output must be valid JSON"); + + // Assert: Expected field confirms environment was purged + assert_eq!( + json["purged"], true, + "Expected `purged: true` in purge JSON output, got: {stdout}" + ); +} diff --git a/tests/e2e/render_command.rs b/tests/e2e/render_command.rs index a6cbee8c..48c5505a 100644 --- a/tests/e2e/render_command.rs +++ b/tests/e2e/render_command.rs @@ -128,11 +128,13 @@ fn it_should_render_artifacts_using_env_name_successfully() { tofu_dir.display() ); - // Assert: Verify success message in output (check both stdout and stderr) - let output = format!("{}{}", render_result.stdout(), render_result.stderr()); + // Assert: Verify the JSON result is on stdout (not stderr) + let stdout = render_result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Render output must be valid JSON on stdout"); assert!( - output.contains("generated successfully"), - "Output should contain success message. Combined output: {output}" + json.get("output_dir").is_some(), + "Output should contain JSON output_dir field. Got stdout: {stdout}" ); } @@ -197,11 +199,13 @@ fn it_should_render_artifacts_using_config_file_successfully() { tofu_dir.display() ); - // Assert: Verify success message in output - let output = format!("{}{}", render_result.stdout(), render_result.stderr()); + // Assert: Verify the JSON result is on stdout (not stderr) + let stdout = render_result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Render output must be valid JSON on stdout"); assert!( - output.contains("generated successfully"), - "Output should contain success message. Combined output: {output}" + json.get("output_dir").is_some(), + "Output should contain JSON output_dir field. Got stdout: {stdout}" ); } @@ -474,10 +478,68 @@ fn it_should_complete_full_lifecycle_from_create_to_render() { // Verify environment remains in Created state (render doesn't change state) env_assertions.assert_environment_state_is("test-full-lifecycle-render", "Created"); - // Verify render output indicates success (check both stdout and stderr) - let output = format!("{}{}", render_result.stdout(), render_result.stderr()); + // Verify the JSON result is on stdout (not stderr) + let stdout = render_result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Render output must be valid JSON on stdout"); assert!( - output.contains("generated successfully"), - "Output should contain success message. Combined output: {output}" + json.get("output_dir").is_some(), + "Output should contain JSON output_dir field. Got stdout: {stdout}" + ); +} + +#[test] +fn it_should_produce_json_by_default() { + // Verify dependencies before running tests + verify_required_dependencies().expect("Dependency verification failed"); + + // Arrange: Create environment first so render has something to work with + let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace"); + let config = create_test_environment_config("test-render-json-default"); + temp_workspace + .write_config_file("environment.json", &config) + .expect("Failed to write config file"); + let config_path = temp_workspace.path().join("environment.json"); + + let create_result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_create_command(config_path.to_str().unwrap()) + .expect("Failed to run create command"); + + assert!( + create_result.success(), + "Pre-condition: create must succeed, stderr: {}", + create_result.stderr() + ); + + // Act: Run render command without --output-format + let output_dir = temp_workspace.path().join("render-output"); + let result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_render_command_with_env_name( + "test-render-json-default", + "192.168.1.100", + output_dir.to_str().unwrap(), + ) + .expect("Failed to run render command"); + + // Assert: Command succeeds + assert!( + result.success(), + "Render command should succeed for an existing environment, stderr: {}", + result.stderr() + ); + + // Assert: stdout is valid JSON + let stdout = result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Render command default output must be valid JSON"); + + // Assert: Expected field is present + assert!( + json.get("output_dir").is_some(), + "Expected `output_dir` field in render JSON output, got: {stdout}" ); } diff --git a/tests/e2e/show_command.rs b/tests/e2e/show_command.rs index f38e7cc2..46e07f64 100644 --- a/tests/e2e/show_command.rs +++ b/tests/e2e/show_command.rs @@ -228,3 +228,54 @@ fn it_should_show_provider_information() { "Expected provider information in output, got: {stdout}" ); } + +#[test] +fn it_should_produce_json_by_default() { + // Verify dependencies before running tests + verify_required_dependencies().expect("Dependency verification failed"); + + // Arrange: Create environment first so show has something to display + let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace"); + let config = create_test_environment_config("test-show-json-default"); + temp_workspace + .write_config_file("environment.json", &config) + .expect("Failed to write config file"); + let config_path = temp_workspace.path().join("environment.json"); + + let create_result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_create_command(config_path.to_str().unwrap()) + .expect("Failed to run create command"); + + assert!( + create_result.success(), + "Pre-condition: create must succeed, stderr: {}", + create_result.stderr() + ); + + // Act: Run show command without --output-format + let result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_show_command("test-show-json-default") + .expect("Failed to run show command"); + + // Assert: Command succeeds + assert!( + result.success(), + "Show command should succeed for an existing environment, stderr: {}", + result.stderr() + ); + + // Assert: stdout is valid JSON + let stdout = result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Show command default output must be valid JSON"); + + // Assert: Expected field is present + assert!( + json.get("name").is_some(), + "Expected `name` field in show JSON output, got: {stdout}" + ); +} diff --git a/tests/e2e/validate_command.rs b/tests/e2e/validate_command.rs index 344ee453..00c1bb30 100644 --- a/tests/e2e/validate_command.rs +++ b/tests/e2e/validate_command.rs @@ -217,3 +217,42 @@ fn it_should_validate_configuration_without_creating_deployment() { "Validate command should not create environment data directory" ); } + +#[test] +fn it_should_produce_json_by_default() { + // Verify dependencies before running tests + verify_required_dependencies().expect("Dependency verification failed"); + + // Arrange: Create a valid environment config file in a temp workspace + let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace"); + let config = create_test_environment_config("test-validate-json-default"); + temp_workspace + .write_config_file("environment.json", &config) + .expect("Failed to write config file"); + let config_path = temp_workspace.path().join("environment.json"); + + // Act: Run validate command without --output-format + let result = process_runner() + .working_dir(temp_workspace.path()) + .log_dir(temp_workspace.path().join("logs")) + .run_validate_command(config_path.to_str().unwrap()) + .expect("Failed to run validate command"); + + // Assert: Command succeeds + assert!( + result.success(), + "Validate command should succeed with a valid config, stderr: {}", + result.stderr() + ); + + // Assert: stdout is valid JSON + let stdout = result.stdout(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Validate command default output must be valid JSON"); + + // Assert: Expected field indicates valid configuration + assert_eq!( + json["is_valid"], true, + "Expected `is_valid: true` in validate JSON output, got: {stdout}" + ); +}