From 8b090c3d0daf8b336aab9074b0d8aa31a688e232 Mon Sep 17 00:00:00 2001 From: main Date: Tue, 24 Mar 2026 19:09:28 -0400 Subject: Surface reusable consult context on failures --- crates/phone-opus/tests/mcp_hardening.rs | 118 +++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) (limited to 'crates/phone-opus/tests') diff --git a/crates/phone-opus/tests/mcp_hardening.rs b/crates/phone-opus/tests/mcp_hardening.rs index 6a3130b..e9a664b 100644 --- a/crates/phone-opus/tests/mcp_hardening.rs +++ b/crates/phone-opus/tests/mcp_hardening.rs @@ -788,6 +788,124 @@ fn consult_surfaces_downstream_cli_failures() -> TestResult { .as_str() .is_some_and(|value| value.contains("permission denied by fake claude")) ); + assert_eq!( + tool_content(&consult)["context"]["consult"]["context_mode"].as_str(), + Some("fresh") + ); + assert!(tool_content(&consult)["context"]["consult"]["reused_session_id"].is_null()); + Ok(()) +} + +#[test] +fn quota_failures_surface_resume_context_for_same_cwd() -> TestResult { + let root = temp_root("consult_quota_failure")?; + let state_home = root.join("state-home"); + let sandbox = root.join("sandbox"); + let caller_home = root.join("caller-home"); + must(fs::create_dir_all(&state_home), "create state home")?; + must(fs::create_dir_all(&sandbox), "create sandbox")?; + must(fs::create_dir_all(&caller_home), "create caller home")?; + seed_caller_claude_home(&caller_home)?; + + let fake_claude = root.join("claude"); + let stdout_file = root.join("stdout.json"); + let remembered_session = "84b9d462-5af9-4a4e-8e44-379a8d0c46d7"; + write_fake_claude_script(&fake_claude)?; + write_fake_claude_stdout(&stdout_file, "ok", remembered_session, "uuid-remembered")?; + + let claude_bin = fake_claude.display().to_string(); + let stdout_path = stdout_file.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_TEST_STDOUT_FILE", stdout_path.as_str()), + ]; + let mut harness = McpHarness::spawn(&state_home, &env)?; + let _ = harness.initialize()?; + harness.notify_initialized()?; + + let first = harness.call_tool( + 3, + "consult", + json!({ + "prompt": "seed remembered session", + "cwd": sandbox.display().to_string() + }), + )?; + assert_tool_ok(&first); + assert_eq!( + tool_content(&first)["session_id"].as_str(), + Some(remembered_session) + ); + + let quota_env = [ + ("HOME", caller_home_path.as_str()), + ("PHONE_OPUS_CLAUDE_BIN", claude_bin.as_str()), + ("PHONE_OPUS_TEST_EXIT_CODE", "17"), + ( + "PHONE_OPUS_TEST_STDERR", + "You've hit your limit · resets 4pm (America/New_York)", + ), + ]; + drop(harness); + let mut harness = McpHarness::spawn(&state_home, "a_env)?; + let _ = harness.initialize()?; + harness.notify_initialized()?; + + let failed = harness.call_tool( + 4, + "consult", + json!({ + "prompt": "quota me", + "cwd": sandbox.display().to_string() + }), + )?; + assert_tool_error(&failed); + assert_eq!( + tool_content(&failed)["fault"]["detail"].as_str(), + Some("You've hit your limit · resets 4pm (America/New_York)") + ); + assert_eq!( + tool_content(&failed)["context"]["consult"]["cwd"].as_str(), + Some(sandbox.display().to_string().as_str()) + ); + assert_eq!( + tool_content(&failed)["context"]["consult"]["context_mode"].as_str(), + Some("reused") + ); + assert_eq!( + tool_content(&failed)["context"]["consult"]["reused_session_id"].as_str(), + Some(remembered_session) + ); + assert_eq!( + tool_content(&failed)["context"]["consult"]["resume_session_id"].as_str(), + Some(remembered_session) + ); + assert_eq!( + tool_content(&failed)["context"]["consult"]["quota_limited"].as_bool(), + Some(true) + ); + assert_eq!( + tool_content(&failed)["context"]["consult"]["quota_reset_hint"].as_str(), + Some("4pm (America/New_York)") + ); + assert!( + tool_content(&failed)["context"]["consult"]["retry_hint"] + .as_str() + .is_some_and(|value| value.contains(remembered_session)) + ); + assert!( + failed["result"]["content"] + .as_array() + .into_iter() + .flatten() + .filter_map(|entry| entry["text"].as_str()) + .any(|text| { + text.contains("resume_session: 84b9d462-5af9-4a4e-8e44-379a8d0c46d7") + && text.contains("quota_reset: 4pm (America/New_York)") + }) + ); Ok(()) } -- cgit v1.2.3