| 1 | use std::path::PathBuf; |
| 2 | use std::process::{Command, Output}; |
| 3 | |
| 4 | fn codewhale_tui_binary() -> PathBuf { |
| 5 | if let Some(path) = option_env!("CARGO_BIN_EXE_codewhale-tui") { |
| 6 | return PathBuf::from(path); |
| 7 | } |
| 8 | if let Ok(path) = std::env::var("CARGO_BIN_EXE_codewhale-tui") { |
| 9 | return PathBuf::from(path); |
| 10 | } |
| 11 | |
| 12 | let mut path = std::env::current_exe().expect("current test executable path"); |
| 13 | path.pop(); |
| 14 | if path.ends_with("deps") { |
| 15 | path.pop(); |
| 16 | } |
| 17 | path.push(format!("codewhale-tui{}", std::env::consts::EXE_SUFFIX)); |
| 18 | path |
| 19 | } |
| 20 | |
| 21 | fn assert_terminal_stream_error(output: Output, expected_fragment: &str) { |
| 22 | assert!( |
| 23 | !output.status.success(), |
| 24 | "workflow-tool unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}", |
| 25 | String::from_utf8_lossy(&output.stdout), |
| 26 | String::from_utf8_lossy(&output.stderr) |
| 27 | ); |
| 28 | let stdout = String::from_utf8(output.stdout).expect("workflow-tool stdout is UTF-8"); |
| 29 | let events = stdout |
| 30 | .lines() |
| 31 | .filter(|line| !line.trim().is_empty()) |
| 32 | .map(|line| { |
| 33 | serde_json::from_str::<serde_json::Value>(line) |
| 34 | .unwrap_or_else(|error| panic!("invalid stream JSON {line:?}: {error}")) |
| 35 | }) |
| 36 | .collect::<Vec<_>>(); |
| 37 | let terminal = events.last().expect("terminal error event"); |
| 38 | assert_eq!(terminal["type"], "error", "events={events:?}"); |
| 39 | assert!( |
| 40 | terminal["error"] |
| 41 | .as_str() |
| 42 | .is_some_and(|error| error.contains(expected_fragment)), |
| 43 | "events={events:?}" |
| 44 | ); |
| 45 | assert!( |
| 46 | events.iter().all(|event| event["type"] != "tool_use"), |
| 47 | "setup failure must happen before tool_use: {events:?}" |
| 48 | ); |
| 49 | } |
| 50 | |
| 51 | #[test] |
| 52 | fn invalid_workflow_input_is_terminal_ndjson() { |
| 53 | let output = Command::new(codewhale_tui_binary()) |
| 54 | .args([ |
| 55 | "workflow-tool", |
| 56 | "--approval-source", |
| 57 | "explicit-workflow-command", |
| 58 | "--input-json", |
| 59 | "{not-json", |
| 60 | ]) |
| 61 | .output() |
| 62 | .expect("run workflow-tool"); |
| 63 | assert_terminal_stream_error(output, "valid Workflow tool input object"); |
| 64 | } |
| 65 | |
| 66 | #[test] |
| 67 | fn missing_profile_is_terminal_ndjson() { |
| 68 | let dir = tempfile::tempdir().expect("tempdir"); |
| 69 | let config = dir.path().join("config.toml"); |
| 70 | std::fs::write(&config, "provider = \"vllm\"\n").expect("write config"); |
| 71 | let output = Command::new(codewhale_tui_binary()) |
| 72 | .arg("--config") |
| 73 | .arg(&config) |
| 74 | .args([ |
| 75 | "--profile", |
| 76 | "missing-profile", |
| 77 | "workflow-tool", |
| 78 | "--approval-source", |
| 79 | "explicit-workflow-command", |
| 80 | "--input-json", |
| 81 | r#"{"action":"run"}"#, |
| 82 | ]) |
| 83 | .env("CODEWHALE_HOME", dir.path().join("codewhale-home")) |
| 84 | .output() |
| 85 | .expect("run workflow-tool with missing profile"); |
| 86 | assert_terminal_stream_error(output, "Profile 'missing-profile' not found"); |
| 87 | } |
| 88 | |
| 89 | #[test] |
| 90 | fn profile_provider_switch_accepts_source_marked_cli_key_offline() { |
| 91 | let dir = tempfile::tempdir().expect("tempdir"); |
| 92 | let config = dir.path().join("config.toml"); |
| 93 | std::fs::write( |
| 94 | &config, |
| 95 | r#" |
| 96 | provider = "deepseek" |
| 97 | |
| 98 | [features] |
| 99 | mcp = false |
| 100 | |
| 101 | [profiles.anthropic] |
| 102 | provider = "anthropic" |
| 103 | "#, |
| 104 | ) |
| 105 | .expect("write profile config"); |
| 106 | let output = Command::new(codewhale_tui_binary()) |
| 107 | .arg("--config") |
| 108 | .arg(&config) |
| 109 | .args([ |
| 110 | "--profile", |
| 111 | "anthropic", |
| 112 | "workflow-tool", |
| 113 | "--approval-source", |
| 114 | "explicit-workflow-command", |
| 115 | "--input-json", |
| 116 | r#"{"action":"run","script":"phase('offline'); return { ok: true };"}"#, |
| 117 | ]) |
| 118 | .env("CODEWHALE_HOME", dir.path().join("codewhale-home")) |
| 119 | .env("DEEPSEEK_API_KEY_SOURCE", "cli") |
| 120 | .env("CODEWHALE_CLI_API_KEY", "profile-switch-secret") |
| 121 | .output() |
| 122 | .expect("run profile-switched workflow-tool"); |
| 123 | |
| 124 | assert!( |
| 125 | output.status.success(), |
| 126 | "workflow-tool failed\nstdout:\n{}\nstderr:\n{}", |
| 127 | String::from_utf8_lossy(&output.stdout), |
| 128 | String::from_utf8_lossy(&output.stderr) |
| 129 | ); |
| 130 | let stdout = String::from_utf8(output.stdout).expect("workflow-tool stdout is UTF-8"); |
| 131 | let event_types = stdout |
| 132 | .lines() |
| 133 | .filter(|line| !line.trim().is_empty()) |
| 134 | .map(|line| { |
| 135 | serde_json::from_str::<serde_json::Value>(line) |
| 136 | .unwrap_or_else(|error| panic!("invalid stream JSON {line:?}: {error}")) |
| 137 | }) |
| 138 | .map(|event| event["type"].as_str().unwrap_or_default().to_string()) |
| 139 | .collect::<Vec<_>>(); |
| 140 | assert!(event_types.iter().any(|kind| kind == "tool_use")); |
| 141 | assert!(event_types.iter().any(|kind| kind == "tool_result")); |
| 142 | assert_eq!(event_types.last().map(String::as_str), Some("done")); |
| 143 | assert!(!stdout.contains("profile-switch-secret")); |
| 144 | assert!(!String::from_utf8_lossy(&output.stderr).contains("profile-switch-secret")); |
| 145 | } |
| 146 |