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