From 481aaa4ee150671d86655d566f52aa1bd7254c16 Mon Sep 17 00:00:00 2001 From: main Date: Wed, 25 Mar 2026 00:03:55 -0400 Subject: Fail fast on silent Claude resume stalls --- crates/phone-opus/tests/mcp_hardening.rs | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) (limited to 'crates/phone-opus/tests/mcp_hardening.rs') diff --git a/crates/phone-opus/tests/mcp_hardening.rs b/crates/phone-opus/tests/mcp_hardening.rs index 0d53c33..0b32442 100644 --- a/crates/phone-opus/tests/mcp_hardening.rs +++ b/crates/phone-opus/tests/mcp_hardening.rs @@ -631,6 +631,7 @@ fn consult_reuses_context_per_cwd_by_default_and_fresh_context_opts_out() -> Tes let args = must(fs::read_to_string(&args_file), "read fake args file")?; let lines = args.lines().collect::>(); assert!(lines.contains(&"-p")); + assert!(lines.contains(&"--verbose")); assert!(lines.contains(&"--output-format")); assert!(lines.contains(&"stream-json")); assert!(lines.contains(&"--strict-mcp-config")); @@ -837,6 +838,56 @@ fn consult_surfaces_downstream_cli_failures() -> TestResult { Ok(()) } +#[test] +fn silent_claude_processes_fail_fast_instead_of_wedging() -> TestResult { + let root = temp_root("consult_stall")?; + let state_home = root.join("state-home"); + let fake_claude = root.join("claude"); + let caller_home = root.join("caller-home"); + must(fs::create_dir_all(&state_home), "create state home")?; + must(fs::create_dir_all(&caller_home), "create caller home")?; + seed_caller_claude_home(&caller_home)?; + write_fake_claude_script(&fake_claude)?; + + let claude_bin = fake_claude.display().to_string(); + let caller_home_path = caller_home.display().to_string(); + let env = [ + ("HOME", caller_home_path.as_str()), + ("PHONE_OPUS_CLAUDE_BIN", claude_bin.as_str()), + ("PHONE_OPUS_CLAUDE_INITIAL_OUTPUT_TIMEOUT_MS", "100"), + ("PHONE_OPUS_TEST_SLEEP_MS", "5000"), + ]; + let mut harness = McpHarness::spawn(&state_home, &env)?; + let _ = harness.initialize()?; + harness.notify_initialized()?; + + let started = std::time::Instant::now(); + let consult = harness.call_tool(3, "consult", json!({ "prompt": "hang forever" }))?; + let elapsed = started.elapsed(); + + assert_tool_error(&consult); + assert_eq!( + tool_content(&consult)["fault"]["class"].as_str(), + Some("downstream") + ); + assert!( + tool_content(&consult)["fault"]["detail"] + .as_str() + .is_some_and(|value| value.contains("produced no stream output within 100 ms")) + ); + assert!(elapsed < std::time::Duration::from_secs(3)); + assert_eq!( + tool_content(&consult)["context"]["consult"]["context_mode"].as_str(), + Some("fresh") + ); + assert!( + tool_content(&consult)["context"]["consult"]["planned_session_id"] + .as_str() + .is_some_and(|value| !value.is_empty()) + ); + Ok(()) +} + #[test] fn quota_failures_surface_resume_context_for_same_cwd() -> TestResult { let root = temp_root("consult_quota_failure")?; -- cgit v1.2.3