mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-09 01:24:49 +08:00
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:
@@ -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>]"
|
||||
));
|
||||
|
||||
@@ -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<_>>();
|
||||
|
||||
Reference in New Issue
Block a user