diff --git a/ROADMAP.md b/ROADMAP.md index 9bc814b6..48bcd148 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7695,3 +7695,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 763. **Config JSON parse errors fall to `error_kind:"unknown"`** — dogfooded 2026-05-27 on `88ce1810`. Malformed `.claw/settings.json` or `.claw.json` (unterminated string, type mismatch, unknown keys) produce serde_json errors like `"/path/.claw/settings.json: expected ',', found end of input"` but classify as `error_kind:"unknown"` + `hint:null`. Callers must regex the error message to route. Fix: added `config_parse_error` classifier arm that matches on presence of `.claw/settings.json` or `.claw.json` in the error message. All three error patterns now consistently produce `error_kind:"config_parse_error"`. Test coverage added. Source: Jobdori event/log opacity probe on `88ce1810`, 2026-05-27. 764. **`config_parse_error` returned `hint: null` despite #763 adding the classifier** — dogfooded 2026-05-27 on `c86dc73d`. #763 fixed `error_kind` classification but `hint` remained `null` because `ConfigError::Parse` Display impl emitted only the bare serde_json error string (no `\n` delimiter). `split_error_hint()` found nothing to split. Fix: updated `Display for ConfigError::Parse` in `runtime/src/config.rs` to append `\nFix: open the file shown above and correct the JSON syntax, then retry.`. Integration test `config_parse_error_has_typed_error_kind_and_hint_764` added to `output_format_contract.rs` asserting non-zero exit + `error_kind:config_parse_error` + non-empty hint. 31 contract tests pass. Source: Jobdori follow-up probe on `c86dc73d`, 2026-05-27. + +765. **`claw login`/`claw logout` returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `4ea255ca` (gaebal-gajae pinpoint against `88ce1810`, revised ID after #763/#764 landed). `removed_auth_surface_error()` emitted single-line string with no `\n` delimiter; `split_error_hint()` couldn't extract hint, and no `removed_subcommand` classifier arm existed. Fix: (1) `removed_auth_surface_error()` now emits two-line format (`has been removed.\nSet ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead.`); (2) `classify_error_kind()` arm added matching `has been removed.`; (3) unit test assertions and integration test `login_logout_removed_subcommands_have_error_kind_and_hint_765` added verifying both `error_kind:removed_subcommand` and non-null hint mentioning the env var migration. 32 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae + Jobdori probe on `4ea255ca`, 2026-05-27. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 76068e29..d34b32e4 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -325,6 +325,9 @@ fn classify_error_kind(message: &str) -> &'static str { "unknown_plugins_action" } else if message.starts_with("missing_prompt:") { "missing_prompt" + } else if message.contains("has been removed.") { + // #765: removed subcommands (login, logout) — hint contains migration guidance + "removed_subcommand" } else if message.starts_with("unknown_option:") { "unknown_option" } else if message.contains("is a slash command") @@ -1478,8 +1481,9 @@ fn compact_interactive_only_error() -> String { } fn removed_auth_surface_error(command_name: &str) -> String { + // #765: two-line format so split_error_hint() extracts hint into JSON envelope format!( - "`claw {command_name}` has been removed. Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead." + "`claw {command_name}` has been removed.\nSet ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead." ) } @@ -12930,6 +12934,19 @@ mod tests { ), "config_parse_error" ); + // #765: removed auth subcommands must classify as removed_subcommand + assert_eq!( + classify_error_kind( + "`claw login` has been removed.\nSet ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead." + ), + "removed_subcommand" + ); + assert_eq!( + classify_error_kind( + "`claw logout` has been removed.\nSet ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead." + ), + "removed_subcommand" + ); } #[test] diff --git a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs index 4e7508c5..bc4dd11c 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -1865,3 +1865,41 @@ fn config_parse_error_has_typed_error_kind_and_hint_764() { "malformed settings.json must return non-null hint (#764), got: {hint:?}" ); } + +#[test] +fn login_logout_removed_subcommands_have_error_kind_and_hint_765() { + // #765: `claw login` and `claw logout` are removed; JSON envelope must carry + // error_kind:removed_subcommand + non-null hint pointing to the env var migration. + // Before fix: single-line error string → error_kind:"unknown" + hint:null. + let root = unique_temp_dir("login-logout-removed-765"); + fs::create_dir_all(&root).expect("temp dir should exist"); + + for subcmd in &["login", "logout"] { + let output = run_claw(&root, &["--output-format", "json", subcmd], &[]); + assert!( + !output.status.success(), + "claw {subcmd} should exit non-zero" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + let json_line = stderr + .lines() + .find(|l| l.trim_start().starts_with('{')) + .unwrap_or_else(|| panic!("claw {subcmd} stderr should contain a JSON envelope")); + let parsed: serde_json::Value = + serde_json::from_str(json_line).expect("error envelope should be valid JSON"); + + assert_eq!( + parsed["error_kind"], "removed_subcommand", + "claw {subcmd} must return error_kind:removed_subcommand (#765)" + ); + let hint = parsed["hint"].as_str().unwrap_or(""); + assert!( + !hint.is_empty(), + "claw {subcmd} must return non-null hint (#765), got: {hint:?}" + ); + assert!( + hint.contains("ANTHROPIC_API_KEY") || hint.contains("ANTHROPIC_AUTH_TOKEN"), + "claw {subcmd} hint must mention the env var migration path, got: {hint:?}" + ); + } +}