Compare commits

...

2 Commits

Author SHA1 Message Date
Yeachan-Heo
cc8da08ad3 Restore Rust formatting compliance for origin/main
Run cargo fmt across the Rust workspace after rebasing onto origin/main so the branch matches rustfmt's current output and the CI formatter gate can pass again. This commit intentionally contains formatter output only and keeps it separate from the README brand refresh.

Constraint: origin/main was failing cargo fmt --all --check
Rejected: Manually format only the initially reported files | cargo fmt --all updated additional workspace files that are part of the formatter gate
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Do not mix behavior edits into formatter-only commits
Tested: cargo fmt --all --check
Tested: cargo test --all
Tested: git diff --check
2026-04-23 00:24:18 +00:00
Yeachan-Heo
e43c6b81db Celebrate the 188K-star milestone in the project brand surface
Refresh the root README hero so the repo acknowledges the 188K GitHub star milestone without turning the page into a campaign landing page. The change keeps the existing structure, sharpens the positioning copy, and adds a compact milestone section that fits the current documentation-first surface.

Constraint: Keep the redesign confined to the repo's primary marketing/docs entrypoint
Constraint: Avoid new assets, dependencies, or multi-page brand churn
Rejected: Broader multi-file docs refresh | exceeded the requested reviewable scope
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep milestone copy lightweight so future README updates can age it out without restructuring the page
Tested: git diff --check
Not-tested: Rendered GitHub HTML preview outside local markdown diff
2026-04-23 00:24:09 +00:00
9 changed files with 219 additions and 112 deletions

View File

@@ -1,5 +1,13 @@
# Claw Code
<p align="center">
<strong>188K GitHub stars and climbing.</strong>
</p>
<p align="center">
<strong>Rust-native agent execution for people who want speed, control, and a real terminal.</strong>
</p>
<p align="center">
<a href="https://github.com/ultraworkers/claw-code">ultraworkers/claw-code</a>
·
@@ -28,8 +36,21 @@
<img src="assets/claw-hero.jpeg" alt="Claw Code" width="300" />
</p>
Claw Code is the public Rust implementation of the `claw` CLI agent harness.
The canonical implementation lives in [`rust/`](./rust), and the current source of truth for this repository is **ultraworkers/claw-code**.
<p align="center">
Claw Code just crossed <strong>188,000 GitHub stars</strong>. This repo is the public Rust implementation of the <code>claw</code> CLI agent harness, built in the open with the UltraWorkers community.
</p>
<p align="center">
The canonical implementation lives in <a href="./rust/">rust/</a>, and the current source of truth for this repository is <strong>ultraworkers/claw-code</strong>.
</p>
## 188K and climbing
Thanks to everyone who starred, tested, reviewed, and pushed the project forward. Claw Code is focused on a straightforward promise: a fast local-first CLI agent runtime with native tools, inspectable behavior, and a Rust workspace that stays close to the metal.
- Native Rust workspace and CLI binary under [`rust/`](./rust)
- Local-first workflows for prompts, sessions, tooling, and parity validation
- Open development across the broader UltraWorkers ecosystem
> [!IMPORTANT]
> Start with [`USAGE.md`](./USAGE.md) for build, auth, CLI, session, and parity-harness workflows. Make `claw doctor` your first health check after building, use [`rust/README.md`](./rust/README.md) for crate-level details, read [`PARITY.md`](./PARITY.md) for the current Rust-port checkpoint, and see [`docs/container.md`](./docs/container.md) for the container-first workflow.

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

@@ -172,7 +172,7 @@ async fn execute_bash_async(
) -> io::Result<BashCommandOutput> {
// Detect and emit ship provenance for git push operations
detect_and_emit_ship_prepared(&input.command);
let mut command = prepare_tokio_command(&input.command, &cwd, &sandbox_status, true);
let output_result = if let Some(timeout_ms) = input.timeout {

View File

@@ -405,7 +405,10 @@ pub enum BlockedSubphase {
#[serde(rename = "blocked.branch_freshness")]
BranchFreshness { behind_main: u32 },
#[serde(rename = "blocked.test_hang")]
TestHang { elapsed_secs: u32, test_name: Option<String> },
TestHang {
elapsed_secs: u32,
test_name: Option<String>,
},
#[serde(rename = "blocked.report_pending")]
ReportPending { since_secs: u32 },
}
@@ -543,7 +546,8 @@ impl LaneEvent {
.with_failure_class(blocker.failure_class)
.with_detail(blocker.detail.clone());
if let Some(ref subphase) = blocker.subphase {
event = event.with_data(serde_json::to_value(subphase).expect("subphase should serialize"));
event =
event.with_data(serde_json::to_value(subphase).expect("subphase should serialize"));
}
event
}
@@ -554,7 +558,8 @@ impl LaneEvent {
.with_failure_class(blocker.failure_class)
.with_detail(blocker.detail.clone());
if let Some(ref subphase) = blocker.subphase {
event = event.with_data(serde_json::to_value(subphase).expect("subphase should serialize"));
event =
event.with_data(serde_json::to_value(subphase).expect("subphase should serialize"));
}
event
}
@@ -562,8 +567,12 @@ impl LaneEvent {
/// Ship prepared — §4.44.5
#[must_use]
pub fn ship_prepared(emitted_at: impl Into<String>, provenance: &ShipProvenance) -> Self {
Self::new(LaneEventName::ShipPrepared, LaneEventStatus::Ready, emitted_at)
.with_data(serde_json::to_value(provenance).expect("ship provenance should serialize"))
Self::new(
LaneEventName::ShipPrepared,
LaneEventStatus::Ready,
emitted_at,
)
.with_data(serde_json::to_value(provenance).expect("ship provenance should serialize"))
}
/// Ship commits selected — §4.44.5
@@ -573,22 +582,34 @@ impl LaneEvent {
commit_count: u32,
commit_range: impl Into<String>,
) -> Self {
Self::new(LaneEventName::ShipCommitsSelected, LaneEventStatus::Ready, emitted_at)
.with_detail(format!("{} commits: {}", commit_count, commit_range.into()))
Self::new(
LaneEventName::ShipCommitsSelected,
LaneEventStatus::Ready,
emitted_at,
)
.with_detail(format!("{} commits: {}", commit_count, commit_range.into()))
}
/// Ship merged — §4.44.5
#[must_use]
pub fn ship_merged(emitted_at: impl Into<String>, provenance: &ShipProvenance) -> Self {
Self::new(LaneEventName::ShipMerged, LaneEventStatus::Completed, emitted_at)
.with_data(serde_json::to_value(provenance).expect("ship provenance should serialize"))
Self::new(
LaneEventName::ShipMerged,
LaneEventStatus::Completed,
emitted_at,
)
.with_data(serde_json::to_value(provenance).expect("ship provenance should serialize"))
}
/// Ship pushed to main — §4.44.5
#[must_use]
pub fn ship_pushed_main(emitted_at: impl Into<String>, provenance: &ShipProvenance) -> Self {
Self::new(LaneEventName::ShipPushedMain, LaneEventStatus::Completed, emitted_at)
.with_data(serde_json::to_value(provenance).expect("ship provenance should serialize"))
Self::new(
LaneEventName::ShipPushedMain,
LaneEventStatus::Completed,
emitted_at,
)
.with_data(serde_json::to_value(provenance).expect("ship provenance should serialize"))
}
#[must_use]

View File

@@ -58,8 +58,8 @@ impl SessionStore {
let workspace_root = workspace_root.as_ref();
// #151: canonicalize workspace_root for consistent fingerprinting
// across equivalent path representations.
let canonical_workspace = fs::canonicalize(workspace_root)
.unwrap_or_else(|_| workspace_root.to_path_buf());
let canonical_workspace =
fs::canonicalize(workspace_root).unwrap_or_else(|_| workspace_root.to_path_buf());
let sessions_root = data_dir
.as_ref()
.join("sessions")
@@ -158,10 +158,9 @@ impl SessionStore {
}
pub fn latest_session(&self) -> Result<ManagedSessionSummary, SessionControlError> {
self.list_sessions()?
.into_iter()
.next()
.ok_or_else(|| SessionControlError::Format(format_no_managed_sessions(&self.sessions_root)))
self.list_sessions()?.into_iter().next().ok_or_else(|| {
SessionControlError::Format(format_no_managed_sessions(&self.sessions_root))
})
}
pub fn load_session(

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,12 @@ 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)?,
} => print_status_snapshot(
&model,
model_flag_raw.as_deref(),
permission_mode,
output_format,
)?,
CliAction::Sandbox { output_format } => print_sandbox_status_snapshot(output_format)?,
CliAction::Prompt {
prompt,
@@ -412,19 +419,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()?);
@@ -627,13 +632,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 +843,13 @@ 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,
) {
return action;
}
@@ -1312,7 +1315,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 +1344,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 +1364,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 +1472,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)");
@@ -4322,7 +4320,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);
@@ -5440,7 +5437,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!(
"{}",
@@ -9006,26 +9009,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,
@@ -10006,8 +10007,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 +10225,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 +10263,8 @@ 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);
assert_eq!(
json.get("status").and_then(|v| v.as_str()),
Some("degraded"),
@@ -10278,8 +10281,14 @@ 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"
);
// Clean path: no config error → status: "ok", config_load_error: null.
let clean_cwd = root.join("project-with-clean-config");
@@ -10288,8 +10297,13 @@ 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,
);
assert_eq!(
clean_json.get("status").and_then(|v| v.as_str()),
Some("ok"),
@@ -10388,11 +10402,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 +10422,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 +10438,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 +10454,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 +10960,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 +11042,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());