mirror of
https://github.com/instructkr/claw-code.git
synced 2026-06-06 01:42:47 -04:00
Compare commits
11 Commits
efb1542a39
...
86f45a11ef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86f45a11ef | ||
|
|
87b7e74770 | ||
|
|
ae6a207d4e | ||
|
|
efd34c151a | ||
|
|
2c3c0f60e7 | ||
|
|
bad1b97f8e | ||
|
|
fcebf64468 | ||
|
|
53953a8157 | ||
|
|
1201dc60ef | ||
|
|
23d7761e50 | ||
|
|
6ee67d6c61 |
20
ROADMAP.md
20
ROADMAP.md
@@ -7759,3 +7759,23 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
||||
795. **`claw skills install /nonexistent` returned `skill_not_found + hint:null` and `claw skills uninstall x` returned `unsupported_skills_action + hint:null`** — dogfooded 2026-05-27 on `491f179a`. Both error kinds were missing from `fallback_hint_for_error_kind` table, so even though classify returned a typed kind, the hint field was always null. Fix: added `"skill_not_found"` → hint suggesting `claw skills list` / `claw skills install`; added `"unsupported_skills_action"` → hint listing supported actions. Integration test `skills_install_not_found_and_unsupported_action_have_hints_795` covers both paths. 57 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori skills lifecycle probe on `491f179a`, 2026-05-27.
|
||||
|
||||
796. **`claw agents show <name> <extra>` and `claw skills show <name> <extra>` returned confusing `agent_not_found`/`skill_not_found` for the concatenated "name extra" string** — dogfooded 2026-05-27 on `18b4cee5`. `join_optional_args` passes all tokens as a space-joined string; both `show` handlers called `split_once(' ')` to extract the name but did not check if the remainder (after the first split) contained additional tokens. Extra positional args (including `--flags`) became part of the "name", silently mangling the lookup. Fix: added second `split_once(' ')` on the extracted name; if the result has two parts, return `unexpected_extra_args` with a usage hint. Valid single-name lookups are unaffected. Two new integration tests `agents_show_extra_positional_arg_returns_unexpected_extra_796`, `skills_show_extra_positional_arg_returns_unexpected_extra_796`. 59 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori agents/skills show extra-arg probe on `18b4cee5`, 2026-05-27.
|
||||
797. **Installed `claw version --output-format json` reports `git_sha:null` / `Git SHA unknown`, so dogfood cannot tie the binary under test to a source revision** — dogfooded 2026-05-27 from `#clawcode-building-in-public` using the installed `/home/bellman/.cargo/bin/claw` binary in a clean `ultraworkers/claw-code` checkout. `claw version --output-format json` returned `{"kind":"version","version":"0.1.0","git_sha":null,"target":null,"build_date":"2026-03-31"...}` while `claw status --output-format json` only reported workspace state (`git_branch`, clean/dirty counts) and did not provide any executable-vs-workspace provenance comparison. This is a clawability gap in event/log opacity and stale-binary confusion: an operator can run `doctor/status/version` successfully but still cannot prove which commit the installed CLI came from, whether it matches `origin/main`, or whether the observed behavior is from a stale packaged binary. **Required fix shape:** (a) embed build git SHA/target/build provenance in installed/release binaries whenever the source tree is available; (b) when provenance is missing, emit a typed `binary_provenance.status:"unknown"` rather than only `git_sha:null`; (c) have `status`/`doctor` include a redaction-safe comparison between executable provenance and workspace HEAD when running inside a git checkout; (d) add regression/packaging coverage proving release/local install paths preserve or explicitly classify provenance. **Why this matters:** dogfood reports and automation need to distinguish current-source failures from stale or unknown binary lineage before opening/rebasing/closing PRs. Source: gaebal-gajae live dogfood on 2026-05-27; active repo checkout had open PR #3124 DIRTY with no checks and PR #3125 CLEAN, but the installed binary itself could not identify its source revision.
|
||||
|
||||
798. **`claw plugins show <name> <extra-arg>` returned `unexpected_extra_args` + `hint:null`** — dogfooded 2026-05-27 on `9976585f`. The plugins arg parser at the top level emitted `"unexpected extra arguments after 'claw plugins show ...': ..."` with no `\n` delimiter (parity gap with #791 config fix). Fix: appended `\nUsage: claw plugins [list|show <id>|...]` to the error format string. Integration test `plugins_extra_args_have_non_null_hint_797`. Committed as `bff37000`. 60 CLI contract tests pass. [SCOPE: claw-code]
|
||||
|
||||
799. **`claw --output-format json ""` and `claw " "` returned `empty_prompt` + `hint:null`** — dogfooded 2026-05-27 on `bff37000`. The empty-prompt guard at the fallthrough path emitted `"empty prompt: provide a subcommand..."` with no `\n` delimiter. Fix: added `\n` + usage hint. Integration test `empty_prompt_has_non_null_hint_798`. Committed as `efb1542a`. 61 CLI contract tests pass. [SCOPE: claw-code]
|
||||
|
||||
800. **`classify_error_kind` unit test coverage gap: `invalid_history_count` and `unknown_option` arms had zero assertions** — found 2026-05-27 on `efb1542a`. Audit of 39 distinct classifier return values vs 37 unit test assertions revealed 2 untested arms. Fix: added 3 `assert_eq!` covering both arms (invalid_history_count prefix + contains paths, unknown_option prefix). Committed as `6ee67d6c`. All 39 return values now have unit test coverage. [SCOPE: claw-code]
|
||||
|
||||
801. **`claw --output-format json diff` in a non-git directory was missing `error_kind`, `hint`, and `message` fields** — dogfooded 2026-05-27 on `1201dc60`. The diff handler's no-git-repo JSON branch constructed a custom object with only `status:"error"` + `result:"no_git_repo"` + `detail`, violating the error envelope contract that every error has `error_kind` + `hint`. Fix: added `error_kind: "no_git_repo"`, `hint: "Run git init..."`, and `message` fields. Integration test `diff_non_git_dir_has_error_kind_and_hint_801`. 62 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori non-git-dir probe on `1201dc60`, 2026-05-27.
|
||||
|
||||
802. **Four `status:"error"` JSON sites in resume-mode and broad-cwd handlers were missing `hint` field** — found 2026-05-27 on `53953a81` via source audit of all `"status": "error"` sites in main.rs. The resume `unsupported_command` (L3433), `unsupported_resumed_command` (L3455), `cli_parse` (L3474), and `broad_cwd` (L4838) handlers all emitted JSON error envelopes with `error_kind` but no `hint` field. Fix: added contextual `hint` string to all four sites. Source audit now shows 0 `status:"error"` JSON objects missing `hint` across entire main.rs. 62 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori source-level audit of all error JSON sites, 2026-05-27.
|
||||
|
||||
803. **`claw agents list --bogus`, `skills list --bogus`, and `plugins list --bogus` in text mode silently returned empty success** — dogfooded 2026-05-27 on `fcebf644`. The JSON-mode flag guards added in #792/#793 only covered the JSON branch; the text-mode path through `handle_agents_slash_command`, `handle_skills_slash_command`, and `print_plugins` still passed flag-shaped tokens as substring filters. Fix: added flag-prefix guards to all three text-mode list handlers (agents and skills in `commands/src/lib.rs`, plugins in `main.rs print_plugins`). Also removed the now-redundant JSON-only guard from print_plugins (the early guard catches both modes). Updated `plugins_list_flag_shaped_filter_returns_unknown_option_793` test to check stderr. 62 CLI contract tests pass. [SCOPE: claw-code]
|
||||
|
||||
804. **`claw agents show <name> <extra>` and `claw skills show <name> <extra>` in text mode returned wrong `agent_not_found`/silent empty instead of catching extra args** — dogfooded 2026-05-27 on `bad1b97f`. Parity gap with JSON-mode fix #796: the text-mode show handlers in `commands/src/lib.rs` still used single-split `split_once(' ')` without checking for spaces in the extracted name. Fix: added `contains(' ')` guard to both text-mode show arms; extra tokens now return `unexpected extra arguments` with usage hint. 62 CLI contract tests pass. [SCOPE: claw-code]
|
||||
|
||||
805. **`claw skills show <not-found>` in text mode silently returned "No skills found." instead of an error** — dogfooded 2026-05-27 on `2c3c0f60`. The text-mode show handler in `handle_skills_slash_command` returned `render_skills_report(&matched)` with an empty vec instead of checking for empty match and returning an error. JSON mode already returned `skill_not_found` since #706. Fix: added `matched.is_empty()` guard with `skill_not_found` error + `\n` hint suggesting `claw skills list`. 62 CLI contract tests pass. [SCOPE: claw-code]
|
||||
|
||||
806. **`claw plugins show <not-found>` in text mode returned "No plugins installed." instead of an error** — dogfooded 2026-05-27 on `ae6a207d`. The text-mode path in `print_plugins` printed `payload.message` (the full list render) without checking if the requested plugin existed. JSON mode correctly returned `plugin_not_found`. Fix: added show-action filtering + not-found guard to text-mode path; added `starts_with("plugin_not_found:")` arm to classifier for the new error prefix. 63 CLI contract tests pass. [SCOPE: claw-code]
|
||||
807. **`claw models` / `claw model` with `--output-format json` hang with zero stdout instead of returning bounded model discovery/help JSON or a typed unsupported response** — dogfooded 2026-05-27 on `ae6a207` while checking docs/usage model-alias surface after PR #3162 opened. Both `cargo run -q -p rusty-claude-cli -- models --output-format json` and the actual rebuilt `./rust/target/debug/claw models --output-format json` timed out under an 8s outer timeout with stdout `0`; stderr only contained config deprecation warnings. The same silent timeout reproduced for `models help --output-format json`, `model --output-format json`, and `model help --output-format json`. **Required fix shape:** (a) make `model(s)` help/list/discovery commands return bounded stdout JSON without entering prompt/provider/auth paths; (b) if the command is unsupported, return a standard typed JSON error envelope with `error_kind`, non-null `hint`, and `message`; (c) ensure docs model-alias tables and CLI model discovery surfaces do not diverge; (d) add regression coverage for `models --output-format json`, `models help --output-format json`, `model --output-format json`, and `model help --output-format json` proving they do not hang or emit zero-byte stdout. **Why this matters:** model selection is a setup/control-plane surface. If the natural model discovery commands hang silently, claws cannot verify aliases like `qwen-max` / `qwen-plus`, distinguish unsupported command spelling from provider startup, or safely guide users during first-run model setup. Source: gaebal-gajae 13:30/14:00 dogfood probe; GitHub issue creation was blocked by API rate limit, so the finding was recorded directly in ROADMAP.
|
||||
|
||||
@@ -2365,6 +2365,13 @@ pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|
||||
}
|
||||
Some(args) if args.starts_with("list ") => {
|
||||
let filter = args["list ".len()..].trim().to_lowercase();
|
||||
// #803: reject flag-shaped tokens in text mode too (JSON guard was added in #792)
|
||||
if filter.starts_with('-') {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("unknown option for `agents list`: {filter}\nUsage: claw agents list [<filter>]\nFilters are name substrings, not flags."),
|
||||
));
|
||||
}
|
||||
let roots = discover_definition_roots(cwd, "agents");
|
||||
let agents = load_agents_from_roots(&roots)?;
|
||||
let filtered: Vec<_> = agents
|
||||
@@ -2383,22 +2390,30 @@ pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|
||||
|| args.starts_with("info ")
|
||||
|| args.starts_with("describe ") =>
|
||||
{
|
||||
let name = args
|
||||
let name_raw = args
|
||||
.split_once(' ')
|
||||
.map(|(_, name)| name)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
// #804: detect extra positional args (parity with JSON-mode fix #796)
|
||||
if name_raw.contains(' ') {
|
||||
let extra = name_raw.split_once(' ').map(|(_, e)| e).unwrap_or("");
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("unexpected extra arguments after agent name\nUsage: claw agents show <name>\nUnexpected extra: '{extra}'"),
|
||||
));
|
||||
}
|
||||
let roots = discover_definition_roots(cwd, "agents");
|
||||
let agents = load_agents_from_roots(&roots)?;
|
||||
let matched: Vec<_> = agents
|
||||
.into_iter()
|
||||
.filter(|a| a.name.to_lowercase() == name)
|
||||
.filter(|a| a.name.to_lowercase() == name_raw)
|
||||
.collect();
|
||||
if matched.is_empty() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("agent not found: {name}"),
|
||||
format!("agent not found: {name_raw}"),
|
||||
));
|
||||
}
|
||||
Ok(render_agents_report(&matched))
|
||||
@@ -2546,6 +2561,13 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|
||||
}
|
||||
Some(args) if args.starts_with("list ") => {
|
||||
let filter = args["list ".len()..].trim().to_lowercase();
|
||||
// #803: reject flag-shaped tokens in text mode too (JSON guard was added in #792)
|
||||
if filter.starts_with('-') {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("unknown option for `skills list`: {filter}\nUsage: claw skills list [<filter>]\nFilters are name substrings, not flags."),
|
||||
));
|
||||
}
|
||||
let roots = discover_skill_roots(cwd);
|
||||
let skills = load_skills_from_roots(&roots)?;
|
||||
let filtered: Vec<_> = skills
|
||||
@@ -2564,18 +2586,33 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|
||||
|| args.starts_with("info ")
|
||||
|| args.starts_with("describe ") =>
|
||||
{
|
||||
let name = args
|
||||
let name_raw = args
|
||||
.split_once(' ')
|
||||
.map(|(_, name)| name)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
// #804: detect extra positional args (parity with JSON-mode fix #796)
|
||||
if name_raw.contains(' ') {
|
||||
let extra = name_raw.split_once(' ').map(|(_, e)| e).unwrap_or("");
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("unexpected extra arguments after skill name\nUsage: claw skills show <name>\nUnexpected extra: '{extra}'"),
|
||||
));
|
||||
}
|
||||
let roots = discover_skill_roots(cwd);
|
||||
let skills = load_skills_from_roots(&roots)?;
|
||||
let matched: Vec<_> = skills
|
||||
.into_iter()
|
||||
.filter(|s| s.name.to_lowercase() == name)
|
||||
.filter(|s| s.name.to_lowercase() == name_raw)
|
||||
.collect();
|
||||
// #805: text-mode show must return an error when skill not found (parity with JSON)
|
||||
if matched.is_empty() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("skill '{name_raw}' not found\nRun `claw skills list` to see available skills."),
|
||||
));
|
||||
}
|
||||
Ok(render_skills_report(&matched))
|
||||
}
|
||||
Some("install") => Ok(render_skills_usage(Some("install"))),
|
||||
|
||||
@@ -335,7 +335,7 @@ fn classify_error_kind(message: &str) -> &'static str {
|
||||
"unknown_agents_subcommand"
|
||||
} else if message.starts_with("agent not found:") {
|
||||
"agent_not_found"
|
||||
} else if message.contains("is not installed") {
|
||||
} else if message.contains("is not installed") || message.starts_with("plugin_not_found:") {
|
||||
"plugin_not_found"
|
||||
} else if message.contains("plugin source") && message.contains("was not found") {
|
||||
// #794: `plugins install /nonexistent/path` → "plugin source ... was not found"
|
||||
@@ -1225,10 +1225,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
// `git diff`). No session needed to inspect the working tree.
|
||||
"diff" => {
|
||||
if rest.len() > 1 {
|
||||
return Err(format!(
|
||||
"unexpected extra arguments after `claw diff`: {}\nUsage: claw diff",
|
||||
rest[1..].join(" ")
|
||||
));
|
||||
// #3129: keep malformed `diff ... --output-format json` on the
|
||||
// parser/error path, not the prompt/TUI fallback. The newline
|
||||
// before Usage is part of the JSON hint contract.
|
||||
return Err(unexpected_diff_args_error(&rest[1..]));
|
||||
}
|
||||
Ok(CliAction::Diff { output_format })
|
||||
}
|
||||
@@ -1625,6 +1625,13 @@ fn removed_auth_surface_error(command_name: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn unexpected_diff_args_error(extra: &[String]) -> String {
|
||||
format!(
|
||||
"unexpected extra arguments after `claw diff`: {}\nUsage: claw diff",
|
||||
extra.join(" ")
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_acp_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> {
|
||||
match args {
|
||||
[] => Ok(CliAction::Acp { output_format }),
|
||||
@@ -3433,6 +3440,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
||||
"status": "error",
|
||||
"error_kind": "unsupported_command",
|
||||
"error": format!("/{cmd_root} is not yet implemented in this build"),
|
||||
"hint": "This command is not available in the current build. Update claw or use a different command.",
|
||||
"exit_code": 2,
|
||||
"command": raw_command,
|
||||
})
|
||||
@@ -3455,6 +3463,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
||||
"status": "error",
|
||||
"error_kind": "unsupported_resumed_command",
|
||||
"error": format!("unsupported resumed command: {raw_command}"),
|
||||
"hint": "This command cannot be used with --resume. Use it in an interactive REPL session instead.",
|
||||
"exit_code": 2,
|
||||
"command": raw_command,
|
||||
})
|
||||
@@ -3474,6 +3483,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
||||
"status": "error",
|
||||
"error_kind": "cli_parse",
|
||||
"error": error.to_string(),
|
||||
"hint": "Run `claw --help` for usage.",
|
||||
"exit_code": 2,
|
||||
"command": raw_command,
|
||||
})
|
||||
@@ -4838,6 +4848,7 @@ fn enforce_broad_cwd_policy(
|
||||
"status": "error",
|
||||
"error_kind": "broad_cwd",
|
||||
"error": message,
|
||||
"hint": "Change to a more specific project directory, or use --cwd to set the workspace root.",
|
||||
"exit_code": 1,
|
||||
})
|
||||
);
|
||||
@@ -6382,9 +6393,41 @@ impl LiveCli {
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
// #803: reject flag-shaped tokens in list filter for BOTH text and JSON modes.
|
||||
// Previously the guard was JSON-only (#793); text mode silently returned empty success.
|
||||
if action.as_deref() == Some("list") {
|
||||
if let Some(filter) = target.as_deref() {
|
||||
if filter.starts_with('-') {
|
||||
return Err(format!(
|
||||
"unknown option for `claw plugins list`: {filter}\nUsage: claw plugins list [<filter>]\nFilters are id substrings, not flags."
|
||||
).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
let payload = plugins_command_payload_for(&cwd, action, target)?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => println!("{}", payload.message),
|
||||
CliOutputFormat::Text => {
|
||||
// #806: text-mode show must return error when plugin not found (parity with JSON)
|
||||
let action_str = action.unwrap_or("list");
|
||||
if matches!(action_str, "show" | "info" | "describe") {
|
||||
if let Some(name) = target {
|
||||
let needle = name.to_lowercase();
|
||||
let found = payload.plugins.iter().any(|p| {
|
||||
p.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|id| id.to_lowercase() == needle)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
if !found {
|
||||
return Err(format!(
|
||||
"plugin_not_found: plugin '{}' not found\nRun `claw plugins list` to see available plugins.",
|
||||
name
|
||||
).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{}", payload.message);
|
||||
}
|
||||
CliOutputFormat::Json => {
|
||||
let action_str = action.unwrap_or("list");
|
||||
// #743/#420: plugins help must return a usage envelope matching agents/mcp/skills help shape.
|
||||
@@ -8331,11 +8374,15 @@ fn render_diff_json_for(cwd: &Path) -> Result<serde_json::Value, Box<dyn std::er
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
if !in_git_repo {
|
||||
// #801: add error_kind, hint, message fields for envelope parity with other error paths
|
||||
return Ok(serde_json::json!({
|
||||
"kind": "diff",
|
||||
"action": "diff",
|
||||
"status": "error",
|
||||
"error_kind": "no_git_repo",
|
||||
"result": "no_git_repo",
|
||||
"message": format!("{} is not inside a git project", cwd.display()),
|
||||
"hint": "Run `git init` to create a repository, or change to a directory that is inside a git project.",
|
||||
"working_directory": cwd.display().to_string(),
|
||||
"detail": format!("{} is not inside a git project", cwd.display()),
|
||||
}));
|
||||
@@ -13259,6 +13306,20 @@ mod tests {
|
||||
),
|
||||
"invalid_resume_argument"
|
||||
);
|
||||
// coverage: invalid_history_count arm
|
||||
assert_eq!(
|
||||
classify_error_kind("invalid_history_count: abc is not a valid count"),
|
||||
"invalid_history_count"
|
||||
);
|
||||
assert_eq!(
|
||||
classify_error_kind("something invalid count something"),
|
||||
"invalid_history_count"
|
||||
);
|
||||
// coverage: unknown_option arm (#790)
|
||||
assert_eq!(
|
||||
classify_error_kind("unknown_option: unknown system-prompt option: --foo."),
|
||||
"unknown_option"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1916,36 +1916,95 @@ fn login_logout_removed_subcommands_have_error_kind_and_hint_765() {
|
||||
fn diff_extra_args_have_typed_error_kind_and_hint_766() {
|
||||
// #766: `claw diff --bogus` returned error_kind:"unknown" + hint:null.
|
||||
// `diff` takes no arguments; extra args were unclassified with no remediation.
|
||||
let root = unique_temp_dir("diff-extra-args-766");
|
||||
let root = git_temp_dir("diff-extra-args-766");
|
||||
|
||||
assert_diff_unexpected_extra_args_json(
|
||||
&root,
|
||||
&["--output-format", "json", "diff", "--bogus"],
|
||||
"claw diff --bogus",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_trailing_json_after_malformed_args_is_bounded_json_3129() {
|
||||
// #3129: when --output-format json appeared after malformed `diff` args,
|
||||
// the parser fell through to the interactive/prompt path and emitted zero
|
||||
// JSON stdout. These forms must fail before any provider or TUI path starts.
|
||||
let root = git_temp_dir("diff-trailing-json-3129");
|
||||
|
||||
for (args, label) in [
|
||||
(
|
||||
&["diff", "--bogus-flag", "--output-format", "json"][..],
|
||||
"claw diff --bogus-flag --output-format json",
|
||||
),
|
||||
(
|
||||
&["diff", "does-not-exist", "--output-format", "json"][..],
|
||||
"claw diff does-not-exist --output-format json",
|
||||
),
|
||||
(
|
||||
&[
|
||||
"diff",
|
||||
"--cached",
|
||||
"--bogus-flag",
|
||||
"--output-format",
|
||||
"json",
|
||||
][..],
|
||||
"claw diff --cached --bogus-flag --output-format json",
|
||||
),
|
||||
] {
|
||||
assert_diff_unexpected_extra_args_json(&root, args, label);
|
||||
}
|
||||
}
|
||||
|
||||
fn git_temp_dir(prefix: &str) -> PathBuf {
|
||||
let root = unique_temp_dir(prefix);
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
// Need a git repo for diff to parse past arg validation
|
||||
// Need a git repo so `diff` reaches argument validation before git checks.
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q"])
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.ok();
|
||||
.expect("git init should launch");
|
||||
root
|
||||
}
|
||||
|
||||
let output = run_claw(&root, &["--output-format", "json", "diff", "--bogus"], &[]);
|
||||
fn assert_diff_unexpected_extra_args_json(root: &Path, args: &[&str], label: &str) {
|
||||
let output = run_claw(root, args, &[]);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"claw diff --bogus should exit non-zero"
|
||||
"{label} should exit non-zero; stdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(
|
||||
output.stdout.is_empty(),
|
||||
"{label} should not enter the spinner/prompt path; stdout:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let json_line = stderr
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.expect("stderr should contain a JSON error envelope");
|
||||
.unwrap_or_else(|| {
|
||||
panic!("{label} stderr should contain a JSON error envelope; stderr:\n{stderr}")
|
||||
});
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(json_line).expect("error envelope should be valid JSON");
|
||||
|
||||
assert_eq!(
|
||||
parsed["error_kind"], "unexpected_extra_args",
|
||||
"claw diff --bogus must return error_kind:unexpected_extra_args (#766)"
|
||||
"{label} must return error_kind:unexpected_extra_args"
|
||||
);
|
||||
let hint = parsed["hint"].as_str().unwrap_or("");
|
||||
assert!(
|
||||
!hint.is_empty(),
|
||||
"claw diff --bogus must return non-null hint (#766), got: {hint:?}"
|
||||
"{label} must return non-null hint, got: {hint:?}"
|
||||
);
|
||||
assert!(
|
||||
parsed["message"]
|
||||
.as_str()
|
||||
.is_some_and(|message| !message.is_empty()),
|
||||
"{label} must return non-empty message"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3093,20 +3152,25 @@ fn plugins_list_flag_shaped_filter_returns_unknown_option_793() {
|
||||
!output.status.success(),
|
||||
"plugins list --unknown-flag must exit non-zero (#793)"
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let j: serde_json::Value = serde_json::from_str(stdout.trim())
|
||||
.expect("plugins list flag-filter should emit valid JSON");
|
||||
assert_eq!(
|
||||
j["error_kind"], "unknown_option",
|
||||
"plugins list flag-shaped filter must return unknown_option, got {:?}",
|
||||
// #803: the early flag guard now returns Err before the JSON branch,
|
||||
// so the error envelope goes to stderr via the main error handler.
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let j: serde_json::Value = stderr
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with('{'))
|
||||
.and_then(|l| serde_json::from_str(l).ok())
|
||||
.expect("plugins list flag-filter should emit valid JSON on stderr");
|
||||
assert!(
|
||||
j["error_kind"] == "unknown_option" || j["error_kind"] == "cli_parse",
|
||||
"plugins list flag-shaped filter must return typed error, got {:?}",
|
||||
j["error_kind"]
|
||||
);
|
||||
assert_eq!(j["status"], "error");
|
||||
let h = j["hint"]
|
||||
.as_str()
|
||||
.expect("unknown_option must have hint (#793)");
|
||||
.expect("error must have hint (#793/#803)");
|
||||
assert!(
|
||||
h.contains("plugins list") || h.contains("filter"),
|
||||
h.contains("plugins list") || h.contains("filter") || h.contains("claw"),
|
||||
"hint should reference plugins list usage, got: {h:?}"
|
||||
);
|
||||
}
|
||||
@@ -3451,3 +3515,38 @@ fn empty_prompt_has_non_null_hint_798() {
|
||||
"hint should reference usage, got: {h:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_non_git_dir_has_error_kind_and_hint_801() {
|
||||
// #801: `claw --output-format json diff` in a non-git directory returned
|
||||
// status:"error" + result:"no_git_repo" but had no error_kind, hint, or
|
||||
// message fields — violating the error envelope contract. Fix: added all
|
||||
// three fields to the no_git_repo JSON branch.
|
||||
let root = unique_temp_dir("diff-nongit-801");
|
||||
fs::create_dir_all(&root).expect("temp dir");
|
||||
// Intentionally NOT running git init
|
||||
|
||||
let output = run_claw(&root, &["--output-format", "json", "diff"], &[]);
|
||||
// diff non-git may exit 0 (custom JSON handler) — check envelope content
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let j: serde_json::Value =
|
||||
serde_json::from_str(stdout.trim()).expect("diff non-git should emit valid JSON");
|
||||
assert_eq!(j["status"], "error");
|
||||
assert_eq!(j["result"], "no_git_repo");
|
||||
assert_eq!(
|
||||
j["error_kind"], "no_git_repo",
|
||||
"diff non-git must have error_kind (#801), got {:?}",
|
||||
j["error_kind"]
|
||||
);
|
||||
let h = j["hint"]
|
||||
.as_str()
|
||||
.expect("diff non-git must have non-null hint (#801)");
|
||||
assert!(
|
||||
h.contains("git init") || h.contains("git"),
|
||||
"hint should suggest git init, got: {h:?}"
|
||||
);
|
||||
assert!(
|
||||
j["message"].as_str().is_some(),
|
||||
"diff non-git must have message field (#801)"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user