Skip to content

Commit 5851f2d

Browse files
committed
fix(cli): 6 cascading test regressions hidden behind client_integration gate
- compact flag: was parsed then discarded (`compact: _`) instead of passed to `run_turn_with_output` — hardcoded `false` meant --compact never took effect - piped stdin vs permission prompter: `read_piped_stdin()` consumed all stdin before `CliPermissionPrompter::decide()` could read interactive approval answers; now only consumes stdin as prompt context when permission mode is `DangerFullAccess` (fully unattended) - session resolver: `resolve_managed_session_path` and `list_managed_sessions` now fall back to the pre-isolation flat `.claw/sessions/` layout so legacy sessions remain accessible - help assertion: match on stable prefix after `/session delete` was added in batch 5 - prompt shorthand: fix copy-paste that changed expected prompt from "help me debug" to "$help overview" - mock parity harness: filter captured requests to `/v1/messages` path only, excluding count_tokens preflight calls added by `be561bf` All 6 failures were pre-existing but masked because `client_integration` always failed first (fixed in 8c6dfe5). Workspace: 810+ tests passing, 0 failing.
1 parent 8c6dfe5 commit 5851f2d

2 files changed

Lines changed: 75 additions & 15 deletions

File tree

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -201,16 +201,25 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
201201
output_format,
202202
allowed_tools,
203203
permission_mode,
204-
compact: _,
204+
compact,
205205
base_commit,
206206
} => {
207207
run_stale_base_preflight(base_commit.as_deref());
208-
let stdin_context = read_piped_stdin();
208+
// Only consume piped stdin as prompt context when the permission
209+
// mode is fully unattended. In modes where the permission
210+
// prompter may invoke CliPermissionPrompter::decide(), stdin
211+
// must remain available for interactive approval; otherwise the
212+
// prompter's read_line() would hit EOF and deny every request.
213+
let stdin_context = if matches!(permission_mode, PermissionMode::DangerFullAccess) {
214+
read_piped_stdin()
215+
} else {
216+
None
217+
};
209218
let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref());
210219
LiveCli::new(model, true, allowed_tools, permission_mode)?.run_turn_with_output(
211220
&effective_prompt,
212221
output_format,
213-
false,
222+
compact,
214223
)?;
215224
}
216225
CliAction::Login { output_format } => run_login(output_format)?,
@@ -4394,6 +4403,22 @@ fn resolve_managed_session_path(session_id: &str) -> Result<PathBuf, Box<dyn std
43944403
return Ok(path);
43954404
}
43964405
}
4406+
// Backward compatibility: pre-isolation sessions were stored at
4407+
// `.claw/sessions/<id>.{jsonl,json}` without the per-workspace hash
4408+
// subdirectory. Walk up from `directory` to the `.claw/sessions/` root
4409+
// and try the flat layout as a fallback so users do not lose access
4410+
// to their pre-upgrade managed sessions.
4411+
if let Some(legacy_root) = directory
4412+
.parent()
4413+
.filter(|parent| parent.file_name().is_some_and(|name| name == "sessions"))
4414+
{
4415+
for extension in [PRIMARY_SESSION_EXTENSION, LEGACY_SESSION_EXTENSION] {
4416+
let path = legacy_root.join(format!("{session_id}.{extension}"));
4417+
if path.exists() {
4418+
return Ok(path);
4419+
}
4420+
}
4421+
}
43974422
Err(format_missing_session_reference(session_id).into())
43984423
}
43994424

@@ -4405,9 +4430,14 @@ fn is_managed_session_file(path: &Path) -> bool {
44054430
})
44064431
}
44074432

4408-
fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::error::Error>> {
4409-
let mut sessions = Vec::new();
4410-
for entry in fs::read_dir(sessions_dir()?)? {
4433+
fn collect_sessions_from_dir(
4434+
directory: &Path,
4435+
sessions: &mut Vec<ManagedSessionSummary>,
4436+
) -> Result<(), Box<dyn std::error::Error>> {
4437+
if !directory.exists() {
4438+
return Ok(());
4439+
}
4440+
for entry in fs::read_dir(directory)? {
44114441
let entry = entry?;
44124442
let path = entry.path();
44134443
if !is_managed_session_file(&path) {
@@ -4457,6 +4487,24 @@ fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::er
44574487
branch_name,
44584488
});
44594489
}
4490+
Ok(())
4491+
}
4492+
4493+
fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::error::Error>> {
4494+
let mut sessions = Vec::new();
4495+
let primary_dir = sessions_dir()?;
4496+
collect_sessions_from_dir(&primary_dir, &mut sessions)?;
4497+
4498+
// Backward compatibility: include sessions stored in the pre-isolation
4499+
// flat `.claw/sessions/` root so users do not lose access to existing
4500+
// managed sessions after the workspace-hashed subdirectory rollout.
4501+
if let Some(legacy_root) = primary_dir
4502+
.parent()
4503+
.filter(|parent| parent.file_name().is_some_and(|name| name == "sessions"))
4504+
{
4505+
collect_sessions_from_dir(legacy_root, &mut sessions)?;
4506+
}
4507+
44604508
sessions.sort_by(|left, right| {
44614509
right
44624510
.modified_epoch_millis
@@ -9018,11 +9066,14 @@ mod tests {
90189066
fn multi_word_prompt_still_uses_shorthand_prompt_mode() {
90199067
let _guard = env_lock();
90209068
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
9069+
// Input is ["help", "me", "debug"] so the joined prompt shorthand
9070+
// must be "help me debug". A previous batch accidentally rewrote
9071+
// the expected string to "$help overview" (copy-paste slip).
90219072
assert_eq!(
90229073
parse_args(&["help".to_string(), "me".to_string(), "debug".to_string()])
90239074
.expect("prompt shorthand should still work"),
90249075
CliAction::Prompt {
9025-
prompt: "$help overview".to_string(),
9076+
prompt: "help me debug".to_string(),
90269077
model: DEFAULT_MODEL.to_string(),
90279078
output_format: CliOutputFormat::Text,
90289079
allowed_tools: None,
@@ -9339,7 +9390,9 @@ mod tests {
93399390
assert!(help.contains("/diff"));
93409391
assert!(help.contains("/version"));
93419392
assert!(help.contains("/export [file]"));
9342-
assert!(help.contains("/session [list|switch <session-id>|fork [branch-name]]"));
9393+
// Batch 5 added `/session delete`; match on the stable core rather than
9394+
// the trailing bracket so future additions don't re-break this.
9395+
assert!(help.contains("/session [list|switch <session-id>|fork [branch-name]"));
93439396
assert!(help.contains(
93449397
"/plugin [list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]"
93459398
));

rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,17 +183,24 @@ fn clean_env_cli_reaches_mock_anthropic_service_across_scripted_parity_scenarios
183183
}
184184

185185
let captured = runtime.block_on(server.captured_requests());
186+
// After `be561bf` added count_tokens preflight, each turn sends an
187+
// extra POST to `/v1/messages/count_tokens` before the messages POST.
188+
// The original count (21) assumed messages-only requests. We now
189+
// filter to `/v1/messages` and verify that subset matches the original
190+
// scenario expectation.
191+
let messages_only: Vec<_> = captured
192+
.iter()
193+
.filter(|r| r.path == "/v1/messages")
194+
.collect();
186195
assert_eq!(
187-
captured.len(),
196+
messages_only.len(),
188197
21,
189-
"twelve scenarios should produce twenty-one requests"
198+
"twelve scenarios should produce twenty-one /v1/messages requests (total captured: {}, includes count_tokens)",
199+
captured.len()
190200
);
191-
assert!(captured
192-
.iter()
193-
.all(|request| request.path == "/v1/messages"));
194-
assert!(captured.iter().all(|request| request.stream));
201+
assert!(messages_only.iter().all(|request| request.stream));
195202

196-
let scenarios = captured
203+
let scenarios = messages_only
197204
.iter()
198205
.map(|request| request.scenario.as_str())
199206
.collect::<Vec<_>>();

0 commit comments

Comments
 (0)