test: add failing unknown subcommand tests (ACT-974)

This commit is contained in:
mcfn
2026-04-23 03:45:18 +08:00
parent a62b3252e8
commit f9e9db35ef
4 changed files with 173 additions and 0 deletions
+2
View File
@@ -6,6 +6,8 @@ use crate::browser::{cookies, interaction, navigation, observation, session, sto
use crate::output::ResponseContext;
use crate::setup;
pub mod did_you_mean;
#[derive(Parser, Debug)]
#[command(
name = "actionbook",
+29
View File
@@ -0,0 +1,29 @@
pub fn suggest(_input: &str, _candidates: &[&str]) -> Vec<String> {
unimplemented!("not yet implemented")
}
#[cfg(test)]
mod tests {
use super::suggest;
#[test]
fn suggest_orders_by_distance_then_alpha() {
let suggestions = suggest("tabs", &["list-tabs", "tab", "click"]);
assert_eq!(
suggestions,
vec!["tab".to_string(), "list-tabs".to_string()]
);
}
#[test]
fn suggest_excludes_exact_match_and_handles_empty() {
assert!(
suggest("tab", &["tab", "click"]).is_empty(),
"exact match should not self-suggest"
);
assert!(
suggest("browsr", &["click", "hover", "fill"]).is_empty(),
"distance > 2 should produce an empty suggestion list"
);
}
}
+1
View File
@@ -37,5 +37,6 @@ mod snapshot;
mod storage;
mod tab_management;
mod timeout;
mod unknown_subcommand;
mod wait;
mod windows_daemon;
@@ -0,0 +1,141 @@
use crate::harness::{
assert_failure, assert_meta, assert_success, headless, parse_json, skip, stderr_str,
};
#[test]
fn unknown_browser_subcmd_json() {
if skip() {
return;
}
let out = headless(&["--json", "browser", "tabs"], 10);
assert_failure(&out, "browser tabs --json should fail structurally");
assert_eq!(
out.status.code(),
Some(64),
"unknown browser subcommand should exit EX_USAGE (64)"
);
let v = parse_json(&out);
assert_eq!(v["ok"], false);
assert_eq!(v["error"]["code"], "UNKNOWN_SUBCOMMAND");
assert!(v["error"]["message"].is_string());
assert_eq!(v["error"]["retryable"], false);
assert_eq!(v["error"]["details"]["parent"], "browser");
assert!(
v["error"]["details"]["did_you_mean"]
.as_array()
.is_some_and(|values| values
.iter()
.any(|value| value.as_str() == Some("list-tabs"))),
"did_you_mean should include list-tabs: {v:?}"
);
assert_meta(&v);
}
#[test]
fn unknown_browser_subcmd_text() {
if skip() {
return;
}
let out = headless(&["browser", "tabs"], 10);
assert_failure(&out, "browser tabs text should fail structurally");
assert_eq!(
out.status.code(),
Some(64),
"unknown browser subcommand text path should exit EX_USAGE (64)"
);
let stderr = stderr_str(&out);
assert!(
stderr.contains("UNKNOWN_SUBCOMMAND"),
"stderr should contain UNKNOWN_SUBCOMMAND, got:\n{stderr}"
);
assert!(
stderr.contains("did you mean") || stderr.contains("try one of"),
"stderr should contain a suggestion hint, got:\n{stderr}"
);
}
#[test]
fn valid_browser_subcmd_arg_error_unchanged() {
if skip() {
return;
}
let out = headless(&["browser", "click"], 10);
assert_failure(
&out,
"browser click without args should remain a clap arg error",
);
assert_ne!(
out.status.code(),
Some(64),
"valid subcommand with missing args must not be reclassified as UNKNOWN_SUBCOMMAND"
);
let stderr = stderr_str(&out);
assert!(
stderr.contains("Usage:") || stderr.contains("required"),
"clap arg error should still print usage/required guidance, got:\n{stderr}"
);
}
#[test]
fn unknown_toplevel_subcmd_json() {
if skip() {
return;
}
let out = headless(&["--json", "browsr"], 10);
assert_failure(&out, "top-level typo should fail structurally");
assert_eq!(
out.status.code(),
Some(64),
"unknown top-level subcommand should exit EX_USAGE (64)"
);
let v = parse_json(&out);
assert_eq!(v["ok"], false);
assert_eq!(v["error"]["code"], "UNKNOWN_SUBCOMMAND");
assert!(v["error"]["message"].is_string());
assert_eq!(v["error"]["retryable"], false);
assert!(
v["error"]["details"]["parent"].is_null(),
"top-level unknown subcommand should set parent=null: {v:?}"
);
assert!(
v["error"]["details"]["did_you_mean"]
.as_array()
.is_some_and(|values| values.iter().any(|value| value.as_str() == Some("browser"))),
"did_you_mean should include browser: {v:?}"
);
assert_meta(&v);
}
#[test]
fn browser_help_not_classified_as_unknown() {
if skip() {
return;
}
let out = headless(&["browser", "--help", "--json"], 10);
assert_success(
&out,
"browser --help --json should stay on the custom help path",
);
let v = parse_json(&out);
assert_eq!(v["ok"], true);
assert_eq!(v["command"], "browser help");
assert!(
v["error"].is_null(),
"help output should not contain an error"
);
assert!(
v["data"]
.as_str()
.is_some_and(|help| help.contains("Usage: actionbook browser <subcommand> [options]")),
"browser help JSON should still contain the grouped help text: {v:?}"
);
assert_meta(&v);
}