Merge pull request #3102 from Yeachan-Heo/fix/issue-696-compact-nontty

Fix compact non-TTY hang
This commit is contained in:
YeonGyu-Kim
2026-05-25 19:41:27 +09:00
committed by GitHub
3 changed files with 147 additions and 5 deletions

View File

@@ -2,9 +2,9 @@
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Output};
use std::process::{Command, Output, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use mock_anthropic_service::{MockAnthropicService, SCENARIO_PREFIX};
use serde_json::Value;
@@ -245,6 +245,84 @@ stderr:
fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed");
}
#[test]
fn compact_subcommand_json_help_fails_fast_when_stdin_closed() {
let workspace = unique_temp_dir("compact-nontty-json-help");
let config_home = workspace.join("config-home");
let home = workspace.join("home");
fs::create_dir_all(&workspace).expect("workspace should exist");
fs::create_dir_all(&config_home).expect("config home should exist");
fs::create_dir_all(&home).expect("home should exist");
let output = run_claw_closed_stdin_with_timeout(
&workspace,
&config_home,
&home,
&["compact", "--output-format", "json", "--help"],
Duration::from_secs(2),
);
assert!(
!output.status.success(),
"compact json help should fail non-zero"
);
assert!(
output.stdout.is_empty(),
"compact json help should not start a prompt/spinner on stdout: {}",
String::from_utf8_lossy(&output.stdout)
);
let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8");
let parsed: Value = serde_json::from_str(stderr.trim()).expect("stderr should be JSON error");
assert_eq!(parsed["status"], "error");
assert_eq!(parsed["error_kind"], "interactive_only");
assert_eq!(parsed["action"], "abort");
assert!(
parsed["message"]
.as_str()
.unwrap_or_default()
.contains("claw compact"),
"message should name compact: {parsed}"
);
fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed");
}
#[test]
fn compact_subcommand_text_fails_fast_when_stdin_closed() {
let workspace = unique_temp_dir("compact-nontty-text");
let config_home = workspace.join("config-home");
let home = workspace.join("home");
fs::create_dir_all(&workspace).expect("workspace should exist");
fs::create_dir_all(&config_home).expect("config home should exist");
fs::create_dir_all(&home).expect("home should exist");
let output = run_claw_closed_stdin_with_timeout(
&workspace,
&config_home,
&home,
&["compact"],
Duration::from_secs(2),
);
assert!(
!output.status.success(),
"compact text should fail non-zero"
);
assert!(
output.stdout.is_empty(),
"compact text should not start a prompt/spinner on stdout: {}",
String::from_utf8_lossy(&output.stdout)
);
let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8");
assert!(
stderr.contains("[error-kind: interactive_only]"),
"{stderr}"
);
assert!(stderr.contains("claw compact"), "{stderr}");
fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed");
}
fn run_claw(
cwd: &std::path::Path,
config_home: &std::path::Path,
@@ -266,6 +344,48 @@ fn run_claw(
command.output().expect("claw should launch")
}
fn run_claw_closed_stdin_with_timeout(
cwd: &std::path::Path,
config_home: &std::path::Path,
home: &std::path::Path,
args: &[&str],
timeout: Duration,
) -> Output {
let mut child = Command::new(env!("CARGO_BIN_EXE_claw"))
.current_dir(cwd)
.env_clear()
.env("CLAW_CONFIG_HOME", config_home)
.env("HOME", home)
.env("NO_COLOR", "1")
.env("PATH", "/usr/bin:/bin")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.args(args)
.spawn()
.expect("claw should launch");
let start = Instant::now();
loop {
if child.try_wait().expect("try_wait should succeed").is_some() {
return child.wait_with_output().expect("output should collect");
}
if start.elapsed() > timeout {
let _ = child.kill();
let output = child
.wait_with_output()
.expect("killed output should collect");
panic!(
"claw did not exit within {:?}\nstdout:\n{}\nstderr:\n{}",
timeout,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
std::thread::sleep(Duration::from_millis(10));
}
}
fn unique_temp_dir(label: &str) -> PathBuf {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)