//! CLI integration tests for pack registry commands #![allow(deprecated)] //! //! This module tests: //! - `attune pack install` command with all sources //! - `attune pack checksum` command //! - `attune pack index-entry` command //! - `attune pack index-update` command //! - `attune pack index-merge` command //! - Error handling and output formatting use assert_cmd::Command; use predicates::prelude::*; use serde_json::Value; use std::fs; use tempfile::TempDir; /// Helper to create a test pack directory with pack.yaml fn create_test_pack(name: &str, version: &str, deps: &[&str]) -> TempDir { let temp_dir = TempDir::new().unwrap(); let deps_yaml = if deps.is_empty() { "dependencies: []".to_string() } else { let dep_list = deps .iter() .map(|d| format!(" - {}", d)) .collect::>() .join("\n"); format!("dependencies:\n{}", dep_list) }; let pack_yaml = format!( r#" ref: {} name: Test Pack {} version: {} description: Test pack for CLI integration tests author: Test Author email: test@example.com license: Apache-2.0 homepage: https://example.com repository: https://github.com/example/pack keywords: - test - cli {} python: "3.8" actions: test_action: entry_point: test.py runner_type: python-script description: Test action sensors: test_sensor: entry_point: sensor.py runner_type: python-script triggers: test_trigger: description: Test trigger "#, name, name, version, deps_yaml ); fs::write(temp_dir.path().join("pack.yaml"), pack_yaml).unwrap(); fs::write(temp_dir.path().join("test.py"), "print('test action')").unwrap(); fs::write(temp_dir.path().join("sensor.py"), "print('test sensor')").unwrap(); temp_dir } /// Helper to create a registry index file fn create_test_index(packs: &[(&str, &str)]) -> TempDir { let temp_dir = TempDir::new().unwrap(); let pack_entries: Vec = packs .iter() .map(|(name, version)| { format!( r#"{{ "ref": "{}", "label": "Test Pack {}", "version": "{}", "author": "Test", "license": "Apache-2.0", "keywords": ["test"], "install_sources": [ {{ "type": "git", "url": "https://github.com/test/{}.git", "ref": "v{}", "checksum": "sha256:abc123" }} ] }}"#, name, name, version, name, version ) }) .collect(); let index = format!( r#"{{ "version": "1.0", "packs": [ {} ] }}"#, pack_entries.join(",\n") ); fs::write(temp_dir.path().join("index.json"), index).unwrap(); temp_dir } /// Create an isolated CLI command that never touches the user's real config. /// /// Returns `(Command, TempDir)` — the `TempDir` must be kept alive for the /// duration of the test so the config directory isn't deleted prematurely. fn isolated_cmd() -> (Command, TempDir) { let config_dir = TempDir::new().expect("Failed to create temp config dir"); // Write a minimal default config so the CLI doesn't try to create one let attune_dir = config_dir.path().join("attune"); fs::create_dir_all(&attune_dir).expect("Failed to create attune config dir"); fs::write( attune_dir.join("config.yaml"), "profile: default\nformat: table\nprofiles:\n default:\n api_url: http://localhost:8080\n", ) .expect("Failed to write test config"); let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", config_dir.path()) .env("HOME", config_dir.path()); (cmd, config_dir) } #[test] fn test_pack_checksum_directory() { let pack_dir = create_test_pack("checksum-test", "1.0.0", &[]); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("--output") .arg("table") .arg("pack") .arg("checksum") .arg(pack_dir.path().to_str().unwrap()); cmd.assert() .success() .stdout(predicate::str::contains("sha256:")); } #[test] fn test_pack_checksum_json_output() { let pack_dir = create_test_pack("checksum-json", "1.0.0", &[]); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("--output") .arg("json") .arg("pack") .arg("checksum") .arg(pack_dir.path().to_str().unwrap()); let output = cmd.assert().success(); let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap(); // Verify it's valid JSON let json: Value = serde_json::from_str(&stdout).unwrap(); assert!(json["checksum"].is_string()); assert!(json["checksum"].as_str().unwrap().starts_with("sha256:")); } #[test] fn test_pack_checksum_nonexistent_path() { let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("pack").arg("checksum").arg("/nonexistent/path"); cmd.assert().failure().stderr( predicate::str::contains("not found").or(predicate::str::contains("does not exist")), ); } #[test] fn test_pack_index_entry_generates_valid_json() { let pack_dir = create_test_pack("index-entry-test", "1.2.3", &[]); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("--output") .arg("json") .arg("pack") .arg("index-entry") .arg(pack_dir.path().to_str().unwrap()) .arg("--git-url") .arg("https://github.com/test/pack.git") .arg("--git-ref") .arg("v1.2.3"); let output = cmd.assert().success(); let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap(); // Verify it's valid JSON let json: Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(json["ref"], "index-entry-test"); assert_eq!(json["version"], "1.2.3"); assert!(json["install_sources"].is_array()); assert!(json["install_sources"][0]["checksum"] .as_str() .unwrap() .starts_with("sha256:")); // Verify metadata assert_eq!(json["author"], "Test Author"); assert_eq!(json["license"], "Apache-2.0"); assert!(!json["keywords"].as_array().unwrap().is_empty()); } #[test] fn test_pack_index_entry_with_archive_url() { let pack_dir = create_test_pack("archive-test", "2.0.0", &[]); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("--output") .arg("json") .arg("pack") .arg("index-entry") .arg(pack_dir.path().to_str().unwrap()) .arg("--archive-url") .arg("https://releases.example.com/pack-2.0.0.tar.gz"); let output = cmd.assert().success(); let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap(); let json: Value = serde_json::from_str(&stdout).unwrap(); assert!(!json["install_sources"].as_array().unwrap().is_empty()); let archive_source = &json["install_sources"][0]; assert_eq!(archive_source["type"], "archive"); assert_eq!( archive_source["url"], "https://releases.example.com/pack-2.0.0.tar.gz" ); } #[test] fn test_pack_index_entry_missing_pack_yaml() { let temp_dir = TempDir::new().unwrap(); fs::write(temp_dir.path().join("readme.txt"), "No pack.yaml here").unwrap(); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("pack") .arg("index-entry") .arg(temp_dir.path().to_str().unwrap()); cmd.assert() .failure() .stderr(predicate::str::contains("pack.yaml")); } #[test] fn test_pack_index_update_adds_new_entry() { let index_dir = create_test_index(&[("existing-pack", "1.0.0")]); let index_path = index_dir.path().join("index.json"); let pack_dir = create_test_pack("new-pack", "1.0.0", &[]); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("pack") .arg("index-update") .arg("--index") .arg(index_path.to_str().unwrap()) .arg(pack_dir.path().to_str().unwrap()) .arg("--git-url") .arg("https://github.com/test/new-pack.git") .arg("--git-ref") .arg("v1.0.0"); cmd.assert() .success() .stdout(predicate::str::contains("new-pack")) .stdout(predicate::str::contains("1.0.0")); // Verify index was updated let updated_index = fs::read_to_string(&index_path).unwrap(); let json: Value = serde_json::from_str(&updated_index).unwrap(); assert_eq!(json["packs"].as_array().unwrap().len(), 2); } #[test] fn test_pack_index_update_prevents_duplicate_without_flag() { let index_dir = create_test_index(&[("existing-pack", "1.0.0")]); let index_path = index_dir.path().join("index.json"); let pack_dir = create_test_pack("existing-pack", "1.0.0", &[]); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("pack") .arg("index-update") .arg("--index") .arg(index_path.to_str().unwrap()) .arg(pack_dir.path().to_str().unwrap()) .arg("--git-url") .arg("https://github.com/test/existing-pack.git"); cmd.assert() .failure() .stderr(predicate::str::contains("already exists")); } #[test] fn test_pack_index_update_with_update_flag() { let index_dir = create_test_index(&[("existing-pack", "1.0.0")]); let index_path = index_dir.path().join("index.json"); let pack_dir = create_test_pack("existing-pack", "2.0.0", &[]); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("pack") .arg("index-update") .arg("--index") .arg(index_path.to_str().unwrap()) .arg(pack_dir.path().to_str().unwrap()) .arg("--git-url") .arg("https://github.com/test/existing-pack.git") .arg("--git-ref") .arg("v2.0.0") .arg("--update"); cmd.assert() .success() .stdout(predicate::str::contains("existing-pack")) .stdout(predicate::str::contains("2.0.0")); // Verify version was updated let updated_index = fs::read_to_string(&index_path).unwrap(); let json: Value = serde_json::from_str(&updated_index).unwrap(); let packs = json["packs"].as_array().unwrap(); assert_eq!(packs.len(), 1); assert_eq!(packs[0]["version"], "2.0.0"); } #[test] fn test_pack_index_update_invalid_index_file() { let temp_dir = TempDir::new().unwrap(); let bad_index = temp_dir.path().join("bad-index.json"); fs::write(&bad_index, "not valid json {").unwrap(); let pack_dir = create_test_pack("test-pack", "1.0.0", &[]); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("pack") .arg("index-update") .arg("--index") .arg(bad_index.to_str().unwrap()) .arg(pack_dir.path().to_str().unwrap()); cmd.assert().failure(); } #[test] fn test_pack_index_merge_combines_indexes() { let index1 = create_test_index(&[("pack-a", "1.0.0"), ("pack-b", "1.0.0")]); let index2 = create_test_index(&[("pack-c", "1.0.0"), ("pack-d", "1.0.0")]); let output_dir = TempDir::new().unwrap(); let output_path = output_dir.path().join("merged.json"); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("--output") .arg("table") .arg("pack") .arg("index-merge") .arg("--file") .arg(output_path.to_str().unwrap()) .arg(index1.path().join("index.json").to_str().unwrap()) .arg(index2.path().join("index.json").to_str().unwrap()); cmd.assert() .success() .stdout(predicate::str::contains("Merged")) .stdout(predicate::str::contains("2")); // Verify merged file let merged_content = fs::read_to_string(&output_path).unwrap(); let json: Value = serde_json::from_str(&merged_content).unwrap(); assert_eq!(json["packs"].as_array().unwrap().len(), 4); } #[test] fn test_pack_index_merge_deduplicates() { let index1 = create_test_index(&[("pack-a", "1.0.0"), ("pack-b", "1.0.0")]); let index2 = create_test_index(&[("pack-a", "2.0.0"), ("pack-c", "1.0.0")]); let output_dir = TempDir::new().unwrap(); let output_path = output_dir.path().join("merged.json"); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("--output") .arg("table") .arg("pack") .arg("index-merge") .arg("--file") .arg(output_path.to_str().unwrap()) .arg(index1.path().join("index.json").to_str().unwrap()) .arg(index2.path().join("index.json").to_str().unwrap()); cmd.assert() .success() .stdout(predicate::str::contains("Duplicates resolved")); // Verify deduplication (should have 3 unique packs: pack-a, pack-b, pack-c) let merged_content = fs::read_to_string(&output_path).unwrap(); let json: Value = serde_json::from_str(&merged_content).unwrap(); let packs = json["packs"].as_array().unwrap(); assert_eq!(packs.len(), 3); // Verify pack-a has the newer version let pack_a = packs.iter().find(|p| p["ref"] == "pack-a").unwrap(); assert_eq!(pack_a["version"], "2.0.0"); } #[test] fn test_pack_index_merge_output_exists_without_force() { let index1 = create_test_index(&[("pack-a", "1.0.0")]); let output_dir = TempDir::new().unwrap(); let output_path = output_dir.path().join("merged.json"); fs::write(&output_path, "existing content").unwrap(); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("pack") .arg("index-merge") .arg("--file") .arg(output_path.to_str().unwrap()) .arg(index1.path().join("index.json").to_str().unwrap()); cmd.assert() .failure() .stderr(predicate::str::contains("already exists").or(predicate::str::contains("force"))); } #[test] fn test_pack_index_merge_with_force_flag() { let index1 = create_test_index(&[("pack-a", "1.0.0")]); let output_dir = TempDir::new().unwrap(); let output_path = output_dir.path().join("merged.json"); fs::write(&output_path, "existing content").unwrap(); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("pack") .arg("index-merge") .arg("--file") .arg(output_path.to_str().unwrap()) .arg(index1.path().join("index.json").to_str().unwrap()) .arg("--force"); cmd.assert().success(); // Verify file was overwritten let merged_content = fs::read_to_string(&output_path).unwrap(); assert_ne!(merged_content, "existing content"); } #[test] fn test_pack_index_merge_empty_input_list() { let output_dir = TempDir::new().unwrap(); let output_path = output_dir.path().join("merged.json"); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("pack") .arg("index-merge") .arg("--file") .arg(output_path.to_str().unwrap()); // Should fail due to missing required inputs cmd.assert().failure(); } #[test] fn test_pack_index_merge_missing_input_file() { let index1 = create_test_index(&[("pack-a", "1.0.0")]); let output_dir = TempDir::new().unwrap(); let output_path = output_dir.path().join("merged.json"); let (mut cmd, _config_dir) = isolated_cmd(); cmd.arg("--output") .arg("table") .arg("pack") .arg("index-merge") .arg("--file") .arg(output_path.to_str().unwrap()) .arg(index1.path().join("index.json").to_str().unwrap()) .arg("/nonexistent/index.json"); // Should succeed but skip missing file (with warning in stderr) cmd.assert() .success() .stderr(predicate::str::contains("Skipping").or(predicate::str::contains("missing"))); } #[test] fn test_pack_commands_help() { let commands = vec![ vec!["pack", "checksum", "--help"], vec!["pack", "index-entry", "--help"], vec!["pack", "index-update", "--help"], vec!["pack", "index-merge", "--help"], ]; for args in commands { let (mut cmd, _config_dir) = isolated_cmd(); for arg in &args { cmd.arg(arg); } cmd.assert() .success() .stdout(predicate::str::contains("Usage:")); } }