Skip to content

Commit 028998d

Browse files
committed
test(#775): integration tests for #769-#771 interactive-only guards and #774 hint fields; fix stale classifier unit test string
1 parent c760a49 commit 028998d

3 files changed

Lines changed: 143 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7715,3 +7715,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
77157715
773. **Config deprecation warnings only emitted as unstructured stderr text in `--output-format json` mode** — dogfooded 2026-05-27 on `212f0b2a`. `emit_config_warning_once()` always wrote to stderr regardless of output format, causing JSON-mode callers to receive an unexpected `warning: ...` text line on stderr before the JSON object. Callers had to implement ad-hoc stripping. Fix: added `ConfigLoader::load_collecting_warnings()` method that returns `(RuntimeConfig, Vec<String>)` so callers can surface warnings structurally; `render_config_json()` now uses this and includes a `warnings: []` array in the config JSON envelope. Existing `load()` path unchanged (still emits to stderr for text-mode callers). 36 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori startup-friction probe on `212f0b2a`, 2026-05-27.
77167716

77177717
774. **`claw agents bogus`, `claw plugins bogus`, `claw mcp bogus` returned `hint: null`** — dogfooded 2026-05-27 on `727a1ea4`. Three "unknown subcommand" envelopes had `error_kind` correctly set but `hint: null`: (1) `unknown_agents_subcommand` — both text and JSON handler emitted single-line error with inline remediation after `.`, no `\n`; (2) `unknown_plugins_action` — same, period-delimited remediation; (3) `unknown_mcp_action` — `render_mcp_usage_json` never included a `hint` field at all. Fixes: (1)+(2) added `\n` before remediation suffix in `commands/src/lib.rs`; (3) added `hint` field to `render_mcp_usage_json` pointing at supported actions. All three now return non-null `hint`. 36 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori envelope-consistency probe on `727a1ea4`, 2026-05-27.
7718+
7719+
775. **Missing integration tests for #769-#771 interactive-only guards and #774 hint fields** — dogfooded 2026-05-27 on `c760a49c`. Fixes #769-#771 (session/cost/clear/memory/ultraplan/model/usage/stats/fork interactive-only guards) and #774 (agents/plugins/mcp unknown-subcommand hints) had no integration tests — a regression in any of those 10+ match arms would go undetected. Also: classify_error_kind unit test for `unknown_agents_subcommand` used the old single-line format string, not the `\n`-delimited format emitted after #774. Fixed: (1) updated unit test string to match new `\n`-delimited emission; (2) added `agents_plugins_mcp_unknown_subcommand_have_hint_774` asserting `error_kind` + non-null `hint` for all three; (3) added `interactive_only_guard_batch_769_to_771` asserting `interactive_only` + non-null `hint` for 10 cases. 38 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori test-coverage sweep on `c760a49c`, 2026-05-27.

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12965,8 +12965,9 @@ mod tests {
1296512965
classify_error_kind("slash command /compact is interactive-only"),
1296612966
"interactive_only"
1296712967
);
12968+
// #774: agents now uses \n-delimited format — update test string to match real emission
1296812969
assert_eq!(
12969-
classify_error_kind("unknown agents subcommand: bogus. Supported: list, show, help"),
12970+
classify_error_kind("unknown agents subcommand: bogus.\nSupported: list, show, help"),
1297012971
"unknown_agents_subcommand"
1297112972
);
1297212973
assert_eq!(

rust/crates/rusty-claude-cli/tests/output_format_contract.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2076,3 +2076,142 @@ fn slash_only_verbs_with_args_return_interactive_only_not_credentials_770() {
20762076
);
20772077
}
20782078
}
2079+
2080+
#[test]
2081+
fn agents_plugins_mcp_unknown_subcommand_have_hint_774() {
2082+
// #774: `claw agents bogus`, `claw plugins bogus`, `claw mcp bogus` returned
2083+
// hint:null despite having correct error_kind. Fixed by adding \n delimiter
2084+
// to error strings in commands/src/lib.rs and explicit hint in mcp JSON envelope.
2085+
let root = unique_temp_dir("unknown-subcommands-774");
2086+
fs::create_dir_all(&root).expect("temp dir should exist");
2087+
2088+
// agents bogus
2089+
{
2090+
let output = run_claw(&root, &["--output-format", "json", "agents", "bogus"], &[]);
2091+
assert!(!output.status.success(), "agents bogus should fail");
2092+
let stderr = String::from_utf8_lossy(&output.stderr);
2093+
let json_line = stderr
2094+
.lines()
2095+
.find(|l| l.trim_start().starts_with('{'))
2096+
.expect("agents bogus should emit JSON error");
2097+
let parsed: serde_json::Value = serde_json::from_str(json_line).unwrap();
2098+
assert_eq!(parsed["error_kind"], "unknown_agents_subcommand");
2099+
let hint = parsed["hint"].as_str().unwrap_or("");
2100+
assert!(
2101+
!hint.is_empty(),
2102+
"agents bogus hint must be non-null (#774)"
2103+
);
2104+
assert!(
2105+
hint.contains("list") || hint.contains("show") || hint.contains("help"),
2106+
"agents bogus hint must mention supported actions, got: {hint:?}"
2107+
);
2108+
}
2109+
2110+
// plugins bogus
2111+
{
2112+
let output = run_claw(&root, &["--output-format", "json", "plugins", "bogus"], &[]);
2113+
assert!(!output.status.success(), "plugins bogus should fail");
2114+
let stderr = String::from_utf8_lossy(&output.stderr);
2115+
let json_line = stderr
2116+
.lines()
2117+
.find(|l| l.trim_start().starts_with('{'))
2118+
.expect("plugins bogus should emit JSON error");
2119+
let parsed: serde_json::Value = serde_json::from_str(json_line).unwrap();
2120+
assert_eq!(parsed["error_kind"], "unknown_plugins_action");
2121+
let hint = parsed["hint"].as_str().unwrap_or("");
2122+
assert!(
2123+
!hint.is_empty(),
2124+
"plugins bogus hint must be non-null (#774)"
2125+
);
2126+
}
2127+
2128+
// mcp bogus
2129+
{
2130+
let output = run_claw(&root, &["--output-format", "json", "mcp", "bogus"], &[]);
2131+
assert!(!output.status.success(), "mcp bogus should fail");
2132+
let stdout = String::from_utf8_lossy(&output.stdout);
2133+
let stderr = String::from_utf8_lossy(&output.stderr);
2134+
let json_str = if stdout.trim().starts_with('{') {
2135+
stdout.to_string()
2136+
} else {
2137+
stderr
2138+
.lines()
2139+
.find(|l| l.trim_start().starts_with('{'))
2140+
.unwrap_or("")
2141+
.to_string()
2142+
};
2143+
let parsed: serde_json::Value =
2144+
serde_json::from_str(json_str.trim()).expect("mcp bogus should emit JSON");
2145+
assert_eq!(parsed["error_kind"], "unknown_mcp_action");
2146+
let hint = parsed["hint"].as_str().unwrap_or("");
2147+
assert!(!hint.is_empty(), "mcp bogus hint must be non-null (#774)");
2148+
}
2149+
}
2150+
2151+
#[test]
2152+
fn interactive_only_guard_batch_769_to_771() {
2153+
// #769-#771: a sweep of slash-only verbs with args that previously fell to
2154+
// CliAction::Prompt hitting the credential gate. All must return
2155+
// error_kind:interactive_only (not missing_credentials) with non-null hint.
2156+
let root = unique_temp_dir("interactive-only-batch-769-771");
2157+
fs::create_dir_all(&root).expect("temp dir should exist");
2158+
// Need a git repo for some subcommands
2159+
std::process::Command::new("git")
2160+
.args(["init", "-q"])
2161+
.current_dir(&root)
2162+
.output()
2163+
.ok();
2164+
2165+
let cases: &[&[&str]] = &[
2166+
// #769: session with unknown subcommand
2167+
&["session", "bogus"],
2168+
&["session", "nuke"],
2169+
// #770: slash-only verbs with trailing args
2170+
&["cost", "breakdown"],
2171+
&["clear", "--force"],
2172+
&["memory", "reset"],
2173+
&["ultraplan", "bogus"],
2174+
&["model", "opus", "extra"],
2175+
// #771: usage/stats/fork
2176+
&["usage", "extra"],
2177+
&["stats", "extra"],
2178+
&["fork", "newbranch"],
2179+
];
2180+
2181+
for args in cases {
2182+
let full_args: Vec<&str> = std::iter::once("--output-format")
2183+
.chain(std::iter::once("json"))
2184+
.chain(args.iter().copied())
2185+
.collect();
2186+
let output = run_claw(&root, &full_args, &[]);
2187+
assert!(
2188+
!output.status.success(),
2189+
"claw {} should exit non-zero",
2190+
args.join(" ")
2191+
);
2192+
let stderr = String::from_utf8_lossy(&output.stderr);
2193+
let json_line = stderr
2194+
.lines()
2195+
.find(|l| l.trim_start().starts_with('{'))
2196+
.unwrap_or_else(|| {
2197+
panic!(
2198+
"claw {} should emit JSON, got stderr: {stderr}",
2199+
args.join(" ")
2200+
)
2201+
});
2202+
let parsed: serde_json::Value = serde_json::from_str(json_line).unwrap();
2203+
assert_eq!(
2204+
parsed["error_kind"],
2205+
"interactive_only",
2206+
"claw {} must return interactive_only, got {:?}",
2207+
args.join(" "),
2208+
parsed["error_kind"]
2209+
);
2210+
let hint = parsed["hint"].as_str().unwrap_or("");
2211+
assert!(
2212+
!hint.is_empty(),
2213+
"claw {} must have non-null hint",
2214+
args.join(" ")
2215+
);
2216+
}
2217+
}

0 commit comments

Comments
 (0)