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,
|
output_format,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
permission_mode,
|
permission_mode,
|
||||||
compact: _,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
} => {
|
} => {
|
||||||
run_stale_base_preflight(base_commit.as_deref());
|
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());
|
let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref());
|
||||||
LiveCli::new(model, true, allowed_tools, permission_mode)?.run_turn_with_output(
|
LiveCli::new(model, true, allowed_tools, permission_mode)?.run_turn_with_output(
|
||||||
&effective_prompt,
|
&effective_prompt,
|
||||||
output_format,
|
output_format,
|
||||||
false,
|
compact,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
CliAction::Login { output_format } => run_login(output_format)?,
|
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);
|
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())
|
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>> {
|
fn collect_sessions_from_dir(
|
||||||
let mut sessions = Vec::new();
|
directory: &Path,
|
||||||
for entry in fs::read_dir(sessions_dir()?)? {
|
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 entry = entry?;
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if !is_managed_session_file(&path) {
|
if !is_managed_session_file(&path) {
|
||||||
@@ -4457,6 +4487,24 @@ fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::er
|
|||||||
branch_name,
|
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| {
|
sessions.sort_by(|left, right| {
|
||||||
right
|
right
|
||||||
.modified_epoch_millis
|
.modified_epoch_millis
|
||||||
@@ -9018,11 +9066,14 @@ mod tests {
|
|||||||
fn multi_word_prompt_still_uses_shorthand_prompt_mode() {
|
fn multi_word_prompt_still_uses_shorthand_prompt_mode() {
|
||||||
let _guard = env_lock();
|
let _guard = env_lock();
|
||||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
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!(
|
assert_eq!(
|
||||||
parse_args(&["help".to_string(), "me".to_string(), "debug".to_string()])
|
parse_args(&["help".to_string(), "me".to_string(), "debug".to_string()])
|
||||||
.expect("prompt shorthand should still work"),
|
.expect("prompt shorthand should still work"),
|
||||||
CliAction::Prompt {
|
CliAction::Prompt {
|
||||||
prompt: "$help overview".to_string(),
|
prompt: "help me debug".to_string(),
|
||||||
model: DEFAULT_MODEL.to_string(),
|
model: DEFAULT_MODEL.to_string(),
|
||||||
output_format: CliOutputFormat::Text,
|
output_format: CliOutputFormat::Text,
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
@@ -9339,7 +9390,9 @@ mod tests {
|
|||||||
assert!(help.contains("/diff"));
|
assert!(help.contains("/diff"));
|
||||||
assert!(help.contains("/version"));
|
assert!(help.contains("/version"));
|
||||||
assert!(help.contains("/export [file]"));
|
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(
|
assert!(help.contains(
|
||||||
"/plugin [list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]"
|
"/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());
|
let captured = runtime.block_on(server.captured_requests());
|
||||||
assert_eq!(
|
// After `be561bf` added count_tokens preflight, each turn sends an
|
||||||
captured.len(),
|
// extra POST to `/v1/messages/count_tokens` before the messages POST.
|
||||||
21,
|
// The original count (21) assumed messages-only requests. We now
|
||||||
"twelve scenarios should produce twenty-one requests"
|
// filter to `/v1/messages` and verify that subset matches the original
|
||||||
);
|
// scenario expectation.
|
||||||
assert!(captured
|
let messages_only: Vec<_> = captured
|
||||||
.iter()
|
.iter()
|
||||||
.all(|request| request.path == "/v1/messages"));
|
.filter(|r| r.path == "/v1/messages")
|
||||||
assert!(captured.iter().all(|request| request.stream));
|
.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()
|
.iter()
|
||||||
.map(|request| request.scenario.as_str())
|
.map(|request| request.scenario.as_str())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|||||||
Reference in New Issue
Block a user