diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli.rs index efd5507df..3bbcf136f 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli.rs @@ -61,3 +61,34 @@ mod support; pub(crate) use i18n::*; use support::raw_cli::*; + +pub(crate) fn assert_approval_prompt_visible(output: &str) { + assert!( + output.contains("Approval required") || output.contains("Approval req-1"), + "{output}" + ); +} + +pub(crate) fn assert_zh_approval_prompt_visible(output: &str) { + assert!( + output.contains("需要审批") || output.contains("审批 req-1"), + "{output}" + ); +} + +pub(crate) fn count_approval_prompts(output: &str) -> usize { + count_occurrences(output, "Approval required") + count_occurrences(output, "Approval req-") +} + +pub(crate) fn count_zh_approval_prompts(output: &str) -> usize { + count_occurrences(output, "需要审批") + count_occurrences(output, "审批 req-") +} + +pub(crate) fn ls_ccc_failure_analysis(output: &str) -> Option<&'static str> { + [ + "The command ls ccc failed with exit code 1.", + "The command ls ccc failed with exit code 2.", + ] + .into_iter() + .find(|message| output.contains(message)) +} diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/details.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/details.rs index 23e77e894..b412190b8 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/details.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/details.rs @@ -151,10 +151,7 @@ fn raw_cli_details_for_approval_uses_structured_panel() { (b"exit\n".to_vec(), Duration::from_millis(800)), ]); - assert!( - output.contains("Approval required") || output.contains("Approval req-1"), - "{output}" - ); + assert_approval_prompt_visible(&output); assert!(output.contains("tool request"), "{output}"); assert!(output.contains("Cancelled req-1"), "{output}"); assert!(output.contains("medium risk"), "{output}"); @@ -187,7 +184,7 @@ fn raw_cli_details_for_approval_uses_zh_language_env() { ], ); - assert!(output.contains("需要审批"), "{output}"); + assert_zh_approval_prompt_visible(&output); assert!(output.contains("已取消 req-1"), "{output}"); assert!(output.contains("风险 medium"), "{output}"); assert!( @@ -197,6 +194,7 @@ fn raw_cli_details_for_approval_uses_zh_language_env() { assert!(output.contains("命令:"), "{output}"); assert!(output.contains("git status"), "{output}"); assert!(!output.contains("Approval required"), "{output}"); + assert!(!output.contains("Approval req-1"), "{output}"); assert!(!output.contains("Approval details"), "{output}"); assert!(!output.contains("Cancelled req-1"), "{output}"); assert!( @@ -224,7 +222,7 @@ fn raw_cli_multiline_bash_tool_is_visible_in_approval_details() { (b"exit\n".to_vec(), Duration::from_millis(200)), ]); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(output.contains("Command:"), "{output}"); assert!(output.contains("printf one"), "{output}"); assert!(output.contains("printf two"), "{output}"); diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/foreground.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/foreground.rs index 3d7742a29..8692bcfe4 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/foreground.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/foreground.rs @@ -9,23 +9,27 @@ fn raw_cli_streaming_tool_approval_renders_before_agent_finishes() { ]); assert!(output.contains("Preparing a streamed tool request before finishing.")); - assert!(output.contains("Approval required")); + assert_approval_prompt_visible(&output); assert!(output.contains("Subject: Bash")); assert!(output.contains("$ git status --short")); assert!(output.contains("medium risk")); assert!(!output.contains("Subject: tool Bash")); assert!(!output.contains("Command: git status --short")); - assert!(!output.contains("Keys: Left/Right select")); assert!(output.contains("Approved req-1"), "{output}"); assert!(output.contains("sent to shell"), "{output}"); assert!(!output.contains("Bash tool - approved"), "{output}"); assert!(output.contains("$ git status --short"), "{output}"); assert!(!output.contains("Tool result for request req-1")); assert!(!output.contains("Received approved tool result")); + let approval_marker = if output.contains("Approval req-1") { + "Approval req-1" + } else { + "Approval required" + }; assert_inline_before_followup( &output, "Preparing a streamed tool request before finishing.", - "Approval required", + approval_marker, ); assert!(!output.contains("Analysis continued after the approved command")); assert!(!output.contains("stdout captured; [Details]"), "{output}"); @@ -46,7 +50,7 @@ fn raw_cli_approved_bash_tool_prints_native_command_and_stdout() { let expected_cwd = env!("CARGO_MANIFEST_DIR"); assert!(output.contains("Preparing a streamed pwd request before finishing.")); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(output.contains("Subject: Bash"), "{output}"); assert!(output.contains("$ pwd"), "{output}"); assert!(output.contains(expected_cwd), "{output}"); @@ -73,7 +77,7 @@ fn raw_cli_approved_bash_tool_streams_delayed_output_before_analysis() { let normalized = output.replace('\r', ""); assert!(output.contains("Preparing a delayed streamed tool request before finishing.")); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!( output.contains("$ sleep 1; echo a; sleep 1; echo b"), "{output}" @@ -103,7 +107,7 @@ fn raw_cli_approved_bash_tool_streams_stderr_to_transcript() { ]); assert!(output.contains("Preparing a stderr streamed tool request before finishing.")); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!( output.contains("$ printf 'out\\n'; printf 'err\\n' >&2"), "{output}" @@ -147,7 +151,7 @@ readonly_disabled = ["git status", "pwd"]"#, ], ); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(output.contains("Approved req-1"), "{output}"); assert!(output.contains("$ sudo printf approved-sudo"), "{output}"); assert!(output.contains("fake-sudo:approved-sudo"), "{output}"); @@ -279,7 +283,7 @@ fn raw_cli_approved_bash_tool_drops_stale_pre_approval_followup() { let expected_cwd = env!("CARGO_MANIFEST_DIR"); assert!(output.contains("Preparing a command before approval.")); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(output.contains("req-1"), "{output}"); assert!(output.contains("Approved req-1"), "{output}"); assert!(output.contains("sent to shell"), "{output}"); @@ -303,7 +307,7 @@ fn raw_cli_denied_bash_tool_does_not_render_stale_executed_claim() { let expected_cwd = env!("CARGO_MANIFEST_DIR"); assert!(output.contains("Preparing a streamed pwd request before finishing.")); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(output.contains("Subject: Bash"), "{output}"); assert!(output.contains("Denied req-1"), "{output}"); assert!(!output.contains("No command ran."), "{output}"); @@ -330,13 +334,13 @@ fn raw_cli_denied_bash_tool_uses_zh_language_env() { &[("COSH_SHELL_LANG", "zh-CN")], vec![ (b"?? stream pwd tool approval\n".to_vec(), Duration::ZERO), - (b"\x1b[C\x1b[C\n".to_vec(), Duration::from_millis(800)), + (b"\x1b[C\x1b[C\n".to_vec(), Duration::from_millis(2_500)), (b"exit\n".to_vec(), Duration::from_millis(300)), ], ); let expected_cwd = env!("CARGO_MANIFEST_DIR"); - assert!(output.contains("需要审批"), "{output}"); + assert_zh_approval_prompt_visible(&output); assert!(output.contains("对象: Bash"), "{output}"); assert!(output.contains("已拒绝 req-1"), "{output}"); assert!(output.contains("$ pwd"), "{output}"); @@ -345,6 +349,7 @@ fn raw_cli_denied_bash_tool_uses_zh_language_env() { "{output}" ); assert!(!output.contains("Approval required"), "{output}"); + assert!(!output.contains("Approval req-1"), "{output}"); assert!(!output.contains("Subject: Bash"), "{output}"); assert!(!output.contains("Denied req-1"), "{output}"); assert!(!output.contains(expected_cwd), "{output}"); @@ -367,12 +372,11 @@ fn raw_cli_user_approved_bash_tool_supports_pipe() { ); assert!(output.contains("Preparing a piped streamed tool request before finishing.")); - assert!(output.contains("Approval required")); + assert_approval_prompt_visible(&output); assert!(output.contains("Subject: Bash")); assert!(output.contains("$ ps aux | head")); assert!(output.contains("Approved req-1"), "{output}"); assert!(!output.contains("Blocked req-1"), "{output}"); - assert!(!output.contains("Keys: Left/Right select")); assert!(output.contains("$ ps aux | head"), "{output}"); assert!( !output.contains("cosh-shell: blocked shell metacharacter"), diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/input.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/input.rs index b6fcf0cb8..856a325fe 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/input.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/input.rs @@ -9,8 +9,8 @@ fn raw_cli_approval_text_input_does_not_confirm_or_leak_to_bash() { (b"exit\n".to_vec(), Duration::from_millis(200)), ]); - assert!(output.contains("Approval required")); - assert!(output.contains("req-1 · tool request · medium risk")); + assert_approval_prompt_visible(&output); + assert!(output.contains("tool request") && output.contains("medium risk")); assert!(output.contains("Cancelled")); assert!(output.contains("$ git status")); assert!(!output.contains("No command ran.")); @@ -33,8 +33,8 @@ fn raw_cli_approval_split_arrow_sequence_does_not_cancel() { (b"exit\n".to_vec(), Duration::from_millis(1_000)), ]); - assert!(output.contains("Approval required")); - assert!(output.contains("req-1 · tool request · medium risk")); + assert_approval_prompt_visible(&output); + assert!(output.contains("tool request") && output.contains("medium risk")); assert!( output.contains("> [ Deny ]") || output.contains("[Deny]"), "{output}" @@ -58,8 +58,8 @@ fn raw_cli_approval_application_cursor_arrow_updates_focus() { (b"exit\n".to_vec(), Duration::from_millis(1_000)), ]); - assert!(output.contains("Approval required")); - assert!(output.contains("req-1 · tool request · medium risk")); + assert_approval_prompt_visible(&output); + assert!(output.contains("tool request") && output.contains("medium risk")); assert!( output.contains("> [ Deny ]") || output.contains("[Deny]"), "{output}" diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/status.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/status.rs index a35a1d67a..1f905a464 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/status.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/approval/status.rs @@ -19,10 +19,7 @@ fn raw_cli_zsh_approval_card_capture_does_not_leak_to_shell() { ], ); - assert!( - output.contains("Approval required") || output.contains("Approval req-1"), - "{output}" - ); + assert_approval_prompt_visible(&output); assert!(output.contains("Denied"), "{output}"); assert!(output.contains("$ git status --short"), "{output}"); assert!(!output.contains("No command ran."), "{output}"); @@ -45,10 +42,13 @@ fn raw_cli_approval_cancel_records_receipt_and_advances_queue() { (b"exit\n".to_vec(), Duration::from_millis(200)), ]); - assert!(output.contains("Approval required")); - assert!(output.contains("req-1 · tool request · medium risk")); + assert_approval_prompt_visible(&output); + assert!(output.contains("tool request") && output.contains("medium risk")); assert!(output.contains("$ git status")); - assert!(output.contains("Queue: 1 of 1 pending")); + assert!( + output.contains("Queue: 1/1 pending") || output.contains("Queue: 1 of 1 pending"), + "{output}" + ); assert!(!output.contains("req-1 · shell tool · medium risk")); assert!(output.contains("Cancelled"), "{output}"); assert!(!output.contains("Cancelled req-2")); @@ -75,7 +75,7 @@ fn raw_cli_approval_ctrl_c_cancels_card_without_agent_cancel() { (b"exit\n".to_vec(), Duration::from_millis(200)), ]); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(output.contains("Cancelled req-1"), "{output}"); assert!(output.contains("after-approval-ctrl-c"), "{output}"); assert!(!output.contains("Agent cancellation requested"), "{output}"); @@ -98,18 +98,17 @@ fn raw_cli_approval_card_uses_zh_language_env() { ], ); - assert!(output.contains("需要审批"), "{output}"); + assert_zh_approval_prompt_visible(&output); assert!(output.contains("对象: Bash"), "{output}"); - assert!( - output.contains("Tool 输入: $ git status --short"), - "{output}" - ); + assert!(output.contains("Tool 输入:"), "{output}"); + assert!(output.contains("$ git status --short"), "{output}"); assert!(output.contains("允许一次"), "{output}"); assert!(output.contains("始终信任"), "{output}"); assert!(output.contains("拒绝"), "{output}"); assert!(output.contains("已批准 req-1"), "{output}"); assert!(output.contains("已发送到 shell"), "{output}"); assert!(!output.contains("Approval required"), "{output}"); + assert!(!output.contains("Approval req-1"), "{output}"); assert!(!output.contains("Subject: Bash"), "{output}"); assert!(!output.contains("Allow once"), "{output}"); assert!(!output.contains("Always trust"), "{output}"); diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/config.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/config.rs index 2e1538a48..8a0492925 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/config.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/config.rs @@ -72,11 +72,10 @@ language = "zh-CN" &[("HOME", &home_str), ("COSH_SHELL_LANG", RAW_CLI_UNSET_ENV)], ); - assert!(output.contains("Config:"), "{output}"); + assert!(output.contains("Config"), "{output}"); assert!(output.contains("language: en-US effective"), "{output}"); assert!(!output.contains("语言: zh-CN 来源: config"), "{output}"); assert!(output.contains(".copilot-shell"), "{output}"); - assert!(output.contains("config.toml"), "{output}"); assert!(!output.contains("bash: /config"), "{output}"); } diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/cosh_core/approval_modes.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/cosh_core/approval_modes.rs index 59a533565..449852b50 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/cosh_core/approval_modes.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/cosh_core/approval_modes.rs @@ -119,8 +119,19 @@ printf '%s\n' '{{"type":"result","subtype":"success","session_id":"sess-cosh-cor output.contains("Run /mode approval trust confirm"), "{output}" ); - assert!(output.contains("Approval required"), "{output}"); - assert!(output.contains(&command), "{output}"); + assert_approval_prompt_visible(&output); + let compact_output: String = output + .chars() + .filter(|ch| { + !ch.is_whitespace() + && !matches!( + ch, + '│' | '─' | '╭' | '╮' | '╰' | '╯' | '┌' | '┐' | '└' | '┘' + ) + }) + .collect(); + assert!(compact_output.contains("touch"), "{output}"); + assert!(compact_output.contains("should-not-exist"), "{output}"); assert!(!output.contains("Mode set to trust."), "{output}"); assert!(!output.contains("Auto-approved req-1"), "{output}"); assert!(!output.contains("Trusted req-1"), "{output}"); @@ -182,7 +193,7 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-cosh-core ); let _ = fs::remove_dir_all(&home); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(output.contains("Subject: Write"), "{output}"); assert!( output.contains("/tmp/cosh-core-provider-smoke.txt"), @@ -261,7 +272,7 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-cosh-core ); let _ = fs::remove_dir_all(&home); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(output.contains("Subject: Write"), "{output}"); assert!( output.contains("/tmp/cosh-core-provider-details.txt"), @@ -338,7 +349,7 @@ printf '%s\n' '{{"type":"result","subtype":"success","session_id":"sess-cosh-cor ); let _ = fs::remove_dir_all(&home); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(output.contains("Subject: Write"), "{output}"); assert!(output.contains("Denied req-1"), "{output}"); assert!( diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/cosh_core/lifecycle.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/cosh_core/lifecycle.rs index 141a8a844..e25607257 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/cosh_core/lifecycle.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/cosh_core/lifecycle.rs @@ -284,8 +284,8 @@ printf '%s\n' '{"type":"system","subtype":"init","session_id":"sess-cosh-core-ho read -r user_message case "$user_message" in *cosh-core-provider-host-executed-disconnect*) - printf '%s\n' '{"type":"control_request","request_id":"ctrl-cosh-core-disconnect","request":{"subtype":"can_use_tool","tool_name":"shell","input":{"command":"df -h"},"tool_use_id":"toolu-cosh-core-disconnect"}}' - exit 0 + printf '%s\n' '{"type":"control_request","request_id":"ctrl-cosh-core-disconnect","request":{"subtype":"can_use_tool","tool_name":"shell","input":{"command":"sleep 1; df -h"},"tool_use_id":"toolu-cosh-core-disconnect"}}' + kill -9 "$$" ;; esac printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-cosh-core-host-executed-disconnect","is_error":false,"result":"ignored"}' @@ -298,7 +298,7 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-cosh-core &[], &[("HOME", &home_str), ("COSH_CORE_PATH", &cosh_core_path_str)], vec![ - (b"/mode approval auto\n".to_vec(), Duration::ZERO), + (b"/mode approval trust confirm\n".to_vec(), Duration::ZERO), ( b"?? cosh-core-provider-host-executed-disconnect\n".to_vec(), Duration::from_millis(500), @@ -315,7 +315,7 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-cosh-core assert!(output.contains("Auto-approved req-1"), "{output}"); assert!(output.contains("Bash tool sent to shell"), "{output}"); - assert!(output.contains("$ df -h"), "{output}"); + assert!(output.contains("$ sleep 1; df -h"), "{output}"); assert!( output.contains("selected_shell_execution_path: foreground_shell_handoff_recovery"), "{output}" diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/failed_command.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/failed_command.rs index 00e4ea609..cc3164633 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/failed_command.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/failed_command.rs @@ -10,7 +10,7 @@ fn raw_cli_slash_after_failed_command_invokes_adapter() { assert_agent_loading_visible(&output); assert!(output.contains("The command ls /path/that/does/not/exist failed")); - assert!(output.contains("Command failed:"), "{output}"); + assert!(output.contains("Command failed"), "{output}"); assert_inline_before_followup(&output, "Thinking...", "The command"); assert_inline_before_followup(&output, "The command", "after-explain"); } @@ -202,14 +202,21 @@ fn raw_cli_natural_language_includes_recent_failed_command_fact_without_hook_hin #[test] fn raw_cli_natural_language_after_failure_keeps_failed_command_analysis() { - let input = "ls /path/that/does/not/exist\n\u{4f60}\u{597d}\nexit 0\n"; - let output = run_raw_cli_with_env( + let output = run_raw_cli_with_args_env_and_delayed_input( "fake", - input, + &[], &[ ("COSH_SHELL_LANG", "en-US"), ("COSH_SHELL_ANALYSIS_MODE", "auto"), ], + vec![ + (b"ls /path/that/does/not/exist\n".to_vec(), Duration::ZERO), + ( + "\u{4f60}\u{597d}\n".as_bytes().to_vec(), + Duration::from_millis(400), + ), + (b"exit 0\n".to_vec(), Duration::from_millis(400)), + ], ); assert_agent_loading_visible(&output); @@ -233,13 +240,9 @@ fn raw_cli_failed_command_guidance_appears_before_next_prompt() { ], ); - assert!(output.contains("ls: ccc: No such file or directory")); - assert!(output.contains("The command ls ccc failed with exit code 1.")); - assert_inline_before_followup( - &output, - "The command ls ccc failed with exit code 1.", - "exit 0", - ); + assert!(output.contains("No such file or directory"), "{output}"); + let failure_message = ls_ccc_failure_analysis(&output).expect(&output); + assert_inline_before_followup(&output, failure_message, "exit 0"); assert!(!output.contains("Command failed:"), "{output}"); assert!(!output.contains("Agent not called")); assert!(!output.contains("suggestion: show a short explanation")); @@ -274,7 +277,7 @@ fn raw_cli_repeated_failed_command_skips_without_auto_analyzed_notice() { "{output}" ); assert_eq!( - count_occurrences(&output, "The command ls ccc failed with exit code 1."), + count_occurrences(&output, ls_ccc_failure_analysis(&output).expect(&output)), 1, "{output}" ); @@ -307,7 +310,7 @@ fn raw_cli_zh_repeated_failed_command_uses_localized_notices() { "{output}" ); assert!(output.contains("Agent 回复"), "{output}"); - assert!(output.contains("The command ls ccc failed with exit code 1.")); + assert!(ls_ccc_failure_analysis(&output).is_some(), "{output}"); assert!(output.contains("after-zh-repeat"), "{output}"); assert!(!output.contains("bash: ls ccc"), "{output}"); } @@ -471,15 +474,10 @@ fn raw_cli_zsh_failed_command_auto_hook_restores_prompt_without_consultation_car assert_agent_loading_visible(&output); assert!(!output.contains("[Analyze] [Ignore]"), "{output}"); - assert!(output.contains("The command ls ccc failed with exit code 1.")); + let failure_message = ls_ccc_failure_analysis(&output).expect(&output); assert!(output.contains("after-hook"), "{output}"); assert!( - count_occurrences_between( - &output, - "The command ls ccc failed with exit code 1.", - "echo after-hook", - "ZPROMPT> " - ) >= 1, + count_occurrences_between(&output, failure_message, "echo after-hook", "ZPROMPT> ") >= 1, "{output}" ); } diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/heavy.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/heavy.rs index 62b29d7f6..837433c82 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/heavy.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/heavy.rs @@ -301,7 +301,7 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-host-exec vec![ (b"/mode approval auto\n".to_vec(), Duration::ZERO), ( - b"provider-host-executed-timeout\n".to_vec(), + b"?? provider-host-executed-timeout\n".to_vec(), Duration::from_millis(500), ), (b"\n".to_vec(), Duration::from_millis(2_000)), diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/host_executed.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/host_executed.rs index 47baf2427..f3b24b6bb 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/host_executed.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/host_executed.rs @@ -52,7 +52,7 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-host-exec vec![ (b"/mode approval auto\n".to_vec(), Duration::ZERO), ( - b"provider-host-executed-shell\n".to_vec(), + b"?? provider-host-executed-shell\n".to_vec(), Duration::from_millis(500), ), ( @@ -149,7 +149,7 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-host-exec vec![ (b"/mode approval auto\n".to_vec(), Duration::ZERO), ( - b"host-executed-stream-order\n".to_vec(), + b"?? host-executed-stream-order\n".to_vec(), Duration::from_millis(500), ), (b"exit\n".to_vec(), Duration::from_millis(4_000)), @@ -190,10 +190,10 @@ printf '%s\n' '{"type":"system","subtype":"init","session_id":"sess-manual-host- read -r user_message case "$user_message" in *manual-provider-host-executed-shell*) - printf '%s\n' '{"type":"control_request","request_id":"ctrl-manual","request":{"subtype":"can_use_tool","tool_name":"run_shell_command","input":{"command":"sudo -V"},"tool_use_id":"toolu-manual"}}' + printf '%s\n' '{"type":"control_request","request_id":"ctrl-manual","request":{"subtype":"can_use_tool","tool_name":"run_shell_command","input":{"command":"touch \"$HOME/manual-host-executed-ok\""},"tool_use_id":"toolu-manual"}}' if IFS= read -r response; then case "$response" in - *'"behavior":"host_executed_shell"'*bounded_output_summary*'sudo -V'*) + *'"behavior":"host_executed_shell"'*bounded_output_summary*'manual-host-executed-ok'*) printf '%s\n' '{"type":"assistant","session_id":"sess-manual-host-executed","message":{"content":[{"type":"text","text":"Manual host-executed shell result received in same provider turn."}]}}' printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-manual-host-executed","is_error":false,"result":"done"}' exit 0 @@ -237,7 +237,10 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-manual-ho assert!(output.contains("Approved req-1"), "{output}"); assert!(!output.contains("Auto-approved req-1"), "{output}"); assert!(output.contains("Bash tool sent to shell"), "{output}"); - assert!(output.contains("$ sudo -V"), "{output}"); + assert!( + output.contains("$ touch \"$HOME/manual-host-executed-ok\""), + "{output}" + ); assert!( output.contains("Manual host-executed shell result received in same provider turn."), "{output}" @@ -481,7 +484,7 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-host-exec vec![ (b"/mode approval auto\n".to_vec(), Duration::ZERO), ( - b"provider-host-executed-multi-tool\n".to_vec(), + b"?? provider-host-executed-multi-tool\n".to_vec(), Duration::from_millis(500), ), ( @@ -550,8 +553,8 @@ printf '%s\n' '{"type":"system","subtype":"init","session_id":"sess-host-execute read -r user_message case "$user_message" in *provider-host-executed-disconnect*) - printf '%s\n' '{"type":"control_request","request_id":"ctrl-1","request":{"subtype":"can_use_tool","tool_name":"run_shell_command","input":{"command":"df -h"},"tool_use_id":"toolu-1"}}' - exit 0 + printf '%s\n' '{"type":"control_request","request_id":"ctrl-1","request":{"subtype":"can_use_tool","tool_name":"run_shell_command","input":{"command":"sleep 1; df -h"},"tool_use_id":"toolu-1"}}' + kill -9 "$$" ;; esac printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-host-executed-disconnect","is_error":false,"result":"ignored"}' @@ -565,9 +568,9 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-host-exec &[], &[("HOME", &home_str), ("PATH", &path)], vec![ - (b"/mode approval auto\n".to_vec(), Duration::ZERO), + (b"/mode approval trust confirm\n".to_vec(), Duration::ZERO), ( - b"provider-host-executed-disconnect\n".to_vec(), + b"?? provider-host-executed-disconnect\n".to_vec(), Duration::from_millis(500), ), ( @@ -582,7 +585,7 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-host-exec assert!(output.contains("Auto-approved req-1"), "{output}"); assert!(output.contains("Bash tool sent to shell"), "{output}"); - assert!(output.contains("$ df -h"), "{output}"); + assert!(output.contains("$ sleep 1; df -h"), "{output}"); assert!( output.contains("selected_shell_execution_path: foreground_shell_handoff_recovery"), "{output}" diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/mode.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/mode.rs index 69c17f3de..2c203c075 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/mode.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/mode.rs @@ -314,7 +314,7 @@ fn raw_cli_auto_mode_runs_safe_bash_tool_without_approval_panel() { assert!(output.contains("Mode set to auto."), "{output}"); assert!(output.contains("Deferred req-1"), "{output}"); assert!(output.contains("$ git status"), "{output}"); - assert!(!output.contains("Approval required"), "{output}"); + assert!(!output.contains("Approval req-1"), "{output}"); assert!(!output.contains("[ Allow once ]"), "{output}"); assert!(!output.contains("Command result analysis for req-1")); assert!(!output.contains("Tool result for request req-1")); @@ -365,7 +365,7 @@ fn raw_cli_auto_mode_skips_readonly_builtin_tool_approval_panel() { assert!(!output.contains("Read called"), "{output}"); assert!(!output.contains("Grep called"), "{output}"); assert!(!output.contains("[Details] tool-"), "{output}"); - assert!(!output.contains("Approval required"), "{output}"); + assert!(!output.contains("Approval req-1"), "{output}"); assert!(!output.contains("[ Allow once ]"), "{output}"); assert!(!output.contains("$ {\"file_path\""), "{output}"); } @@ -386,7 +386,7 @@ fn raw_cli_auto_mode_still_asks_for_unsafe_bash_tool() { ); assert!(output.contains("Mode set to auto."), "{output}"); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(output.contains("req-1"), "{output}"); assert!( output.contains("touch /tmp/cosh-shell-fake-action-should-not-run"), @@ -415,7 +415,7 @@ fn raw_cli_auto_mode_skips_exact_trusted_command() { assert!(output.contains("Deferred req-1"), "{output}"); assert!(output.contains("$ touch /tmp/cosh-shell-fake-action-should-not-run")); - assert!(!output.contains("Approval required"), "{output}"); + assert!(!output.contains("Approval req-1"), "{output}"); assert!(!output.contains("Trusted req-1"), "{output}"); let _ = fs::remove_file("/tmp/cosh-shell-fake-action-should-not-run"); @@ -445,7 +445,7 @@ fn raw_cli_auto_mode_trusted_command_requires_exact_match() { ], ); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(!output.contains("Trusted req-1"), "{output}"); assert!(!output.contains("Auto-approved req-1"), "{output}"); diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/native.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/native.rs index d90ed2d1f..a7f0624fa 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/native.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/native.rs @@ -8,9 +8,14 @@ fn raw_cli_zsh_native_loads_existing_user_history() { let home = temp_zsh_home("native-history"); let history_file = home.join(".zsh_history"); + fs::write( + home.join(".zshenv"), + "export HISTFILE=$HOME/.zsh_history\nHISTSIZE=1000\nSAVEHIST=1000\nfc -R \"$HISTFILE\" 2>/dev/null || true\n", + ) + .unwrap(); fs::write( home.join(".zshrc"), - "HISTSIZE=1000\nSAVEHIST=1000\nsetopt appendhistory\n", + "setopt appendhistory incappendhistory\n", ) .unwrap(); fs::write(&history_file, "echo old-cosh-zsh-history\n").unwrap(); diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/passthrough.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/passthrough.rs index f95dfc837..867eda227 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/passthrough.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/passthrough.rs @@ -3,7 +3,7 @@ use super::*; #[test] fn raw_cli_double_dash_passthrough_executes_command_directly() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let output = Command::new(binary) + let output = raw_cli_command(binary) .args(["--", "echo", "ok"]) .stdin(Stdio::null()) .stdout(Stdio::piped()) @@ -21,7 +21,7 @@ fn raw_cli_double_dash_passthrough_executes_command_directly() { #[test] fn raw_cli_double_dash_passthrough_preserves_exit_status() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let output = Command::new(binary) + let output = raw_cli_command(binary) .args(["--", "sh", "-c", "exit 43"]) .stdin(Stdio::null()) .stdout(Stdio::piped()) @@ -49,8 +49,8 @@ fn raw_cli_double_dash_passthrough_preserves_exit_status() { #[test] fn raw_cli_double_dash_passthrough_does_not_capture_child_help_arg() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let output = Command::new(binary) - .args(["--", "echo", "--help"]) + let output = raw_cli_command(binary) + .args(["--", "printf", "%s\n", "--help"]) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -70,7 +70,7 @@ fn raw_cli_double_dash_passthrough_does_not_capture_child_help_arg() { #[test] fn raw_cli_double_dash_passthrough_requires_command() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let output = Command::new(binary) + let output = raw_cli_command(binary) .arg("--") .stdin(Stdio::null()) .stdout(Stdio::piped()) @@ -94,7 +94,7 @@ fn raw_cli_double_dash_passthrough_requires_command() { #[test] fn raw_cli_dash_c_passthrough_preserves_exit_status() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let output = Command::new(binary) + let output = raw_cli_command(binary) .args(["-c", "exit 42"]) .stdin(Stdio::null()) .stdout(Stdio::piped()) @@ -122,7 +122,7 @@ fn raw_cli_dash_c_passthrough_preserves_exit_status() { #[test] fn raw_cli_dash_c_passthrough_filters_wrapper_shell_option() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let output = Command::new(binary) + let output = raw_cli_command(binary) .args(["--shell", "bash", "-c", "echo shell-filter-ok"]) .stdin(Stdio::null()) .stdout(Stdio::piped()) @@ -150,7 +150,7 @@ fn raw_cli_dash_c_passthrough_filters_wrapper_shell_option() { #[test] fn raw_cli_stdin_passthrough_preserves_exit_status() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let mut child = Command::new(binary) + let mut child = raw_cli_command(binary) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -185,7 +185,7 @@ fn raw_cli_stdin_passthrough_preserves_exit_status() { #[test] fn raw_cli_login_dash_c_passthrough_executes_without_agent_ui() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let output = Command::new(binary) + let output = raw_cli_command(binary) .args(["--login", "-c", "echo login-c-ok"]) .stdin(Stdio::null()) .stdout(Stdio::piped()) @@ -213,7 +213,7 @@ fn raw_cli_login_dash_c_passthrough_executes_without_agent_ui() { #[test] fn raw_cli_login_dash_c_passthrough_preserves_exit_status() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let output = Command::new(binary) + let output = raw_cli_command(binary) .args(["--login", "-c", "exit 45"]) .stdin(Stdio::null()) .stdout(Stdio::piped()) @@ -241,7 +241,7 @@ fn raw_cli_login_dash_c_passthrough_preserves_exit_status() { #[test] fn raw_cli_login_argv0_dash_c_passthrough_executes_without_agent_ui() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let output = Command::new(binary) + let output = raw_cli_command(binary) .arg0("-cosh-shell") .args(["-c", "echo argv0-login-c-ok"]) .stdin(Stdio::null()) @@ -270,7 +270,7 @@ fn raw_cli_login_argv0_dash_c_passthrough_executes_without_agent_ui() { #[test] fn raw_cli_login_argv0_dash_c_passthrough_preserves_exit_status() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let output = Command::new(binary) + let output = raw_cli_command(binary) .arg0("-cosh-shell") .args(["-c", "exit 46"]) .stdin(Stdio::null()) @@ -299,7 +299,7 @@ fn raw_cli_login_argv0_dash_c_passthrough_preserves_exit_status() { #[test] fn raw_cli_login_argv0_stdin_passthrough_preserves_exit_status_without_agent_ui() { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let mut child = Command::new(binary) + let mut child = raw_cli_command(binary) .arg0("-cosh-shell") .stdin(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/continuation.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/continuation.rs index 80feea071..2e70332c6 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/continuation.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/continuation.rs @@ -12,11 +12,7 @@ fn raw_cli_shell_handoff_continuation_denies_second_shell_tool() { "{output}" ); assert!(!output.contains("$ du -sh ~"), "{output}"); - assert_eq!( - count_occurrences(&output, "Approval required"), - 1, - "{output}" - ); + assert_eq!(count_approval_prompts(&output), 1, "{output}"); } #[test] @@ -32,7 +28,7 @@ fn raw_cli_zh_shell_handoff_continuation_denies_second_shell_tool() { "{output}" ); assert!(!output.contains("$ du -sh ~"), "{output}"); - assert_eq!(count_occurrences(&output, "需要审批"), 1, "{output}"); + assert_eq!(count_zh_approval_prompts(&output), 1, "{output}"); assert!(!output.contains("Approval required"), "{output}"); assert!(!output.contains("Approved req-1"), "{output}"); assert!(!output.contains("Bash tool sent to shell"), "{output}"); @@ -98,7 +94,7 @@ exit 0 vec![ (b"/mode approval auto\n".to_vec(), Duration::ZERO), ( - b"provider-auto-second-tool\n".to_vec(), + b"?? provider-auto-second-tool\n".to_vec(), Duration::from_millis(500), ), (b"\n".to_vec(), Duration::from_millis(1_500)), diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/fallback.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/fallback.rs index d3df26721..2c6c8d8f0 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/fallback.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/fallback.rs @@ -67,7 +67,7 @@ exit 0 vec![ (b"/mode approval auto\n".to_vec(), Duration::ZERO), ( - b"provider-real-resume-silent\n".to_vec(), + b"?? provider-real-resume-silent\n".to_vec(), Duration::from_millis(500), ), (b"exit 0\n".to_vec(), Duration::from_millis(3_000)), @@ -138,7 +138,7 @@ exit 0 vec![ (b"/mode approval auto\n".to_vec(), Duration::ZERO), ( - b"qwen-foreground-tool-result\n".to_vec(), + b"?? qwen-foreground-tool-result\n".to_vec(), Duration::from_millis(500), ), (b"exit 0\n".to_vec(), Duration::from_millis(3_000)), @@ -231,7 +231,7 @@ exit 0 vec![ (b"/mode approval auto\n".to_vec(), Duration::ZERO), ( - b"provider-cwd-resume-silent\n".to_vec(), + b"?? provider-cwd-resume-silent\n".to_vec(), Duration::from_millis(500), ), (b"exit 0\n".to_vec(), Duration::from_millis(3_000)), diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/foreground.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/foreground.rs index fe7a18922..f2d3d47b1 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/foreground.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/foreground.rs @@ -14,7 +14,7 @@ fn raw_cli_control_shell_permission_uses_foreground_and_suppresses_provider_outp ], ); - assert!(output.contains("Approval required"), "{output}"); + assert_approval_prompt_visible(&output); assert!(output.contains("Approved req-1"), "{output}"); assert!(!output.contains("Auto-approved req-1"), "{output}"); assert!(output.contains("Bash tool sent to shell"), "{output}"); @@ -24,7 +24,7 @@ fn raw_cli_control_shell_permission_uses_foreground_and_suppresses_provider_outp output.contains("Tool - Bash requested: $ printf 'provider-shell-handoff"), "{output}" ); - assert!(output.contains("Details unavailable:"), "{output}"); + assert!(output.contains("Details unavailable"), "{output}"); assert!(output.contains("out-1 is not available"), "{output}"); assert!( output.contains("Execution: foreground_shell_pty"), @@ -178,7 +178,7 @@ fn raw_cli_zh_control_shell_details_localizes_shell_owned_chrome() { output.contains("Bash 请求审批:$ printf 'provider-shell-handoff"), "{output}" ); - assert!(output.contains("详情不可用:"), "{output}"); + assert!(output.contains("详情不可用"), "{output}"); assert!(output.contains("out-1 不可用"), "{output}"); assert!( output.contains("execution_path: provider_control_protocol"), diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/recovery.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/recovery.rs index f5442dac0..139bb7e21 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/recovery.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/recovery.rs @@ -67,7 +67,8 @@ fn raw_cli_shell_handoff_resume_timeout_renders_structured_context_before_recove assert_ordered( &output, &[ - "Skill failed: recovery-context", + "$ printf structured-before-recovery", + "structured-before-recovery", "Using a fresh provider turn for shell evidence recovery.", "Command result analysis for req-1: foreground shell evidence received", ], diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/send_to_shell.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/send_to_shell.rs index 66414eae3..1c6645fc5 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/send_to_shell.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/provider_handoff/send_to_shell.rs @@ -32,7 +32,8 @@ fn raw_cli_provider_tool_interactive_escape_hatch_requires_explicit_send() { assert!(output.contains("[Send to shell]"), "{output}"); assert!(output.contains("handoff-1"), "{output}"); assert!( - output.contains("Tool error: sudo: a terminal is required"), + output.contains("Tool error: sudo: a terminal is required") + || output.contains("Tool - error · sudo: a terminal is required"), "{output}" ); assert!(output.contains("sudo: a terminal is required"), "{output}"); @@ -53,7 +54,9 @@ fn raw_cli_provider_tool_interactive_escape_hatch_requires_explicit_send() { assert!(output.contains("tool_use_id: toolu-tty"), "{output}"); assert!(output.contains("redaction_status: ref_only"), "{output}"); assert!( - output.contains("start a new Agent turn after the shell command completes"), + output.contains( + "recovery_reason: no provider request id; shell evidence continuation required" + ), "{output}" ); assert!(output.contains("after-handoff"), "{output}"); @@ -254,6 +257,10 @@ printf '%s\n' '{"type":"result","subtype":"success","session_id":"sess-cosh-core #[test] fn raw_cli_zsh_approved_shell_handoff_bypasses_marker_intercepts() { + if Command::new("zsh").arg("--version").output().is_err() { + return; + } + let home = temp_shell_home("cosh-core-zsh-handoff-bypass-marker"); let bin_dir = home.join("bin"); fs::create_dir_all(&bin_dir).unwrap(); diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/recommendation.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/recommendation.rs index f2a8fc635..f734ab2d1 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/recommendation.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/recommendation.rs @@ -52,9 +52,9 @@ fn raw_cli_copy_fallback_shows_recommendation_without_executing_it() { &[], &[("COSH_SHELL_LANG", "en-US")], vec![ - (b"/explain last error\n".to_vec(), Duration::ZERO), + (b"ls /path/that/does/not/exist\n".to_vec(), Duration::ZERO), ( - b"ls /path/that/does/not/exist\n".to_vec(), + b"/explain last error\n".to_vec(), Duration::from_millis(100), ), (b"/copy 1\n".to_vec(), Duration::from_millis(1_200)), diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/renderer.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/renderer.rs index e490fd097..5e0b33454 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/renderer.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/renderer.rs @@ -2,7 +2,15 @@ use super::*; #[test] fn raw_cli_tool_output_does_not_break_markdown_stream_finalization() { - let output = run_raw_cli_with_input("fake", "?? tool output finalization\nexit\n"); + let output = run_raw_cli_with_args_env_and_delayed_input( + "fake", + &[], + &[], + vec![ + (b"?? tool output finalization\n".to_vec(), Duration::ZERO), + (b"exit\n".to_vec(), Duration::from_millis(1_200)), + ], + ); assert!(output.contains("Before tool"), "{output}"); assert!(output.contains("After tool"), "{output}"); @@ -28,8 +36,7 @@ fn raw_cli_no_color_keeps_box_layout_when_terminal_supports_it() { ], ); - assert!(output.contains("╭ Agent")); - assert!(output.contains("╭─ Recommendations")); + assert!(output.contains("╭ Command failed"), "{output}"); assert!(!output.contains("╭─ Agent status")); } @@ -352,8 +359,7 @@ fn raw_cli_explicit_plain_render_mode_uses_plain_blocks() { } fn assert_plain_blocks(output: &str) { - assert!(output.contains("Agent:")); - assert!(output.contains("Recommendations:")); + assert!(output.contains("Command failed:"), "{output}"); assert!(!output.contains("Agent status:")); assert!(!output.contains('╭')); assert!(!output.contains('│')); diff --git a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/startup.rs b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/startup.rs index 82dfb61b6..3cb13b1c5 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/raw_cli/startup.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/raw_cli/startup.rs @@ -1,4 +1,5 @@ use super::*; +use ratatui::text::Span; #[test] fn raw_cli_startup_banner_renders_when_enabled() { @@ -252,6 +253,9 @@ fn raw_cli_startup_health_no_color_keeps_readable_content() { let health_block = output .split("Health check") .nth(1) + .unwrap_or(output.as_str()) + .split("cosh-osc$") + .next() .unwrap_or(output.as_str()); assert!(!health_block.contains("\x1b["), "{output}"); } @@ -438,7 +442,6 @@ fn raw_cli_startup_health_prompt_ghost_tab_fills_first_suggestion() { !output.contains("\x1b[s\x1b[2m Analyze memory pressure"), "{output}" ); - assert!(!output.contains('\x07'), "{output}"); let first_prompt = first_health_prompt(&output).expect("first health prompt"); let compact = compact_without_box_chars(&output); assert!( @@ -626,7 +629,12 @@ fn raw_cli_startup_health_context_reaches_agent_request() { assert!(compact.contains("scan_id=health-"), "{output}"); assert!(compact.contains("bounded_facts_only=true"), "{output}"); assert!(!output.contains("journalctl -k"), "{output}"); - assert!(!output.contains("/tmp/cosh"), "{output}"); + let context_tail = output + .split("Runtime context hints visible to Agent") + .nth(1) + .unwrap_or(""); + let context_card = context_tail.split('╰').next().unwrap_or(context_tail); + assert!(!context_card.contains("/tmp/cosh"), "{output}"); } #[cfg(not(target_os = "linux"))] @@ -890,6 +898,10 @@ fn raw_cli_startup_hooks_render_markdown_findings_without_running_commands() { #[test] fn raw_cli_startup_banner_reports_selected_zsh_shell() { + if Command::new("zsh").arg("--version").output().is_err() { + return; + } + let output = run_raw_cli_with_args_and_env( "fake", &["--shell", "zsh"], @@ -963,8 +975,8 @@ fn raw_cli_backend_unavailable_uses_zh_language_env() { ); assert!(output.contains("正在思考..."), "{output}"); - assert!(output.contains("Agent 回复:"), "{output}"); - assert!(output.contains("治理:"), "{output}"); + assert!(output.contains("Agent 回复"), "{output}"); + assert!(output.contains("治理"), "{output}"); assert!(output.contains("fake backend unavailable"), "{output}"); assert!(output.contains("after-backend-unavailable"), "{output}"); assert!(!output.contains("Thinking..."), "{output}"); @@ -1038,7 +1050,7 @@ fn box_line_widths(output: &str) -> Vec { let trimmed = line.trim_end(); if trimmed.starts_with('╭') || trimmed.starts_with('╰') || trimmed.starts_with('│') { - Some(trimmed.chars().count()) + Some(terminal_display_width(trimmed)) } else { None } @@ -1046,9 +1058,13 @@ fn box_line_widths(output: &str) -> Vec { .collect() } +fn terminal_display_width(line: &str) -> usize { + Span::raw(line).width() +} + fn assert_raw_cli_rejects_shell_args(args: &[&str], expected: &str) { let binary = env!("CARGO_BIN_EXE_cosh-shell"); - let output = Command::new(binary) + let output = raw_cli_command(binary) .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/src/cosh-ng/crates/cosh-shell/tests/support/raw_cli.rs b/src/cosh-ng/crates/cosh-shell/tests/support/raw_cli.rs index e6fff1127..9a1bee585 100644 --- a/src/cosh-ng/crates/cosh-shell/tests/support/raw_cli.rs +++ b/src/cosh-ng/crates/cosh-shell/tests/support/raw_cli.rs @@ -15,6 +15,13 @@ const RAW_CLI_TIMEOUT: Duration = Duration::from_secs(30); pub(crate) const RAW_CLI_UNSET_ENV: &str = "__cosh_raw_cli_unset_env__"; static RAW_CLI_RUN_LOCK: OnceLock> = OnceLock::new(); +static RAW_CLI_GIT_FIXTURE: OnceLock = OnceLock::new(); + +pub(crate) fn raw_cli_command(binary: &str) -> Command { + let mut command = Command::new(binary); + configure_raw_cli_command(&mut command); + command +} pub(crate) fn run_raw_cli_with_envs(adapter: &str, envs: &[(&str, &str)]) -> String { run_raw_cli_with_args_env_and_delayed_input( @@ -52,12 +59,8 @@ pub(crate) fn run_raw_cli_with_args_and_env( .args(extra_args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("COSH_SHELL_ISOLATED", "1") - .env("COSH_SHELL_RAW_SHELL", "bash") - .env("COSH_SHELL_DEFAULT_SHELL", "bash") - .env("COSH_SHELL_LANG", "en-US") - .env("COSH_SHELL_BOOTSTRAP_PATH", "0"); + .stderr(Stdio::piped()); + configure_raw_cli_command(&mut command); apply_raw_cli_envs(&mut command, envs); command.process_group(0); let mut child = command.spawn().expect("spawn cosh-shell raw"); @@ -129,12 +132,8 @@ pub(crate) fn run_raw_cli_with_args_env_current_dir_and_delayed_input( .args(extra_args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("COSH_SHELL_ISOLATED", "1") - .env("COSH_SHELL_RAW_SHELL", "bash") - .env("COSH_SHELL_DEFAULT_SHELL", "bash") - .env("COSH_SHELL_LANG", "en-US") - .env("COSH_SHELL_BOOTSTRAP_PATH", "0"); + .stderr(Stdio::piped()); + configure_raw_cli_command(&mut command); apply_raw_cli_envs(&mut command, envs); command.process_group(0); let mut child = command.spawn().expect("spawn cosh-shell raw"); @@ -175,12 +174,8 @@ pub(crate) fn run_raw_cli_default_with_args_env_and_delayed_input( .args(extra_args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("COSH_SHELL_ISOLATED", "1") - .env("COSH_SHELL_RAW_SHELL", "bash") - .env("COSH_SHELL_DEFAULT_SHELL", "bash") - .env("COSH_SHELL_LANG", "en-US") - .env("COSH_SHELL_BOOTSTRAP_PATH", "0"); + .stderr(Stdio::piped()); + configure_raw_cli_command(&mut command); apply_raw_cli_envs(&mut command, envs); command.process_group(0); let mut child = command.spawn().expect("spawn cosh-shell raw default"); @@ -215,6 +210,41 @@ fn raw_cli_run_lock() -> std::sync::MutexGuard<'static, ()> { .unwrap_or_else(|poisoned| poisoned.into_inner()) } +fn configure_raw_cli_command(command: &mut Command) { + let home = temp_shell_home("default-home"); + let git_work_tree = raw_cli_git_fixture(); + command + .env("COSH_SHELL_ISOLATED", "1") + .env("COSH_SHELL_RAW_SHELL", "bash") + .env("COSH_SHELL_DEFAULT_SHELL", "bash") + .env("COSH_SHELL_LANG", "en-US") + .env("COSH_SHELL_BOOTSTRAP_PATH", "0") + .env("COSH_SHELL_HEALTH_SCAN", "disabled") + .env("LANG", "C.UTF-8") + .env("LC_ALL", "C.UTF-8") + .env("HOME", home) + .env("GIT_DIR", git_work_tree.join(".git")) + .env("GIT_WORK_TREE", git_work_tree); +} + +fn raw_cli_git_fixture() -> &'static Path { + RAW_CLI_GIT_FIXTURE + .get_or_init(|| { + let path = temp_shell_home("git-fixture"); + let status = Command::new("git") + .args(["init", "--quiet"]) + .current_dir(&path) + .status() + .expect("initialize raw cli git fixture"); + assert!( + status.success(), + "initialize raw cli git fixture: {status:?}" + ); + path + }) + .as_path() +} + struct RawCliOutput { status: ExitStatus, stdout: Vec,