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.
This commit is contained in:
YeonGyu-Kim
2026-04-08 14:54:10 +09:00
parent 8c6dfe57e6
commit 5851f2dee8
2 changed files with 77 additions and 17 deletions

View File

@@ -201,16 +201,25 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
output_format,
allowed_tools,
permission_mode,
compact: _,
compact,
base_commit,
} => {
run_stale_base_preflight(base_commit.as_deref());
let stdin_context = read_piped_stdin();
// Only consume piped stdin as prompt context when the permission
// mode is fully unattended. In modes where the permission
// prompter may invoke CliPermissionPrompter::decide(), stdin
// must remain available for interactive approval; otherwise the
// prompter's read_line() would hit EOF and deny every request.
let stdin_context = if matches!(permission_mode, PermissionMode::DangerFullAccess) {
read_piped_stdin()
} else {
None
};
let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref());
LiveCli::new(model, true, allowed_tools, permission_mode)?.run_turn_with_output(
&effective_prompt,
output_format,
false,
compact,
)?;
}
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
return Ok(path);
}
}
// Backward compatibility: pre-isolation sessions were stored at
// `.claw/sessions/<id>.{jsonl,json}` without the per-workspace hash
// subdirectory. Walk up from `directory` to the `.claw/sessions/` root
// and try the flat layout as a fallback so users do not lose access
// to their pre-upgrade managed sessions.
if let Some(legacy_root) = directory
.parent()
.filter(|parent| parent.file_name().is_some_and(|name| name == "sessions"))
{
for extension in [PRIMARY_SESSION_EXTENSION, LEGACY_SESSION_EXTENSION] {
let path = legacy_root.join(format!("{session_id}.{extension}"));
if path.exists() {
return Ok(path);
}
}
}
Err(format_missing_session_reference(session_id).into())
}
@@ -4405,9 +4430,14 @@ fn is_managed_session_file(path: &Path) -> bool {
})
}
fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::error::Error>> {
let mut sessions = Vec::new();
for entry in fs::read_dir(sessions_dir()?)? {
fn collect_sessions_from_dir(
directory: &Path,
sessions: &mut Vec<ManagedSessionSummary>,
) -> Result<(), Box<dyn std::error::Error>> {
if !directory.exists() {
return Ok(());
}
for entry in fs::read_dir(directory)? {
let entry = entry?;
let path = entry.path();
if !is_managed_session_file(&path) {
@@ -4457,6 +4487,24 @@ fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::er
branch_name,
});
}
Ok(())
}
fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::error::Error>> {
let mut sessions = Vec::new();
let primary_dir = sessions_dir()?;
collect_sessions_from_dir(&primary_dir, &mut sessions)?;
// Backward compatibility: include sessions stored in the pre-isolation
// flat `.claw/sessions/` root so users do not lose access to existing
// managed sessions after the workspace-hashed subdirectory rollout.
if let Some(legacy_root) = primary_dir
.parent()
.filter(|parent| parent.file_name().is_some_and(|name| name == "sessions"))
{
collect_sessions_from_dir(legacy_root, &mut sessions)?;
}
sessions.sort_by(|left, right| {
right
.modified_epoch_millis
@@ -9018,11 +9066,14 @@ mod tests {
fn multi_word_prompt_still_uses_shorthand_prompt_mode() {
let _guard = env_lock();
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
// Input is ["help", "me", "debug"] so the joined prompt shorthand
// must be "help me debug". A previous batch accidentally rewrote
// the expected string to "$help overview" (copy-paste slip).
assert_eq!(
parse_args(&["help".to_string(), "me".to_string(), "debug".to_string()])
.expect("prompt shorthand should still work"),
CliAction::Prompt {
prompt: "$help overview".to_string(),
prompt: "help me debug".to_string(),
model: DEFAULT_MODEL.to_string(),
output_format: CliOutputFormat::Text,
allowed_tools: None,
@@ -9339,7 +9390,9 @@ mod tests {
assert!(help.contains("/diff"));
assert!(help.contains("/version"));
assert!(help.contains("/export [file]"));
assert!(help.contains("/session [list|switch <session-id>|fork [branch-name]]"));
// Batch 5 added `/session delete`; match on the stable core rather than
// the trailing bracket so future additions don't re-break this.
assert!(help.contains("/session [list|switch <session-id>|fork [branch-name]"));
assert!(help.contains(
"/plugin [list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]"
));

View File

@@ -183,17 +183,24 @@ fn clean_env_cli_reaches_mock_anthropic_service_across_scripted_parity_scenarios
}
let captured = runtime.block_on(server.captured_requests());
assert_eq!(
captured.len(),
21,
"twelve scenarios should produce twenty-one requests"
);
assert!(captured
// After `be561bf` added count_tokens preflight, each turn sends an
// extra POST to `/v1/messages/count_tokens` before the messages POST.
// The original count (21) assumed messages-only requests. We now
// filter to `/v1/messages` and verify that subset matches the original
// scenario expectation.
let messages_only: Vec<_> = captured
.iter()
.all(|request| request.path == "/v1/messages"));
assert!(captured.iter().all(|request| request.stream));
.filter(|r| r.path == "/v1/messages")
.collect();
assert_eq!(
messages_only.len(),
21,
"twelve scenarios should produce twenty-one /v1/messages requests (total captured: {}, includes count_tokens)",
captured.len()
);
assert!(messages_only.iter().all(|request| request.stream));
let scenarios = captured
let scenarios = messages_only
.iter()
.map(|request| request.scenario.as_str())
.collect::<Vec<_>>();