fix(#803): agents/skills/plugins list --flag in text mode silently returned empty success

This commit is contained in:
YeonGyu-Kim
2026-05-27 19:38:31 +09:00
parent fcebf64468
commit bad1b97f8e
4 changed files with 40 additions and 8 deletions

View File

@@ -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
@@ -2546,6 +2553,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

View File

@@ -6386,6 +6386,17 @@ 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),

View File

@@ -3093,20 +3093,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:?}"
);
}