返回 CodeWhale
workflow_tool_stream_acceptance.rs
根目录 / crates / tui / tests / workflow_tool_stream_acceptance.rs
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
146 lines RUST