返回 CodeWhale
terminal_mode_test_cases_03.rs
根目录 / crates / tui / src / lib / terminal_mode_test_cases_03.rs
1
2 #[test]
3 fn exec_only_installs_an_explicit_headless_turn_cap() {
4 let cli = parse_cli(&["codewhale", "exec", "--auto", "benchmark this"]);
5 let Some(Commands::Exec(args)) = cli.command else {
6 panic!("expected exec command");
7 };
8
9 assert_eq!(args.max_turns, None);
10 let defaulted = exec_max_steps(args.max_turns);
11 assert_eq!(
12 defaulted,
13 crate::core::engine::turn_budget::DEFAULT_MAX_MODEL_STEPS
14 );
15 assert_eq!(
16 crate::core::turn::TurnContext::new(defaulted).step_limit(),
17 None
18 );
19 assert_eq!(exec_max_steps(Some(7)), 7);
20 let mut capped = crate::core::turn::TurnContext::new(exec_max_steps(Some(7)));
21 for _ in 0..7 {
22 capped.next_step();
23 }
24 assert!(capped.at_max_steps());
25 assert_eq!(capped.stop_diagnostics.effective_max_steps, Some(7));
26 assert_eq!(
27 exec_max_steps(Some(u32::MAX)),
28 crate::core::engine::turn_budget::MAX_MAX_MODEL_STEPS,
29 "even the largest override stays finite"
30 );
31 }
32
33 #[test]
34 fn exec_accepts_continue_for_latest_workspace_session() {
35 let cli = parse_cli(&["codewhale", "exec", "--continue", "follow up"]);
36 let Some(Commands::Exec(args)) = cli.command else {
37 panic!("expected exec command");
38 };
39
40 assert!(args.continue_session);
41 }
42
43 #[test]
44 fn sessions_footer_points_to_resume_subcommand() {
45 let cli = parse_cli(&["codewhale", "resume", "abc123"]);
46 let Some(Commands::Resume { session_id, last }) = cli.command else {
47 panic!("expected resume command");
48 };
49
50 assert_eq!(session_id.as_deref(), Some("abc123"));
51 assert!(!last);
52 assert_eq!(sessions_resume_command(), "codewhale resume");
53 assert!(!sessions_resume_command().contains("--resume"));
54 }
55
56 #[test]
57 fn plugin_registry_initialization_precedes_dotenv_for_all_launch_paths() {
58 use std::cell::Cell;
59
60 #[derive(Clone, Copy)]
61 enum Expected {
62 Plain,
63 Resume,
64 Fork,
65 Exec,
66 Serve,
67 }
68
69 let cases: &[(&[&str], Expected)] = &[
70 (&["codewhale"], Expected::Plain),
71 (&["codewhale", "resume", "--last"], Expected::Resume),
72 (&["codewhale", "fork", "--last"], Expected::Fork),
73 (&["codewhale", "exec", "probe"], Expected::Exec),
74 (&["codewhale", "serve", "--mcp"], Expected::Serve),
75 ];
76
77 for (args, expected) in cases {
78 let phase = Cell::new(0);
79 let (_cli, command) = prepare_cli_startup(
80 parse_cli(args),
81 || {
82 assert_eq!(phase.get(), 0, "plugin init order for {args:?}");
83 phase.set(1);
84 },
85 || {
86 assert_eq!(phase.get(), 1, "dotenv load order for {args:?}");
87 phase.set(2);
88 },
89 );
90
91 assert_eq!(phase.get(), 2, "startup phases for {args:?}");
92 let correct_variant = matches!(
93 (expected, command.as_ref()),
94 (Expected::Plain, None)
95 | (Expected::Resume, Some(Commands::Resume { .. }))
96 | (Expected::Fork, Some(Commands::Fork { .. }))
97 | (Expected::Exec, Some(Commands::Exec(_)))
98 | (Expected::Serve, Some(Commands::Serve(_)))
99 );
100 assert!(correct_variant, "unexpected command for {args:?}");
101 }
102 }
103
104 #[test]
105 fn scorecard_rejects_a_threshold_that_cannot_gate() {
106 for bad in ["NaN", "inf", "-inf"] {
107 assert!(
108 Cli::try_parse_from([
109 "codewhale",
110 "scorecard",
111 "--input",
112 "t.json",
113 "--threshold",
114 bad
115 ])
116 .is_err(),
117 "--threshold {bad} must be refused"
118 );
119 }
120 assert!(
121 Cli::try_parse_from([
122 "codewhale",
123 "scorecard",
124 "--input",
125 "t.json",
126 "--threshold",
127 "2.5"
128 ])
129 .is_ok()
130 );
131 }
132
133 #[test]
134 fn workspace_dotenv_is_found_from_the_launch_workspace_not_the_process_directory() {
135 let workspace = tempfile::tempdir().expect("workspace tempdir");
136 let nested = workspace.path().join("crates/app");
137 std::fs::create_dir_all(&nested).expect("mkdir nested");
138 std::fs::create_dir_all(workspace.path().join(".git")).expect("mkdir .git");
139 std::fs::write(workspace.path().join(".env"), "OPENAI_API_KEY=workspace\n")
140 .expect("write .env");
141
142 // `--workspace <dir>` from a process directory in another tree.
143 assert_eq!(
144 find_workspace_dotenv(workspace.path()).expect("search"),
145 Some(workspace.path().join(".env"))
146 );
147 // Nested launch directory walks up to the repository root.
148 assert_eq!(
149 find_workspace_dotenv(&nested).expect("search"),
150 Some(workspace.path().join(".env"))
151 );
152 }
153
154 #[test]
155 fn workspace_dotenv_loads_only_provider_credentials_and_preserves_shell_values() {
156 let _lock = crate::test_support::lock_test_env();
157 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY");
158 let _nvidia = crate::test_support::EnvVarGuard::set("NVIDIA_API_KEY", "shell-key");
159 let _home = crate::test_support::EnvVarGuard::remove("CODEWHALE_HOME");
160 let _config = crate::test_support::EnvVarGuard::remove("CODEWHALE_CONFIG_PATH");
161 let _shell = crate::test_support::EnvVarGuard::remove("DEEPSEEK_ALLOW_SHELL");
162 let tmp = tempfile::TempDir::new().expect("temp workspace");
163 let dotenv = tmp.path().join(".env");
164 std::fs::write(
165 &dotenv,
166 "DEEPSEEK_API_KEY=workspace-key\n\
167 NVIDIA_API_KEY=repo-must-not-override-shell\n\
168 CODEWHALE_HOME=./attacker-home\n\
169 CODEWHALE_CONFIG_PATH=./attacker.toml\n\
170 DEEPSEEK_ALLOW_SHELL=true\n",
171 )
172 .expect("write dotenv");
173
174 let report = load_workspace_dotenv_credentials_from_path(&dotenv).expect("safe load");
175
176 assert_eq!(
177 std::env::var("DEEPSEEK_API_KEY").as_deref(),
178 Ok("workspace-key")
179 );
180 assert_eq!(std::env::var("NVIDIA_API_KEY").as_deref(), Ok("shell-key"));
181 assert!(std::env::var_os("CODEWHALE_HOME").is_none());
182 assert!(std::env::var_os("CODEWHALE_CONFIG_PATH").is_none());
183 assert!(std::env::var_os("DEEPSEEK_ALLOW_SHELL").is_none());
184 assert_eq!(
185 report.loaded,
186 BTreeSet::from(["DEEPSEEK_API_KEY".to_string()])
187 );
188 assert_eq!(
189 report.ignored,
190 BTreeSet::from([
191 "CODEWHALE_CONFIG_PATH".to_string(),
192 "CODEWHALE_HOME".to_string(),
193 "DEEPSEEK_ALLOW_SHELL".to_string(),
194 ])
195 );
196 }
197
198 #[test]
199 fn workspace_dotenv_rejects_ambient_variable_substitution() {
200 let _lock = crate::test_support::lock_test_env();
201 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY");
202 let _ambient = crate::test_support::EnvVarGuard::set(
203 "CODEWHALE_JS_SECRET_LEAK_TEST",
204 "ambient-secret-must-not-expand",
205 );
206 let tmp = tempfile::TempDir::new().expect("temp workspace");
207 let dotenv = tmp.path().join(".env");
208 std::fs::write(
209 &dotenv,
210 "DEEPSEEK_API_KEY=${CODEWHALE_JS_SECRET_LEAK_TEST}\n",
211 )
212 .expect("write dotenv");
213
214 let error = load_workspace_dotenv_credentials_from_path(&dotenv)
215 .expect_err("expansion must fail closed")
216 .to_string();
217
218 assert!(error.contains("variable expansion"));
219 assert!(!error.contains("ambient-secret-must-not-expand"));
220 assert!(std::env::var_os("DEEPSEEK_API_KEY").is_none());
221 }
222
223 #[test]
224 fn workspace_dotenv_rejects_multiline_ambient_variable_substitution() {
225 let _lock = crate::test_support::lock_test_env();
226 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY");
227 let _ambient = crate::test_support::EnvVarGuard::set(
228 "CODEWHALE_JS_SECRET_LEAK_TEST",
229 "ambient-secret-must-not-expand",
230 );
231 let tmp = tempfile::TempDir::new().expect("temp workspace");
232 let dotenv = tmp.path().join(".env");
233 std::fs::write(
234 &dotenv,
235 "DEEPSEEK_API_KEY=\"prefix\n$CODEWHALE_JS_SECRET_LEAK_TEST=bar\nsuffix\"\n",
236 )
237 .expect("write dotenv");
238
239 let error = load_workspace_dotenv_credentials_from_path(&dotenv)
240 .expect_err("multiline expansion must fail closed")
241 .to_string();
242
243 assert!(error.contains("variable expansion"));
244 assert!(!error.contains("ambient-secret-must-not-expand"));
245 assert!(std::env::var_os("DEEPSEEK_API_KEY").is_none());
246 }
247
248 #[test]
249 fn workspace_dotenv_comment_quote_cannot_hide_later_expansion() {
250 let _lock = crate::test_support::lock_test_env();
251 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY");
252 let _ambient = crate::test_support::EnvVarGuard::set(
253 "CODEWHALE_JS_SECRET_LEAK_TEST",
254 "ambient-secret-must-not-expand",
255 );
256 let tmp = tempfile::TempDir::new().expect("temp workspace");
257 let dotenv = tmp.path().join(".env");
258 std::fs::write(
259 &dotenv,
260 "# unmatched quote in ignored comment: '\n\
261 DEEPSEEK_API_KEY=$CODEWHALE_JS_SECRET_LEAK_TEST\n",
262 )
263 .expect("write dotenv");
264
265 let error = load_workspace_dotenv_credentials_from_path(&dotenv)
266 .expect_err("comment quote must not hide expansion")
267 .to_string();
268
269 assert!(error.contains("variable expansion"));
270 assert!(!error.contains("ambient-secret-must-not-expand"));
271 assert!(std::env::var_os("DEEPSEEK_API_KEY").is_none());
272 }
273
274 #[test]
275 fn workspace_dotenv_allows_single_quoted_literal_dollar() {
276 let _lock = crate::test_support::lock_test_env();
277 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY");
278 let tmp = tempfile::TempDir::new().expect("temp workspace");
279 let dotenv = tmp.path().join(".env");
280 std::fs::write(&dotenv, "DEEPSEEK_API_KEY='$literal-value'\n").expect("write dotenv");
281
282 load_workspace_dotenv_credentials_from_path(&dotenv).expect("literal dollar load");
283
284 assert_eq!(
285 std::env::var("DEEPSEEK_API_KEY").as_deref(),
286 Ok("$literal-value")
287 );
288 }
289
290 #[test]
291 fn workspace_dotenv_parse_failure_applies_no_earlier_credentials() {
292 let _lock = crate::test_support::lock_test_env();
293 let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY");
294 let tmp = tempfile::TempDir::new().expect("temp workspace");
295 let dotenv = tmp.path().join(".env");
296 std::fs::write(
297 &dotenv,
298 "DEEPSEEK_API_KEY=must-not-survive\nBROKEN=\"unterminated\n",
299 )
300 .expect("write dotenv");
301
302 let error = load_workspace_dotenv_credentials_from_path(&dotenv)
303 .expect_err("parse failure must be transactional")
304 .to_string();
305
306 assert!(error.contains("could not be parsed safely"), "{error}");
307 assert!(!error.contains("must-not-survive"));
308 assert!(std::env::var_os("DEEPSEEK_API_KEY").is_none());
309 }
310
311 #[test]
312 fn workspace_dotenv_credential_allowlist_excludes_control_plane_names() {
313 for provider in codewhale_config::provider::providers_sorted_for_display() {
314 for key in provider.env_vars() {
315 assert!(
316 is_workspace_dotenv_credential_key(key),
317 "provider credential {key} must remain supported"
318 );
319 }
320 }
321 for key in [
322 "CODEWHALE_HOME",
323 "CODEWHALE_CONFIG_PATH",
324 "DEEPSEEK_CONFIG_PATH",
325 "DEEPSEEK_PROFILE",
326 "DEEPSEEK_MANAGED_CONFIG_PATH",
327 "DEEPSEEK_REQUIREMENTS_PATH",
328 "DEEPSEEK_PROVIDER",
329 "DEEPSEEK_BASE_URL",
330 "DEEPSEEK_MODEL",
331 "DEEPSEEK_APPROVAL_POLICY",
332 "DEEPSEEK_SANDBOX_MODE",
333 "DEEPSEEK_ALLOW_SHELL",
334 "DEEPSEEK_YOLO",
335 "DEEPSEEK_MCP_CONFIG",
336 "CODEWHALE_RUNTIME_TOKEN",
337 "PATH",
338 "NODE_OPTIONS",
339 "PYTHONPATH",
340 "LD_PRELOAD",
341 "DYLD_INSERT_LIBRARIES",
342 ] {
343 assert!(
344 !is_workspace_dotenv_credential_key(key),
345 "control-plane variable {key} must not load from a workspace"
346 );
347 }
348 }
349
350 #[cfg(unix)]
351 #[test]
352 fn workspace_dotenv_does_not_follow_symbolic_links() {
353 use std::os::unix::fs::symlink;
354
355 let tmp = tempfile::TempDir::new().expect("temp workspace");
356 let external = tmp.path().join("external-credentials");
357 let dotenv = tmp.path().join(".env");
358 std::fs::write(&external, "DEEPSEEK_API_KEY=external-secret\n")
359 .expect("write external fixture");
360 symlink(&external, &dotenv).expect("create dotenv symlink");
361
362 let error = load_workspace_dotenv_credentials_from_path(&dotenv)
363 .expect_err("symlink must fail closed")
364 .to_string();
365
366 assert!(error.contains("securely open"), "{error}");
367 assert!(!error.contains("external-secret"));
368 }
369
370 #[cfg(unix)]
371 #[test]
372 fn workspace_dotenv_rejects_hard_links_to_external_files() {
373 let tmp = tempfile::TempDir::new().expect("temp workspace");
374 let external = tmp.path().join("external-credentials");
375 let dotenv = tmp.path().join(".env");
376 std::fs::write(&external, "DEEPSEEK_API_KEY=external-secret\n")
377 .expect("write external fixture");
378 std::fs::hard_link(&external, &dotenv).expect("create dotenv hard link");
379
380 let error = load_workspace_dotenv_credentials_from_path(&dotenv)
381 .expect_err("hard link must fail closed")
382 .to_string();
383
384 assert!(error.contains("multiple filesystem links"), "{error}");
385 assert!(!error.contains("external-secret"));
386 }
387
388 #[cfg(unix)]
389 #[test]
390 fn workspace_dotenv_rejects_fifo_without_blocking_startup() {
391 use std::ffi::CString;
392 use std::os::unix::ffi::OsStrExt;
393 use std::sync::mpsc;
394 use std::time::Duration;
395
396 let tmp = tempfile::TempDir::new().expect("temp workspace");
397 let dotenv = tmp.path().join(".env");
398 let c_path = CString::new(dotenv.as_os_str().as_bytes()).expect("fifo path");
399 // SAFETY: `c_path` is a live, NUL-terminated path and the requested
400 // mode grants access only to the current user.
401 let result = unsafe { libc::mkfifo(c_path.as_ptr(), libc::S_IRUSR | libc::S_IWUSR) };
402 assert_eq!(result, 0, "mkfifo failed: {}", io::Error::last_os_error());
403
404 let (tx, rx) = mpsc::channel();
405 let worker_path = dotenv.clone();
406 let worker = std::thread::spawn(move || {
407 let result = load_workspace_dotenv_credentials_from_path(&worker_path)
408 .map(|_| "unexpected success".to_string())
409 .unwrap_or_else(|error| error.to_string());
410 tx.send(result).expect("send loader result");
411 });
412
413 let error = match rx.recv_timeout(Duration::from_secs(1)) {
414 Ok(error) => error,
415 Err(timeout) => {
416 // Release a regressed blocking reader so the test can fail
417 // promptly instead of leaving a stuck process behind.
418 let _writer = std::fs::OpenOptions::new()
419 .write(true)
420 .open(&dotenv)
421 .expect("open fifo writer to release blocked reader");
422 let _ = rx.recv_timeout(Duration::from_secs(1));
423 worker.join().expect("join released loader");
424 panic!("workspace .env FIFO blocked startup: {timeout}");
425 }
426 };
427 worker.join().expect("join loader");
428
429 assert!(error.contains("not a regular file"), "{error}");
430 }
431
432 #[test]
433 fn exec_json_conflicts_with_stream_json_output() {
434 let err = Cli::try_parse_from([
435 "codewhale",
436 "exec",
437 "--json",
438 "--output-format",
439 "stream-json",
440 "hello",
441 ])
442 .expect_err("json summary and stream-json must not mix");
443
444 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
445 }
446
447 #[test]
448 fn exec_stream_turn_usage_event_serializes_reported_fields() {
449 let event = ExecStreamEvent::TurnUsage {
450 turn: 2,
451 input_tokens: 1200,
452 output_tokens: 180,
453 reasoning_tokens: Some(90),
454 prompt_cache_hit_tokens: Some(900),
455 prompt_cache_miss_tokens: Some(300),
456 prompt_cache_write_tokens: Some(0),
457 reasoning_replay_tokens: Some(40),
458 duration_ms: 1834,
459 };
460
461 let value = exec_stream_value(&event).expect("serializes");
462 let json = serde_json::to_string(&value).expect("serializes");
463 assert!(!json.contains('\n'));
464 let parsed: serde_json::Value = serde_json::from_str(&json).expect("valid json");
465 assert_eq!(parsed["type"], "turn_usage");
466 assert_eq!(parsed["schema"], "codewhale.exec-stream");
467 assert_eq!(parsed["schema_version"], 1);
468 assert_eq!(parsed["turn"], 2);
469 assert_eq!(parsed["input_tokens"], 1200);
470 assert_eq!(parsed["output_tokens"], 180);
471 assert_eq!(parsed["reasoning_tokens"], 90);
472 assert_eq!(parsed["prompt_cache_hit_tokens"], 900);
473 assert_eq!(parsed["prompt_cache_miss_tokens"], 300);
474 assert_eq!(parsed["prompt_cache_write_tokens"], 0);
475 assert_eq!(parsed["reasoning_replay_tokens"], 40);
476 assert_eq!(parsed["duration_ms"], 1834);
477 }
478
479 #[test]
480 fn exec_stream_turn_usage_event_omits_unreported_fields() {
481 // Honest absence: optional token fields the provider did not report
482 // are dropped from the object entirely — never emitted as null and
483 // never backfilled with fabricated zeros.
484 let event = ExecStreamEvent::TurnUsage {
485 turn: 1,
486 input_tokens: 11,
487 output_tokens: 3,
488 reasoning_tokens: None,
489 prompt_cache_hit_tokens: None,
490 prompt_cache_miss_tokens: None,
491 prompt_cache_write_tokens: None,
492 reasoning_replay_tokens: None,
493 duration_ms: 250,
494 };
495
496 let value = exec_stream_value(&event).expect("serializes");
497 let parsed = value;
498 assert_eq!(parsed["type"], "turn_usage");
499 assert_eq!(parsed["input_tokens"], 11);
500 assert_eq!(parsed["output_tokens"], 3);
501 assert_eq!(parsed["duration_ms"], 250);
502 let object = parsed.as_object().expect("event object");
503 for absent in [
504 "reasoning_tokens",
505 "prompt_cache_hit_tokens",
506 "prompt_cache_miss_tokens",
507 "prompt_cache_write_tokens",
508 "reasoning_replay_tokens",
509 ] {
510 assert!(!object.contains_key(absent), "{absent} leaked: {parsed}");
511 }
512 }
513
514 #[test]
515 fn exec_stream_pre_existing_event_type_tags_are_unchanged() {
516 // Contract guard for existing stream consumers (bench harness, fleet
517 // ledger): the pre-turn_usage event vocabulary keeps its exact tags.
518 let cases: Vec<(ExecStreamEvent, &str)> = vec![
519 (
520 ExecStreamEvent::Content {
521 content: "hi".to_string(),
522 },
523 "content",
524 ),
525 (
526 ExecStreamEvent::ToolUse {
527 name: "read_file".to_string(),
528 id: "call_1".to_string(),
529 input: serde_json::json!({}),
530 started_at: "2026-08-03T00:00:00Z".to_string(),
531 },
532 "tool_use",
533 ),
534 (
535 ExecStreamEvent::ToolResult {
536 id: "call_1".to_string(),
537 name: "read_file".to_string(),
538 output: "ok".to_string(),
539 status: "success".to_string(),
540 started_at: "2026-08-03T00:00:00Z".to_string(),
541 completed_at: "2026-08-03T00:00:01Z".to_string(),
542 duration_ms: 1,
543 side_effect_status: "unknown".to_string(),
544 error_category: None,
545 truncated: None,
546 artifact: None,
547 result_metadata: None,
548 },
549 "tool_result",
550 ),
551 (
552 ExecStreamEvent::SandboxDenied {
553 tool_id: "call_1".to_string(),
554 tool_name: "exec_shell".to_string(),
555 reason: "denied".to_string(),
556 outcome: "approval_required".to_string(),
557 },
558 "sandbox_denied",
559 ),
560 (
561 ExecStreamEvent::WorkflowEvent {
562 run_id: "workflow_1".to_string(),
563 event: serde_json::json!({"type": "task_completed"}),
564 },
565 "workflow_event",
566 ),
567 (
568 ExecStreamEvent::SessionCapture {
569 content: "x".to_string(),
570 saved_session_id: "session-x".to_string(),
571 },
572 "session_capture",
573 ),
574 (
575 ExecStreamEvent::Error {
576 error: "boom".to_string(),
577 },
578 "error",
579 ),
580 (ExecStreamEvent::Done, "done"),
581 ];
582
583 for (event, expected_type) in cases {
584 let value = exec_stream_value(&event).expect("serializes");
585 assert_eq!(value["type"], expected_type, "event tag drifted");
586 assert_eq!(value["schema"], "codewhale.exec-stream");
587 assert_eq!(value["schema_version"], 1);
588 }
589 }
590
591 #[test]
592 fn exec_stream_events_are_json_lines() {
593 let event = ExecStreamEvent::ToolResult {
594 id: "call_1".to_string(),
595 name: "read_file".to_string(),
596 output: "line 1\nline 2".to_string(),
597 status: "success".to_string(),
598 started_at: "2026-07-13T00:00:00Z".to_string(),
599 completed_at: "2026-07-13T00:00:01Z".to_string(),
600 duration_ms: 1000,
601 side_effect_status: "not_started".to_string(),
602 error_category: None,
603 truncated: Some(false),
604 artifact: None,
605 result_metadata: None,
606 };
607
608 let value = exec_stream_value(&event).expect("serializes");
609 let json = serde_json::to_string(&value).expect("serializes");
610 assert!(!json.contains('\n'));
611 let parsed: serde_json::Value = serde_json::from_str(&json).expect("valid json");
612 assert_eq!(parsed["type"], "tool_result");
613 assert_eq!(parsed["schema"], "codewhale.exec-stream");
614 assert_eq!(parsed["schema_version"], 1);
615 assert_eq!(parsed["duration_ms"], 1000);
616 assert_eq!(parsed["side_effect_status"], "not_started");
617 }
618
619 #[test]
620 fn workflow_receipt_stream_event_is_one_json_line() {
621 let event = ExecStreamEvent::WorkflowEvent {
622 run_id: "workflow_1234".to_string(),
623 event: serde_json::json!({
624 "type": "handoff_promoted",
625 "artifact_id": "workflow_1234:agent_1:review-gate:review_report",
626 "gate_id": "review-gate",
627 "kind": "review_report",
628 "from_role": "reviewer",
629 "to_role": "verifier",
630 "producer_task_id": "agent_1"
631 }),
632 };
633
634 let value = exec_stream_value(&event).expect("serializes");
635 let json = serde_json::to_string(&value).expect("serializes");
636 assert!(!json.contains('\n'));
637 let parsed: serde_json::Value = serde_json::from_str(&json).expect("valid json");
638 assert_eq!(parsed["type"], "workflow_event");
639 assert_eq!(parsed["schema"], "codewhale.exec-stream");
640 assert_eq!(parsed["schema_version"], 1);
641 assert_eq!(parsed["run_id"], "workflow_1234");
642 assert_eq!(parsed["event"]["type"], "handoff_promoted");
643 assert_eq!(
644 parsed["event"]["artifact_id"],
645 "workflow_1234:agent_1:review-gate:review_report"
646 );
647 assert_eq!(parsed["event"]["gate_id"], "review-gate");
648 assert_eq!(parsed["event"]["kind"], "review_report");
649 assert_eq!(parsed["event"]["from_role"], "reviewer");
650 assert_eq!(parsed["event"]["to_role"], "verifier");
651 assert_eq!(parsed["event"]["producer_task_id"], "agent_1");
652 assert!(parsed["event"].get("payload").is_none(), "{parsed}");
653
654 let consumed = ExecStreamEvent::WorkflowEvent {
655 run_id: "workflow_1234".to_string(),
656 event: serde_json::json!({
657 "type": "handoff_consumed",
658 "artifact_id": "workflow_1234:agent_1:review-gate:review_report",
659 "kind": "review_report",
660 "from_role": "reviewer",
661 "to_role": "verifier",
662 "consumer_task_id": "agent_2"
663 }),
664 };
665 let consumed = exec_stream_value(&consumed).expect("serializes consumed receipt");
666 assert_eq!(consumed["type"], "workflow_event");
667 assert_eq!(consumed["schema"], "codewhale.exec-stream");
668 assert_eq!(consumed["schema_version"], 1);
669 assert_eq!(consumed["event"]["type"], "handoff_consumed");
670 assert_eq!(
671 consumed["event"]["artifact_id"],
672 "workflow_1234:agent_1:review-gate:review_report"
673 );
674 assert_eq!(consumed["event"]["consumer_task_id"], "agent_2");
675 assert!(consumed["event"].get("payload").is_none(), "{consumed}");
676 }
677
678 #[test]
679 fn exec_stream_metadata_redacts_resume_breadcrumbs() {
680 let raw_session_id = "abc123fullsecret";
681 let event = ExecStreamEvent::Metadata {
682 meta: Box::new(ExecStreamMeta {
683 receipt_kind: "terminal",
684 provider: "deepseek".to_string(),
685 provider_id: None,
686 model: "deepseek-v4-flash".to_string(),
687 route_source: "explicit_or_configured".to_string(),
688 input_tokens: Some(123),
689 output_tokens: Some(45),
690 prompt_cache_hit_tokens: Some(10),
691 prompt_cache_miss_tokens: None,
692 prompt_cache_write_tokens: None,
693 reasoning_tokens: Some(3),
694 codewhale_max_output_tokens: Some(384_000),
695 codewhale_max_output_tokens_source: Some("documented"),
696 duration_ms: 2500,
697 retry_count: None,
698 approval_posture: "ask".to_string(),
699 sandbox_posture: "configured_default".to_string(),
700 binary_sha256: Some("sha256:binary".to_string()),
701 config_sha256: None,
702 prompt_sha256: "sha256:prompt".to_string(),
703 tool_catalog_sha256: Some("sha256:tools".to_string()),
704 input_analysis: ExecStreamInputAnalysis::default(),
705 visible_final_answer_chars: 17,
706 visible_final_answer_excerpt: "the visible reply".to_string(),
707 session_id: exec_stream_session_ref(raw_session_id),
708 resume_command: exec_stream_resume_hint(raw_session_id),
709 workspace: "/tmp/work".to_string(),
710 message_count: 4,
711 status: Some("completed".to_string()),
712 termination_reason: Some("resolved".to_string()),
713 error_category: None,
714 error: None,
715 }),
716 };
717
718 let json = serde_json::to_string(&event).expect("serializes");
719 assert!(!json.contains('\n'));
720 assert!(!json.contains(raw_session_id));
721 let parsed: serde_json::Value = serde_json::from_str(&json).expect("valid json");
722 assert_eq!(parsed["type"], "metadata");
723 assert_ne!(parsed["meta"]["session_id"], raw_session_id);
724 assert!(
725 parsed["meta"]["session_id"]
726 .as_str()
727 .unwrap()
728 .starts_with("<redacted:")
729 );
730 assert_eq!(
731 parsed["meta"]["resume_command"],
732 "codewhale exec --resume <session_capture.saved_session_id>"
733 );
734 assert_eq!(parsed["meta"]["workspace"], "/tmp/work");
735 assert_eq!(parsed["meta"]["message_count"], 4);
736 assert_eq!(parsed["meta"]["visible_final_answer_chars"], 17);
737 assert_eq!(
738 parsed["meta"]["visible_final_answer_excerpt"],
739 "the visible reply"
740 );
741
742 // Contract (#5946): the raw saved-session id is carried by exactly one
743 // field, `session_capture.saved_session_id`. The `metadata` receipt
744 // above stays fingerprint-only, and the capture's own `content` keeps
745 // the same fingerprint so both surfaces can be correlated in a log.
746 let capture = ExecStreamEvent::SessionCapture {
747 content: exec_stream_session_ref(raw_session_id),
748 saved_session_id: raw_session_id.to_string(),
749 };
750 let capture_json = serde_json::to_string(&capture).expect("serializes");
751 let parsed_capture: serde_json::Value =
752 serde_json::from_str(&capture_json).expect("valid json");
753 assert_eq!(parsed_capture["type"], "session_capture");
754 assert_eq!(parsed_capture["content"], parsed["meta"]["session_id"]);
755 assert_ne!(parsed_capture["content"], raw_session_id);
756 assert_eq!(parsed_capture["saved_session_id"], raw_session_id);
757 assert!(parsed_capture.get("session_id").is_none(), "{capture_json}");
758 }
759
760 #[test]
761 fn exec_stream_final_answer_excerpt_is_bounded_and_redacted() {
762 assert_eq!(
763 exec_stream_final_answer_excerpt(" short reply \n"),
764 "short reply"
765 );
766 let long = "x".repeat(EXEC_STREAM_FINAL_ANSWER_EXCERPT_CHARS + 5);
767 let excerpt = exec_stream_final_answer_excerpt(&long);
768 assert_eq!(
769 excerpt.chars().count(),
770 EXEC_STREAM_FINAL_ANSWER_EXCERPT_CHARS + 3
771 );
772 assert!(excerpt.ends_with("..."));
773 let leaked = exec_stream_final_answer_excerpt("token: sk-ant-must-not-leak-1234567890");
774 assert!(!leaked.contains("sk-ant-must-not-leak"), "{leaked}");
775 }
776
777 #[test]
778 fn exec_stream_final_answer_text_is_the_last_assistant_reply() {
779 // Multi-step turn: pre-tool commentary, a tool result, then a
780 // distinct final answer. The receipt must carry only the final
781 // reply, not the cumulative stream output.
782 let messages = vec![
783 Message {
784 role: Role::User,
785 content: vec![ContentBlock::Text {
786 text: "write the report".to_string(),
787 cache_control: None,
788 }],
789 },
790 Message {
791 role: Role::Assistant,
792 content: vec![ContentBlock::Text {
793 text: "let me check the workspace first".to_string(),
794 cache_control: None,
795 }],
796 },
797 Message {
798 role: Role::User,
799 content: vec![ContentBlock::ToolResult {
800 execution_id: None,
801 tool_use_id: "call-1".to_string(),
802 content: "listed files".to_string(),
803 is_error: Some(false),
804 content_blocks: None,
805 }],
806 },
807 Message {
808 role: Role::Assistant,
809 content: vec![
810 ContentBlock::thinking("final reasoning"),
811 ContentBlock::Text {
812 text: "the final report".to_string(),
813 cache_control: None,
814 },
815 ],
816 },
817 ];
818 assert_eq!(
819 exec_stream_final_answer_text(&messages, true),
820 Some("the final report".to_string())
821 );
822 }
823
824 #[test]
825 fn exec_stream_final_answer_text_never_reuses_a_resumed_turn_reply() {
826 let mut messages = vec![
827 Message {
828 role: Role::User,
829 content: vec![ContentBlock::Text {
830 text: "old prompt".into(),
831 cache_control: None,
832 }],
833 },
834 Message {
835 role: Role::Assistant,
836 content: vec![ContentBlock::Text {
837 text: "old reply must not be delivered again".into(),
838 cache_control: None,
839 }],
840 },
841 ];
842 // The synchronization event can precede acceptance of the new prompt.
843 assert_eq!(exec_stream_final_answer_text(&messages, false), None);
844 messages.push(Message {
845 role: Role::User,
846 content: vec![ContentBlock::Text {
847 text: "new prompt that fails before an answer".into(),
848 cache_control: None,
849 }],
850 });
851 assert_eq!(exec_stream_final_answer_text(&messages, true), None);
852 messages.push(Message {
853 role: Role::InterruptedAssistant,
854 content: vec![ContentBlock::Text {
855 text: "current partial reply".into(),
856 cache_control: None,
857 }],
858 });
859 assert_eq!(
860 exec_stream_final_answer_text(&messages, true).as_deref(),
861 Some("current partial reply")
862 );
863 // Tool results are user-role records inside this same turn.
864 messages.push(Message {
865 role: Role::User,
866 content: vec![ContentBlock::ToolResult {
867 execution_id: None,
868 tool_use_id: "call-current".into(),
869 content: "result".into(),
870 is_error: None,
871 content_blocks: None,
872 }],
873 });
874 assert_eq!(
875 exec_stream_final_answer_text(&messages, true).as_deref(),
876 Some("current partial reply")
877 );
878 }
879
880 #[test]
881 fn exec_stream_final_answer_text_requires_assistant_text() {
882 assert_eq!(exec_stream_final_answer_text(&[], true), None);
883 let user_only = vec![Message {
884 role: Role::User,
885 content: vec![ContentBlock::Text {
886 text: "prompt".to_string(),
887 cache_control: None,
888 }],
889 }];
890 assert_eq!(exec_stream_final_answer_text(&user_only, true), None);
891 let textless_assistant = vec![Message {
892 role: Role::Assistant,
893 content: vec![ContentBlock::thinking("reasoning only")],
894 }];
895 assert_eq!(
896 exec_stream_final_answer_text(&textless_assistant, true),
897 None
898 );
899 }
900
901 #[test]
902 fn exec_stream_input_analysis_reports_prompt_composition() {
903 let system = SystemPrompt::Text("system rules".to_string());
904 let messages = vec![
905 Message {
906 role: Role::User,
907 content: vec![ContentBlock::Text {
908 text: "run tests".to_string(),
909 cache_control: None,
910 }],
911 },
912 Message {
913 role: Role::Assistant,
914 content: vec![
915 ContentBlock::thinking("checking context"),
916 ContentBlock::Text {
917 text: "working".to_string(),
918 cache_control: None,
919 },
920 ContentBlock::ToolUse {
921 execution_id: None,
922 id: "call-1".to_string(),
923 name: "exec_shell".to_string(),
924 input: serde_json::json!({"command": "cargo test"}),
925 caller: None,
926 thought_signature: None,
927 },
928 ],
929 },
930 Message {
931 role: Role::User,
932 content: vec![ContentBlock::ToolResult {
933 execution_id: None,
934 tool_use_id: "call-1".to_string(),
935 content: "stdout line\nstderr line".to_string(),
936 is_error: Some(false),
937 content_blocks: Some(vec![serde_json::json!({
938 "type": "text",
939 "text": "structured output"
940 })]),
941 }],
942 },
943 ];
944
945 let analysis = exec_stream_input_analysis(&messages, Some(&system));
946
947 assert_eq!(analysis.user_message_count, 2);
948 assert_eq!(analysis.assistant_message_count, 1);
949 assert_eq!(analysis.tool_message_count, 0);
950 assert_eq!(analysis.tool_use_count, 1);
951 assert_eq!(analysis.tool_result_count, 1);
952 assert_eq!(analysis.thinking_chars, "checking context".chars().count());
953 assert!(analysis.text_chars >= "run testsworking".chars().count());
954 assert!(analysis.tool_use_input_chars > 0);
955 assert!(analysis.tool_result_chars >= "stdout line\nstderr line".chars().count());
956 assert!(analysis.estimated_system_tokens > 0);
957 assert!(analysis.estimated_message_content_tokens > 0);
958 assert!(
959 analysis.estimated_request_tokens
960 >= analysis.estimated_system_tokens
961 + analysis.estimated_message_content_tokens
962 + analysis.estimated_framing_tokens
963 );
964 }
965
966 #[test]
967 fn review_receipt_check_public_json_omits_private_details() {
968 let validation = crate::tools::review::ReviewReceiptValidation {
969 passed: false,
970 reason: "secret reason with /tmp/private/receipt.json".to_string(),
971 diff_fingerprint: "sha256:current".to_string(),
972 receipt_fingerprint: Some("sha256:current".to_string()),
973 receipt_path: Some(PathBuf::from("/tmp/private/receipt.json")),
974 unresolved_risk: Some(crate::tools::review::ReviewReceiptRisk {
975 unresolved: true,
976 level: "error".to_string(),
977 summary: "secret summary".to_string(),
978 }),
979 };
980
981 let public = review_receipt_validation_public_json(&validation);
982 let encoded = serde_json::to_string(&public).expect("public json");
983
984 assert_eq!(public["passed"], false);
985 assert_eq!(public["status"], "unresolved_risk");
986 assert_eq!(public["risk_level"], "error");
987 assert!(!encoded.contains("secret"));
988 assert!(!encoded.contains("/tmp/private"));
989 }
990
991 #[test]
992 fn exec_text_session_breadcrumbs_use_compact_ids() {
993 let session_id = "1234567890abcdef";
994
995 assert_eq!(exec_saved_session_line(session_id), "session: 12345678");
996 assert_eq!(
997 exec_resumed_session_line(session_id),
998 "resumed session: 12345678"
999 );
1000 assert!(!exec_saved_session_line(session_id).contains(session_id));
1001 assert!(!exec_resumed_session_line(session_id).contains(session_id));
1002 }
1003
1004 #[test]
1005 fn alternate_screen_defaults_on_in_auto_mode() {
1006 let cli = parse_cli(&["codewhale"]);
1007 let config = Config::default();
1008
1009 assert_eq!(startup_screen_mode(&cli, &config), ScreenMode::Fullscreen);
1010 }
1011
1012 #[test]
1013 fn screen_mode_round_trips_through_its_own_vocabulary() {
1014 for mode in [ScreenMode::Fullscreen, ScreenMode::Inline] {
1015 assert_eq!(ScreenMode::parse(mode.as_str()), Some(mode));
1016 }
1017 // The `tui.alternate_screen` words the config file already documents.
1018 assert_eq!(ScreenMode::parse("auto"), Some(ScreenMode::Fullscreen));
1019 assert_eq!(ScreenMode::parse("always"), Some(ScreenMode::Fullscreen));
1020 assert_eq!(ScreenMode::parse("never"), Some(ScreenMode::Inline));
1021 assert_eq!(ScreenMode::parse(" INLINE "), Some(ScreenMode::Inline));
1022 assert_eq!(ScreenMode::parse("sideways"), None);
1023 assert!(ScreenMode::Fullscreen.uses_alt_screen());
1024 assert!(!ScreenMode::Inline.uses_alt_screen());
1025 }
1026
1027 #[test]
1028 fn removed_no_alt_screen_flag_is_rejected() {
1029 // Negative test: the retired compatibility flag must not be silently
1030 // accepted and must not reach the alternate-screen decision at all.
1031 let error = Cli::try_parse_from(["codewhale", "--no-alt-screen"])
1032 .expect_err("--no-alt-screen must no longer parse");
1033 assert_eq!(
1034 error.kind(),
1035 clap::error::ErrorKind::UnknownArgument,
1036 "retired flag should fail as an unknown argument, not be absorbed"
1037 );
1038 }
1039
1040 #[test]
1041 fn config_never_selects_the_inline_screen() {
1042 let cli = parse_cli(&["codewhale"]);
1043 let config = Config {
1044 tui: Some(crate::config::TuiConfig {
1045 alternate_screen: Some("never".to_string()),
1046 mouse_capture: None,
1047 selection_copy_markdown: None,
1048 stream_chunk_timeout_secs: None,
1049 max_model_steps: None,
1050 turn_wall_clock_secs: None,
1051 stream_max_content_mb: None,
1052 stream_max_duration_secs: None,
1053 stream_max_resumes: None,
1054 stream_max_transparent_retries: None,
1055 stream_max_errors: None,
1056 stream_open_timeout_secs: None,
1057 connect_timeout_secs: None,
1058 force_http1: None,
1059 status_items: None,
1060 posture_bar: None,
1061 metrics_line: None,
1062 osc8_links: None,
1063 composer_arrows_scroll: None,
1064 notification_condition: None,
1065 }),
1066 ..Config::default()
1067 };
1068
1069 assert_eq!(startup_screen_mode(&cli, &config), ScreenMode::Inline);
1070 }
1071
1072 #[test]
1073 #[cfg(not(windows))]
1074 fn mouse_capture_defaults_on_when_alternate_screen_is_active() {
1075 let cli = parse_cli(&["codewhale"]);
1076 let config = Config::default();
1077
1078 assert!(should_use_mouse_capture_with(
1079 &cli, &config, true, None, None, None
1080 ));
1081 }
1082
1083 #[test]
1084 #[cfg(windows)]
1085 fn mouse_capture_defaults_off_on_legacy_windows_console() {
1086 // Legacy conhost (no `WT_SESSION` and no `ConEmuPID`) keeps the
1087 // v0.8.x default-off behavior: mouse-mode reporting on legacy console
1088 // can leak SGR escapes into the composer.
1089 let cli = parse_cli(&["codewhale"]);
1090 let config = Config::default();
1091
1092 assert!(!should_use_mouse_capture_with(
1093 &cli, &config, true, None, None, None
1094 ));
1095 }
1096
1097 // #1169: Windows Terminal sets `WT_SESSION` and handles mouse-mode
1098 // reporting cleanly, so default-on there gives users in-app text
1099 // selection (and the side-effect of clamping selection to the
1100 // transcript region instead of the terminal painting across the
1101 // sidebar via native selection).
1102 #[test]
1103 #[cfg(windows)]
1104 fn mouse_capture_defaults_on_in_windows_terminal() {
1105 let cli = parse_cli(&["codewhale"]);
1106 let config = Config::default();
1107
1108 assert!(should_use_mouse_capture_with(
1109 &cli,
1110 &config,
1111 true,
1112 None,
1113 Some("{a3a3b3a8-aa00-0000-0000-000000000000}"),
1114 None,
1115 ));
1116 }
1117
1118 // ConEmu/Cmder sets `ConEmuPID` and handles VT mouse-mode reporting
1119 // cleanly; default mouse capture on there so users get in-app scrolling.
1120 #[test]
1121 #[cfg(windows)]
1122 fn mouse_capture_defaults_on_in_conemu() {
1123 let cli = parse_cli(&["codewhale"]);
1124 let config = Config::default();
1125
1126 assert!(should_use_mouse_capture_with(
1127 &cli,
1128 &config,
1129 true,
1130 None,
1131 None,
1132 Some("12345"),
1133 ));
1134 }
1135
1136 #[test]
1137 fn no_mouse_capture_flag_disables_mouse_capture() {
1138 let cli = parse_cli(&["codewhale", "--no-mouse-capture"]);
1139 let config = Config::default();
1140
1141 assert!(!should_use_mouse_capture_with(
1142 &cli, &config, true, None, None, None
1143 ));
1144 }
1145
1146 #[test]
1147 fn config_can_disable_default_mouse_capture() {
1148 let cli = parse_cli(&["codewhale"]);
1149 let config = Config {
1150 tui: Some(crate::config::TuiConfig {
1151 alternate_screen: None,
1152 mouse_capture: Some(false),
1153 selection_copy_markdown: None,
1154 stream_chunk_timeout_secs: None,
1155 max_model_steps: None,
1156 turn_wall_clock_secs: None,
1157 stream_max_content_mb: None,
1158 stream_max_duration_secs: None,
1159 stream_max_resumes: None,
1160 stream_max_transparent_retries: None,
1161 stream_max_errors: None,
1162 stream_open_timeout_secs: None,
1163 connect_timeout_secs: None,
1164 force_http1: None,
1165 status_items: None,
1166 posture_bar: None,
1167 metrics_line: None,
1168 osc8_links: None,
1169 composer_arrows_scroll: None,
1170 notification_condition: None,
1171 }),
1172 ..Config::default()
1173 };
1174
1175 assert!(!should_use_mouse_capture_with(
1176 &cli, &config, true, None, None, None
1177 ));
1178 }
1179
1180 #[test]
1181 fn mouse_capture_flag_enables_mouse_capture() {
1182 let cli = parse_cli(&["codewhale", "--mouse-capture"]);
1183 let config = Config::default();
1184
1185 assert!(should_use_mouse_capture_with(
1186 &cli, &config, true, None, None, None
1187 ));
1188 }
1189
1190 #[test]
1191 fn config_can_enable_mouse_capture() {
1192 let cli = parse_cli(&["codewhale"]);
1193 let config = Config {
1194 tui: Some(crate::config::TuiConfig {
1195 alternate_screen: None,
1196 mouse_capture: Some(true),
1197 selection_copy_markdown: None,
1198 stream_chunk_timeout_secs: None,
1199 max_model_steps: None,
1200 turn_wall_clock_secs: None,
1201 stream_max_content_mb: None,
1202 stream_max_duration_secs: None,
1203 stream_max_resumes: None,
1204 stream_max_transparent_retries: None,
1205 stream_max_errors: None,
1206 stream_open_timeout_secs: None,
1207 connect_timeout_secs: None,
1208 force_http1: None,
1209 status_items: None,
1210 posture_bar: None,
1211 metrics_line: None,
1212 osc8_links: None,
1213 composer_arrows_scroll: None,
1214 notification_condition: None,
1215 }),
1216 ..Config::default()
1217 };
1218
1219 assert!(should_use_mouse_capture_with(
1220 &cli, &config, true, None, None, None
1221 ));
1222 }
1223
1224 #[test]
1225 fn mouse_capture_is_off_without_alternate_screen() {
1226 let cli = parse_cli(&["codewhale", "--mouse-capture"]);
1227 let config = Config::default();
1228
1229 assert!(!should_use_mouse_capture_with(
1230 &cli, &config, false, None, None, None
1231 ));
1232 }
1233
1234 // Issue #878 / #898: JetBrains JediTerm advertises mouse support but
1235 // forwards SGR mouse-event escapes as raw input characters, producing
1236 // the "input box auto-fills with garbled characters when I move the
1237 // mouse" failure mode in PyCharm/IDEA terminals. Default the capture
1238 // off when we see TERMINAL_EMULATOR=JetBrains-JediTerm; explicit
1239 // config / --mouse-capture still wins.
1240
1241 #[test]
1242 fn mouse_capture_defaults_off_in_jetbrains_jediterm() {
1243 let cli = parse_cli(&["codewhale"]);
1244 let config = Config::default();
1245
1246 assert!(!should_use_mouse_capture_with(
1247 &cli,
1248 &config,
1249 true,
1250 Some("JetBrains-JediTerm"),
1251 None,
1252 None,
1253 ));
1254 }
1255
1256 #[test]
1257 fn jetbrains_default_off_is_case_insensitive() {
1258 let cli = parse_cli(&["codewhale"]);
1259 let config = Config::default();
1260
1261 // JetBrains has occasionally varied the casing across releases;
1262 // a case-insensitive match keeps the protection in place.
1263 assert!(!should_use_mouse_capture_with(
1264 &cli,
1265 &config,
1266 true,
1267 Some("jetbrains-jediterm"),
1268 None,
1269 None,
1270 ));
1271 }
1272
1273 #[test]
1274 fn mouse_capture_flag_overrides_jetbrains_default() {
1275 let cli = parse_cli(&["codewhale", "--mouse-capture"]);
1276 let config = Config::default();
1277
1278 assert!(should_use_mouse_capture_with(
1279 &cli,
1280 &config,
1281 true,
1282 Some("JetBrains-JediTerm"),
1283 None,
1284 None,
1285 ));
1286 }
1287
1288 #[test]
1289 fn config_mouse_capture_true_overrides_jetbrains_default() {
1290 let cli = parse_cli(&["codewhale"]);
1291 let config = Config {
1292 tui: Some(crate::config::TuiConfig {
1293 alternate_screen: None,
1294 mouse_capture: Some(true),
1295 selection_copy_markdown: None,
1296 stream_chunk_timeout_secs: None,
1297 max_model_steps: None,
1298 turn_wall_clock_secs: None,
1299 stream_max_content_mb: None,
1300 stream_max_duration_secs: None,
1301 stream_max_resumes: None,
1302 stream_max_transparent_retries: None,
1303 stream_max_errors: None,
1304 stream_open_timeout_secs: None,
1305 connect_timeout_secs: None,
1306 force_http1: None,
1307 status_items: None,
1308 posture_bar: None,
1309 metrics_line: None,
1310 osc8_links: None,
1311 composer_arrows_scroll: None,
1312 notification_condition: None,
1313 }),
1314 ..Config::default()
1315 };
1316
1317 assert!(should_use_mouse_capture_with(
1318 &cli,
1319 &config,
1320 true,
1321 Some("JetBrains-JediTerm"),
1322 None,
1323 None,
1324 ));
1325 }
1326
1326 lines RUST