Compare commits

...

5 Commits

Author SHA1 Message Date
Yeachan-Heo
cb56dc12ab Document Rust formatting wrapper
Make scripts/fmt.sh robust to caller cwd and document it as the supported repo-root formatting entrypoint for the Rust workspace.
2026-04-28 09:38:46 +00:00
Yeachan-Heo
71686a20fc Resolve fmt wrapper path from its own directory
The formatting wrapper should remain safe when invoked through different current directories or shell contexts, so resolve the script directory before entering the Rust workspace and forwarding cargo fmt arguments.

Constraint: Wrapper must be runnable from repo root while forwarding flags like --check
Rejected: Leave relative dirname cd | less robust if invocation context changes
Confidence: high
Scope-risk: narrow
Tested: scripts/fmt.sh --check
Tested: git diff --check
2026-04-28 09:38:40 +00:00
Yeachan-Heo
07992b8a1b Make Rust formatting guidance runnable from repo root
The Rust crate layout expects formatting to run from the rust directory, so add a root-level wrapper that preserves the working command while forwarding user flags like --check. Documentation now points contributors at the wrapper instead of the misleading virtual-workspace manifest invocation.

Constraint: Root-level cargo fmt --manifest-path rust/Cargo.toml is misleading for this virtual workspace
Rejected: Document cd rust && cargo fmt directly | a root wrapper gives one stable repo-root command
Confidence: high
Scope-risk: narrow
Tested: scripts/fmt.sh --check
Tested: git diff --check
2026-04-28 09:38:08 +00:00
Yeachan-Heo
74ea754d29 Restore Rust formatting compliance
Run rustfmt from the Rust workspace so CI format checks pass without changing behavior.

Constraint: Scope is formatting-only across tracked Rust files

Confidence: high

Scope-risk: narrow

Tested: cd rust && cargo fmt --check

Tested: git diff --check
2026-04-28 09:19:16 +00:00
Yeachan-Heo
77afde768c Clarify allowed tool status handling
Reject empty --allowedTools inputs instead of treating them as an empty restriction, and surface status JSON metadata that distinguishes default unrestricted tools from flag-provided allow lists.

Confidence: high
Scope-risk: narrow
Tested: cargo test -p rusty-claude-cli rejects_empty_allowed_tools_flag -- --nocapture
Tested: cargo test -p tools allowed_tools_rejects_empty_token_lists -- --nocapture
Tested: cargo check -p rusty-claude-cli -p tools
Tested: cargo test -p rusty-claude-cli -p tools
Not-tested: full workspace cargo fmt --check is blocked by pre-existing unrelated formatting drift
2026-04-28 05:44:14 +00:00
9 changed files with 266 additions and 94 deletions

View File

@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- Frameworks: none detected from the supported starter markers.
## Verification
- Run Rust verification from `rust/`: `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`
- Run Rust verification from repo root: `scripts/fmt.sh --check`; for formatting use `scripts/fmt.sh`. Run Rust clippy/tests from `rust/`: `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`
- `src/` and `tests/` are both present; update both surfaces together when behavior changes.
## Repository shape

View File

@@ -7,7 +7,8 @@ This file provides guidance to Claw Code (clawcode.dev) when working with code i
- Frameworks: none detected from the supported starter markers.
## Verification
- Run Rust verification from the repo root: `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`
- From the repository root, run Rust formatting with `scripts/fmt.sh` (or `scripts/fmt.sh --check` for CI-style checks). From this `rust/` directory, the equivalent command is `../scripts/fmt.sh`. Root-level `cargo fmt --manifest-path rust/Cargo.toml` is not the supported formatting command.
- From this `rust/` directory, run Rust verification with `cargo clippy --workspace --all-targets -- -D warnings` and `cargo test --workspace`.
## Working agreement
- Prefer small, reviewable changes and keep generated bootstrap files aligned with actual repo workflows.

View File

@@ -753,14 +753,14 @@ mod tests {
#[test]
fn returns_context_window_metadata_for_kimi_models() {
// kimi-k2.5
let k25_limit = model_token_limit("kimi-k2.5")
.expect("kimi-k2.5 should have token limit metadata");
let k25_limit =
model_token_limit("kimi-k2.5").expect("kimi-k2.5 should have token limit metadata");
assert_eq!(k25_limit.max_output_tokens, 16_384);
assert_eq!(k25_limit.context_window_tokens, 256_000);
// kimi-k1.5
let k15_limit = model_token_limit("kimi-k1.5")
.expect("kimi-k1.5 should have token limit metadata");
let k15_limit =
model_token_limit("kimi-k1.5").expect("kimi-k1.5 should have token limit metadata");
assert_eq!(k15_limit.max_output_tokens, 16_384);
assert_eq!(k15_limit.context_window_tokens, 256_000);
}
@@ -768,11 +768,13 @@ mod tests {
#[test]
fn kimi_alias_resolves_to_kimi_k25_token_limits() {
// The "kimi" alias resolves to "kimi-k2.5" via resolve_model_alias()
let alias_limit = model_token_limit("kimi")
.expect("kimi alias should resolve to kimi-k2.5 limits");
let direct_limit = model_token_limit("kimi-k2.5")
.expect("kimi-k2.5 should have limits");
assert_eq!(alias_limit.max_output_tokens, direct_limit.max_output_tokens);
let alias_limit =
model_token_limit("kimi").expect("kimi alias should resolve to kimi-k2.5 limits");
let direct_limit = model_token_limit("kimi-k2.5").expect("kimi-k2.5 should have limits");
assert_eq!(
alias_limit.max_output_tokens,
direct_limit.max_output_tokens
);
assert_eq!(
alias_limit.context_window_tokens,
direct_limit.context_window_tokens

View File

@@ -2195,9 +2195,16 @@ mod tests {
#[test]
fn provider_specific_size_limits_are_correct() {
assert_eq!(OpenAiCompatConfig::dashscope().max_request_body_bytes, 6_291_456); // 6MB
assert_eq!(OpenAiCompatConfig::openai().max_request_body_bytes, 104_857_600); // 100MB
assert_eq!(OpenAiCompatConfig::xai().max_request_body_bytes, 52_428_800); // 50MB
assert_eq!(
OpenAiCompatConfig::dashscope().max_request_body_bytes,
6_291_456
); // 6MB
assert_eq!(
OpenAiCompatConfig::openai().max_request_body_bytes,
104_857_600
); // 100MB
assert_eq!(OpenAiCompatConfig::xai().max_request_body_bytes, 52_428_800);
// 50MB
}
#[test]

View File

@@ -2623,10 +2623,8 @@ fn render_mcp_report_json_for(
// runs, the existing serializer adds `status: "ok"` below.
match loader.load() {
Ok(runtime_config) => {
let mut value = render_mcp_summary_report_json(
cwd,
runtime_config.mcp().servers(),
);
let mut value =
render_mcp_summary_report_json(cwd, runtime_config.mcp().servers());
if let Some(map) = value.as_object_mut() {
map.insert("status".to_string(), Value::String("ok".to_string()));
map.insert("config_load_error".to_string(), Value::Null);

View File

@@ -228,8 +228,10 @@ fn main() {
// don't need to regex-scrape the prose.
let kind = classify_error_kind(&message);
if message.contains("`claw --help`") {
eprintln!("[error-kind: {kind}]
error: {message}");
eprintln!(
"[error-kind: {kind}]
error: {message}"
);
} else {
eprintln!(
"[error-kind: {kind}]
@@ -372,7 +374,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
model_flag_raw,
permission_mode,
output_format,
} => print_status_snapshot(&model, model_flag_raw.as_deref(), permission_mode, output_format)?,
allowed_tools,
} => print_status_snapshot(
&model,
model_flag_raw.as_deref(),
permission_mode,
output_format,
allowed_tools.as_ref(),
)?,
CliAction::Sandbox { output_format } => print_sandbox_status_snapshot(output_format)?,
CliAction::Prompt {
prompt,
@@ -412,19 +421,17 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
CliAction::Config {
section,
output_format,
} => {
match output_format {
CliOutputFormat::Text => {
println!("{}", render_config_report(section.as_deref())?);
}
CliOutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&render_config_json(section.as_deref())?)?
);
}
} => match output_format {
CliOutputFormat::Text => {
println!("{}", render_config_report(section.as_deref())?);
}
}
CliOutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&render_config_json(section.as_deref())?)?
);
}
},
CliAction::Diff { output_format } => match output_format {
CliOutputFormat::Text => {
println!("{}", render_diff_report()?);
@@ -510,6 +517,7 @@ enum CliAction {
model_flag_raw: Option<String>,
permission_mode: PermissionMode,
output_format: CliOutputFormat,
allowed_tools: Option<AllowedToolSet>,
},
Sandbox {
output_format: CliOutputFormat,
@@ -627,13 +635,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
}
"--help" | "-h"
if !rest.is_empty()
&& matches!(
rest[0].as_str(),
"prompt"
| "commit"
| "pr"
| "issue"
) =>
&& matches!(rest[0].as_str(), "prompt" | "commit" | "pr" | "issue") =>
{
// `--help` following a subcommand that would otherwise forward
// the arg to the API (e.g. `claw prompt --help`) should show
@@ -844,9 +846,14 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
if let Some(action) = parse_local_help_action(&rest) {
return action;
}
if let Some(action) =
parse_single_word_command_alias(&rest, &model, model_flag_raw.as_deref(), permission_mode_override, output_format)
{
if let Some(action) = parse_single_word_command_alias(
&rest,
&model,
model_flag_raw.as_deref(),
permission_mode_override,
output_format,
allowed_tools.clone(),
) {
return action;
}
@@ -1051,6 +1058,7 @@ fn parse_single_word_command_alias(
model_flag_raw: Option<&str>,
permission_mode_override: Option<PermissionMode>,
output_format: CliOutputFormat,
allowed_tools: Option<AllowedToolSet>,
) -> Option<Result<CliAction, String>> {
if rest.is_empty() {
return None;
@@ -1095,6 +1103,7 @@ fn parse_single_word_command_alias(
model_flag_raw: model_flag_raw.map(str::to_string), // #148
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
output_format,
allowed_tools,
})),
"sandbox" => Some(Ok(CliAction::Sandbox { output_format })),
"doctor" => Some(Ok(CliAction::Doctor { output_format })),
@@ -1312,7 +1321,6 @@ fn suggest_closest_term<'a>(input: &str, candidates: &'a [&'a str]) -> Option<&'
ranked_suggestions(input, candidates).into_iter().next()
}
fn suggest_similar_subcommand(input: &str) -> Option<Vec<String>> {
const KNOWN_SUBCOMMANDS: &[&str] = &[
"help",
@@ -1342,8 +1350,7 @@ fn suggest_similar_subcommand(input: &str) -> Option<Vec<String>> {
let prefix_match = common_prefix_len(&normalized_input, &normalized_candidate) >= 4;
let substring_match = normalized_candidate.contains(&normalized_input)
|| normalized_input.contains(&normalized_candidate);
((distance <= 2) || prefix_match || substring_match)
.then_some((distance, *candidate))
((distance <= 2) || prefix_match || substring_match).then_some((distance, *candidate))
})
.collect::<Vec<_>>();
ranked.sort_by(|left, right| left.cmp(right).then_with(|| left.1.cmp(right.1)));
@@ -1363,7 +1370,6 @@ fn common_prefix_len(left: &str, right: &str) -> usize {
.count()
}
fn looks_like_subcommand_typo(input: &str) -> bool {
!input.is_empty()
&& input
@@ -1472,13 +1478,11 @@ fn validate_model_syntax(model: &str) -> Result<(), String> {
err_msg.push_str("\nDid you mean `openai/");
err_msg.push_str(trimmed);
err_msg.push_str("`? (Requires OPENAI_API_KEY env var)");
}
else if trimmed.starts_with("qwen") {
} else if trimmed.starts_with("qwen") {
err_msg.push_str("\nDid you mean `qwen/");
err_msg.push_str(trimmed);
err_msg.push_str("`? (Requires DASHSCOPE_API_KEY env var)");
}
else if trimmed.starts_with("grok") {
} else if trimmed.starts_with("grok") {
err_msg.push_str("\nDid you mean `xai/");
err_msg.push_str(trimmed);
err_msg.push_str("`? (Requires XAI_API_KEY env var)");
@@ -3226,6 +3230,7 @@ fn run_resume_command(
default_permission_mode().as_str(),
&context,
None, // #148: resumed sessions don't have flag provenance
None,
)),
})
}
@@ -4322,7 +4327,6 @@ impl LiveCli {
Ok(())
}
fn run_prompt_compact_json(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
let (mut runtime, hook_abort_monitor) = self.prepare_turn_runtime(false)?;
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
@@ -5417,6 +5421,7 @@ fn print_status_snapshot(
model_flag_raw: Option<&str>,
permission_mode: PermissionMode,
output_format: CliOutputFormat,
allowed_tools: Option<&AllowedToolSet>,
) -> Result<(), Box<dyn std::error::Error>> {
let usage = StatusUsage {
message_count: 0,
@@ -5440,7 +5445,13 @@ fn print_status_snapshot(
match output_format {
CliOutputFormat::Text => println!(
"{}",
format_status_report(&provenance.resolved, usage, permission_mode.as_str(), &context, Some(&provenance))
format_status_report(
&provenance.resolved,
usage,
permission_mode.as_str(),
&context,
Some(&provenance)
)
),
CliOutputFormat::Json => println!(
"{}",
@@ -5450,6 +5461,7 @@ fn print_status_snapshot(
permission_mode.as_str(),
&context,
Some(&provenance),
allowed_tools,
))?
),
}
@@ -5467,6 +5479,7 @@ fn status_json_value(
// that don't have provenance (legacy resume paths) pass None, in which
// case both new fields are omitted.
provenance: Option<&ModelProvenance>,
allowed_tools: Option<&AllowedToolSet>,
) -> serde_json::Value {
// #143: top-level `status` marker so claws can distinguish
// a clean run from a degraded run (config parse failed but other fields
@@ -5476,6 +5489,7 @@ fn status_json_value(
let degraded = context.config_load_error.is_some();
let model_source = provenance.map(|p| p.source.as_str());
let model_raw = provenance.and_then(|p| p.raw.clone());
let allowed_tool_entries = allowed_tools.map(|tools| tools.iter().cloned().collect::<Vec<_>>());
json!({
"kind": "status",
"status": if degraded { "degraded" } else { "ok" },
@@ -5484,6 +5498,11 @@ fn status_json_value(
"model_source": model_source,
"model_raw": model_raw,
"permission_mode": permission_mode,
"allowed_tools": {
"source": if allowed_tools.is_some() { "flag" } else { "default" },
"restricted": allowed_tools.is_some(),
"entries": allowed_tool_entries,
},
"usage": {
"messages": usage.message_count,
"turns": usage.turns,
@@ -9006,26 +9025,24 @@ fn print_help(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::
mod tests {
use super::{
build_runtime_plugin_state_with_loader, build_runtime_with_plugin_state,
collect_session_prompt_history, create_managed_session_handle, describe_tool_progress,
filter_tool_specs, format_bughunter_report, format_commit_preflight_report,
format_commit_skipped_report, format_compact_report, format_connected_line,
format_cost_report, format_history_timestamp, format_internal_prompt_progress_line,
format_issue_report, format_model_report, format_model_switch_report,
format_permissions_report, format_permissions_switch_report, format_pr_report,
format_resume_report, format_status_report, format_tool_call_start, format_tool_result,
format_ultraplan_report, format_unknown_slash_command,
classify_error_kind, collect_session_prompt_history, create_managed_session_handle,
describe_tool_progress, filter_tool_specs, format_bughunter_report,
format_commit_preflight_report, format_commit_skipped_report, format_compact_report,
format_connected_line, format_cost_report, format_history_timestamp,
format_internal_prompt_progress_line, format_issue_report, format_model_report,
format_model_switch_report, format_permissions_report, format_permissions_switch_report,
format_pr_report, format_resume_report, format_status_report, format_tool_call_start,
format_tool_result, format_ultraplan_report, format_unknown_slash_command,
format_unknown_slash_command_message, format_user_visible_api_error,
classify_error_kind,
merge_prompt_with_stdin, normalize_permission_mode, parse_args, parse_export_args,
parse_git_status_branch, parse_git_status_metadata_for, parse_git_workspace_summary,
parse_history_count, permission_policy, print_help_to, push_output_block,
render_config_report, render_diff_report, render_diff_report_for, render_memory_report,
split_error_hint,
render_help_topic, render_prompt_history_report, render_repl_help, render_resume_usage,
render_config_report, render_diff_report, render_diff_report_for, render_help_topic,
render_memory_report, render_prompt_history_report, render_repl_help, render_resume_usage,
render_session_markdown, resolve_model_alias, resolve_model_alias_with_config,
resolve_repl_model, resolve_session_reference, response_to_events,
resume_supported_slash_commands, run_resume_command, short_tool_id,
slash_command_completion_candidates_with_sessions, status_context,
slash_command_completion_candidates_with_sessions, split_error_hint, status_context,
summarize_tool_payload_for_markdown, try_resolve_bare_skill_prompt, validate_no_args,
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic,
@@ -9807,6 +9824,18 @@ mod tests {
assert!(error.contains("unsupported tool in --allowedTools: teleport"));
}
#[test]
fn rejects_empty_allowed_tools_flag() {
for raw in ["", ",,"] {
let error = parse_args(&["--allowedTools".to_string(), raw.to_string()])
.expect_err("empty allowedTools should be rejected");
assert!(
error.contains("--allowedTools was provided with no usable tool names"),
"unexpected error for {raw:?}: {error}"
);
}
}
#[test]
fn parses_system_prompt_options() {
let args = vec![
@@ -10006,8 +10035,8 @@ mod tests {
// with a specific error instead of falling through to the prompt
// path (where they surface a misleading "missing Anthropic
// credentials" error or burn API tokens on an empty prompt).
let empty_err = parse_args(&["".to_string()])
.expect_err("empty positional arg should be rejected");
let empty_err =
parse_args(&["".to_string()]).expect_err("empty positional arg should be rejected");
assert!(
empty_err.starts_with("empty prompt:"),
"empty-arg error should be specific, got: {empty_err}"
@@ -10224,7 +10253,8 @@ mod tests {
.expect("write malformed .claw.json");
let context = with_current_dir(&cwd, || {
super::status_context(None).expect("status_context should not hard-fail on config parse errors (#143)")
super::status_context(None)
.expect("status_context should not hard-fail on config parse errors (#143)")
});
// Phase 1 contract: config_load_error is populated with the parse error.
@@ -10261,7 +10291,14 @@ mod tests {
cumulative: runtime::TokenUsage::default(),
estimated_tokens: 0,
};
let json = super::status_json_value(Some("test-model"), usage, "workspace-write", &context, None);
let json = super::status_json_value(
Some("test-model"),
usage,
"workspace-write",
&context,
None,
None,
);
assert_eq!(
json.get("status").and_then(|v| v.as_str()),
Some("degraded"),
@@ -10278,8 +10315,54 @@ mod tests {
json.get("model").and_then(|v| v.as_str()),
Some("test-model")
);
assert!(json.get("workspace").is_some(), "workspace field still reported");
assert!(json.get("sandbox").is_some(), "sandbox field still reported");
assert!(
json.get("workspace").is_some(),
"workspace field still reported"
);
assert!(
json.get("sandbox").is_some(),
"sandbox field still reported"
);
assert_eq!(
json.pointer("/allowed_tools/source")
.and_then(|v| v.as_str()),
Some("default"),
"default status should expose unrestricted tool source: {json}"
);
assert_eq!(
json.pointer("/allowed_tools/restricted")
.and_then(|v| v.as_bool()),
Some(false),
"default status should expose unrestricted tool state: {json}"
);
let allowed: super::AllowedToolSet = ["read_file", "grep_search"]
.into_iter()
.map(str::to_string)
.collect();
let restricted_json = super::status_json_value(
Some("test-model"),
usage,
"workspace-write",
&context,
None,
Some(&allowed),
);
assert_eq!(
restricted_json
.pointer("/allowed_tools/source")
.and_then(|v| v.as_str()),
Some("flag"),
"flag status should expose allow-list source: {restricted_json}"
);
assert_eq!(
restricted_json
.pointer("/allowed_tools/entries")
.and_then(|v| v.as_array())
.map(Vec::len),
Some(2),
"flag status should expose allow-list entries: {restricted_json}"
);
// Clean path: no config error → status: "ok", config_load_error: null.
let clean_cwd = root.join("project-with-clean-config");
@@ -10288,8 +10371,14 @@ mod tests {
super::status_context(None).expect("clean status_context should succeed")
});
assert!(clean_context.config_load_error.is_none());
let clean_json =
super::status_json_value(Some("test-model"), usage, "workspace-write", &clean_context, None);
let clean_json = super::status_json_value(
Some("test-model"),
usage,
"workspace-write",
&clean_context,
None,
None,
);
assert_eq!(
clean_json.get("status").and_then(|v| v.as_str()),
Some("ok"),
@@ -10366,6 +10455,7 @@ mod tests {
model_flag_raw: None, // #148: no --model flag passed
permission_mode: PermissionMode::DangerFullAccess,
output_format: CliOutputFormat::Text,
allowed_tools: None,
}
);
assert_eq!(
@@ -10388,11 +10478,18 @@ mod tests {
// Other unrecognized args should NOT trigger the --json hint.
let err_other = parse_args(&["doctor".to_string(), "garbage".to_string()])
.expect_err("`doctor garbage` should fail without --json hint");
assert!(!err_other.contains("--output-format json"),
"unrelated args should not trigger --json hint: {err_other}");
assert!(
!err_other.contains("--output-format json"),
"unrelated args should not trigger --json hint: {err_other}"
);
// #154: model syntax error should hint at provider prefix when applicable
let err_gpt = parse_args(&["prompt".to_string(), "test".to_string(), "--model".to_string(), "gpt-4".to_string()])
.expect_err("`--model gpt-4` should fail with OpenAI hint");
let err_gpt = parse_args(&[
"prompt".to_string(),
"test".to_string(),
"--model".to_string(),
"gpt-4".to_string(),
])
.expect_err("`--model gpt-4` should fail with OpenAI hint");
assert!(
err_gpt.contains("Did you mean `openai/gpt-4`?"),
"GPT model error should hint openai/ prefix: {err_gpt}"
@@ -10401,8 +10498,13 @@ mod tests {
err_gpt.contains("OPENAI_API_KEY"),
"GPT model error should mention env var: {err_gpt}"
);
let err_qwen = parse_args(&["prompt".to_string(), "test".to_string(), "--model".to_string(), "qwen-plus".to_string()])
.expect_err("`--model qwen-plus` should fail with DashScope hint");
let err_qwen = parse_args(&[
"prompt".to_string(),
"test".to_string(),
"--model".to_string(),
"qwen-plus".to_string(),
])
.expect_err("`--model qwen-plus` should fail with DashScope hint");
assert!(
err_qwen.contains("Did you mean `qwen/qwen-plus`?"),
"Qwen model error should hint qwen/ prefix: {err_qwen}"
@@ -10412,8 +10514,13 @@ mod tests {
"Qwen model error should mention env var: {err_qwen}"
);
// Unrelated invalid model should NOT get a hint
let err_garbage = parse_args(&["prompt".to_string(), "test".to_string(), "--model".to_string(), "asdfgh".to_string()])
.expect_err("`--model asdfgh` should fail");
let err_garbage = parse_args(&[
"prompt".to_string(),
"test".to_string(),
"--model".to_string(),
"asdfgh".to_string(),
])
.expect_err("`--model asdfgh` should fail");
assert!(
!err_garbage.contains("Did you mean"),
"Unrelated model errors should not get a hint: {err_garbage}"
@@ -10423,15 +10530,42 @@ mod tests {
#[test]
fn classify_error_kind_returns_correct_discriminants() {
// #77: error kind classification for JSON error payloads
assert_eq!(classify_error_kind("missing Anthropic credentials; export ..."), "missing_credentials");
assert_eq!(classify_error_kind("no worker state file found at /tmp/..."), "missing_worker_state");
assert_eq!(classify_error_kind("session not found: abc123"), "session_not_found");
assert_eq!(classify_error_kind("failed to restore session: no managed sessions found"), "session_load_failed");
assert_eq!(classify_error_kind("unrecognized argument `--foo` for subcommand `doctor`"), "cli_parse");
assert_eq!(classify_error_kind("invalid model syntax: 'gpt-4'. Expected ..."), "invalid_model_syntax");
assert_eq!(classify_error_kind("unsupported resumed command: /blargh"), "unsupported_resumed_command");
assert_eq!(classify_error_kind("api failed after 3 attempts: ..."), "api_http_error");
assert_eq!(classify_error_kind("something completely unknown"), "unknown");
assert_eq!(
classify_error_kind("missing Anthropic credentials; export ..."),
"missing_credentials"
);
assert_eq!(
classify_error_kind("no worker state file found at /tmp/..."),
"missing_worker_state"
);
assert_eq!(
classify_error_kind("session not found: abc123"),
"session_not_found"
);
assert_eq!(
classify_error_kind("failed to restore session: no managed sessions found"),
"session_load_failed"
);
assert_eq!(
classify_error_kind("unrecognized argument `--foo` for subcommand `doctor`"),
"cli_parse"
);
assert_eq!(
classify_error_kind("invalid model syntax: 'gpt-4'. Expected ..."),
"invalid_model_syntax"
);
assert_eq!(
classify_error_kind("unsupported resumed command: /blargh"),
"unsupported_resumed_command"
);
assert_eq!(
classify_error_kind("api failed after 3 attempts: ..."),
"api_http_error"
);
assert_eq!(
classify_error_kind("something completely unknown"),
"unknown"
);
}
#[test]
@@ -10902,7 +11036,6 @@ mod tests {
assert!(report.contains("Use /help"));
}
#[test]
fn typoed_doctor_subcommand_returns_did_you_mean_error() {
let error = parse_args(&["doctorr".to_string()]).expect_err("doctorr should error");
@@ -10985,7 +11118,6 @@ mod tests {
);
}
#[test]
fn punctuation_bearing_single_token_still_dispatches_to_prompt() {
// #140: Guard against test pollution — isolate cwd + env so this test

View File

@@ -172,7 +172,10 @@ stderr:
);
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
let parsed: Value = serde_json::from_str(&stdout).expect("compact json stdout should parse");
assert_eq!(parsed["message"], "Mock streaming says hello from the parity harness.");
assert_eq!(
parsed["message"],
"Mock streaming says hello from the parity harness."
);
assert_eq!(parsed["compact"], true);
assert_eq!(parsed["model"], "claude-sonnet-4-6");
assert!(parsed["usage"].is_object());

View File

@@ -240,6 +240,13 @@ impl GlobalToolRegistry {
}
}
if allowed.is_empty() {
return Err(format!(
"--allowedTools was provided with no usable tool names (got `{}`). Omit the flag to allow all tools.",
values.join(" ")
));
}
Ok(Some(allowed))
}
@@ -6883,6 +6890,21 @@ mod tests {
assert!(empty_permission.contains("unsupported plugin permission: "));
}
#[test]
fn allowed_tools_rejects_empty_token_lists() {
let registry = GlobalToolRegistry::builtin();
for raw in ["", ",,", " "] {
let err = registry
.normalize_allowed_tools(&[raw.to_string()])
.expect_err("empty allow-list input should be rejected");
assert!(
err.contains("--allowedTools was provided with no usable tool names"),
"unexpected error for {raw:?}: {err}"
);
}
}
#[test]
fn runtime_tools_extend_registry_definitions_permissions_and_search() {
let registry = GlobalToolRegistry::builtin()

7
scripts/fmt.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT/rust"
exec cargo fmt "$@"