pub mod bulk_scan;
pub mod doctor;
pub mod profile_cleanup;
pub mod profile_write;
pub mod profiles;
pub mod scan;
pub mod tenant;
pub mod tenant_import;
pub mod tenant_write;
pub mod topic_assign;
pub mod topic_eval;
pub mod topic_write;
pub mod topics;
use crate::cli::{Cli, ProfilesCommand, RuntimeCommand, TenantCommand, TopCommand, TopicsCommand};
use crate::debug::{DebugLog, DebugPolicy};
use crate::error::{
self, CliError, InterruptContext, Operation, Phase, Plane, SdkContext, SecretSet,
};
use crate::output::{self, Format, Output};
use crate::pagination::{self, GetParams, ListParams};
use crate::tenant::{self as registry, Environment, TenantContext};
use std::time::Duration;
pub struct Invocation {
pub output: Output,
pub session: scm::Session,
pub env: Environment,
pub tenant_flag: Option<String>,
pub secrets: SecretSet,
pub debug: Option<DebugLog>,
pub terminals: crate::input::Terminals,
pub committed: bool,
pub interrupt: Option<InterruptContext>,
}
impl Invocation {
pub fn new(output: Output, tenant_flag: Option<String>) -> Self {
Self {
output,
session: scm::Session::new(),
env: Environment::capture(),
tenant_flag,
secrets: SecretSet::default(),
debug: None,
terminals: crate::input::Terminals::detect(),
committed: false,
interrupt: None,
}
}
pub fn open_debug_log(&mut self) -> Result<(), CliError> {
let dir =
crate::debug::directory(self.env.xdg_state_home.as_deref(), self.env.home.as_deref())?;
let log = DebugLog::open(
&dir,
self.secrets.clone(),
crate::render::style::stderr_color(),
)?;
self.output.status(&format!(
"Debug: API log → {}",
output::terminal_safe(&log.path().display().to_string())
));
self.debug = Some(log);
Ok(())
}
pub fn load_tenant(&mut self) -> Result<TenantContext, CliError> {
let context = registry::load_selected(&self.env, self.tenant_flag.as_deref())?;
self.secrets.register_config(&context.config);
Ok(context)
}
pub async fn connect_management(
&mut self,
format: Option<Format>,
options: Option<&scm::RequestOptions>,
) -> Result<(scm::airs::management::ManagementClient, Format), CliError> {
let context = self.load_tenant()?;
let format = output::resolve_tenant_default(format, context.config.default_output());
crate::tenant::config::require_management(&context.config, Some(context.name()))?;
let clients = crate::clients::ManagementClients::build(
&context.config,
&self.session,
&self.secrets,
self.debug.as_ref(),
)?;
let budget = if options.is_some() {
crate::clients::WALK_BUDGET
} else {
crate::clients::TOKEN_BUDGET
};
clients.authenticate(options, budget).await?;
Ok((clients.management, format))
}
pub fn management_clients(&mut self) -> Result<crate::clients::ManagementClients, CliError> {
let context = self.load_tenant()?;
crate::tenant::config::require_management(&context.config, Some(context.name()))?;
Ok(crate::clients::ManagementClients::build(
&context.config,
&self.session,
&self.secrets,
self.debug.as_ref(),
)?)
}
pub fn management_error(&self, error: &scm::Error, budget: Duration) -> CliError {
error::from_sdk(
error,
&SdkContext {
plane: Plane::Management,
phase: Phase::Api,
operation: Operation::Read,
timeout: budget,
secrets: &self.secrets,
},
)
}
}
pub fn nothing_changed(error: CliError) -> CliError {
match error {
CliError::Interrupted(InterruptContext::NoDataWritten) => {
CliError::Interrupted(InterruptContext::NothingChanged)
}
other => other,
}
}
pub fn confirm(invocation: &mut Invocation, message: &str) -> Result<bool, CliError> {
if let Some(signals) = invocation.output.signals() {
signals.set_runtime_prompt();
}
if invocation.output.pending_signal().is_some() {
return Err(CliError::Interrupted(InterruptContext::NothingChanged));
}
let stdin = std::io::stdin();
let mut input = stdin.lock();
Ok(crate::input::Prompter::new(&mut input, &mut invocation.output, true).confirm(message))
}
pub const FORMAT_DEPRECATED: &str =
"⚠--format is deprecated and will be removed in v4 — use --output";
fn format_alias(cli: &Cli) -> Option<&str> {
match &cli.command {
Some(TopCommand::Runtime(crate::cli::RuntimeGroup {
command:
Some(RuntimeCommand::Topics(crate::cli::TopicsGroup {
command: Some(command),
})),
})) => match command {
TopicsCommand::Create(args) => args.format.as_deref(),
TopicsCommand::Apply(args) => args.format.as_deref(),
TopicsCommand::Revert(args) => args.format.as_deref(),
TopicsCommand::Eval(args) => args.format.as_deref(),
_ => None,
},
_ => None,
}
}
pub fn uses_format_alias(cli: &Cli) -> bool {
format_alias(cli).is_some()
}
pub use bulk_scan::INPUT_DEPRECATED;
fn bulk_scan_args(cli: &Cli) -> Option<&crate::cli::BulkScanArgs> {
match &cli.command {
Some(TopCommand::Runtime(crate::cli::RuntimeGroup {
command: Some(RuntimeCommand::BulkScan(args)),
})) => Some(args),
_ => None,
}
}
pub fn uses_input_alias(cli: &Cli) -> bool {
bulk_scan_args(cli).is_some_and(|args| args.input.is_some())
}
fn is_runtime_write(cli: &Cli) -> bool {
match &cli.command {
Some(TopCommand::Runtime(crate::cli::RuntimeGroup {
command:
Some(RuntimeCommand::Topics(crate::cli::TopicsGroup {
command: Some(command),
})),
})) => !matches!(command, TopicsCommand::List(_) | TopicsCommand::Get(_)),
Some(TopCommand::Runtime(crate::cli::RuntimeGroup {
command:
Some(RuntimeCommand::Profiles(crate::cli::ProfilesGroup {
command: Some(command),
})),
})) => matches!(
command,
ProfilesCommand::Create(_)
| ProfilesCommand::Update(_)
| ProfilesCommand::Delete(_)
| ProfilesCommand::Cleanup(_)
),
_ => false,
}
}
fn runtime_write_format(cli: &Cli) -> Result<Format, CliError> {
let alias = output::validate_explicit(format_alias(cli))?;
let explicit = output::validate_explicit(cli.global.output.as_deref())?;
Ok(explicit.or(alias).unwrap_or(Format::Pretty))
}
pub fn walk_options() -> Result<scm::RequestOptions, CliError> {
scm::RequestOptions::with_timeout(crate::clients::WALK_BUDGET).map_err(|_| {
CliError::operational("AISEC_SDK_ERROR:The request options could not be configured")
})
}
pub enum Plan {
GroupHelp(&'static [&'static str]),
TenantList(Format),
TenantPath(Option<String>),
TenantRead(Option<String>, Format),
TenantGet {
name: String,
key: &'static str,
format: Format,
},
Doctor(Format),
Scan(scan::ScanInput, Format),
ProfilesList(ListParams, Option<Format>),
ProfilesGet(GetParams, Option<Format>),
TopicsList(ListParams, Option<Format>),
TopicsGet(GetParams, Option<Format>),
TenantWrite(tenant_write::WritePlan),
TenantImport(tenant_import::ImportPlan),
TopicsCreate(topic_write::CreatePlan),
TopicsUpdate(topic_write::UpdatePlan),
TopicsDelete(topic_write::DeletePlan),
TopicsApply(topic_assign::ApplyPlan),
TopicsRevert(topic_assign::RevertPlan),
TopicsEval(topic_eval::EvalPlan),
TopicsSample(topic_eval::SamplePlan),
ProfilesCreate(Box<profile_write::CreatePlan>),
ProfilesUpdate(Box<profile_write::UpdatePlan>),
ProfilesDelete(profile_write::DeletePlan),
ProfilesCleanup(profile_cleanup::CleanupPlan),
BulkScan(Box<bulk_scan::BulkPlan>),
ResumePoll(bulk_scan::ResumePlan),
}
impl Plan {
pub fn interrupt_context(&self) -> InterruptContext {
match self {
Self::Scan(..) => InterruptContext::ScanOutcomeUnknown,
Self::TenantWrite(_) | Self::TenantImport(_) => {
InterruptContext::NoConfigurationChanged
}
Self::TopicsCreate(_)
| Self::TopicsUpdate(_)
| Self::TopicsDelete(_)
| Self::ProfilesCreate(_)
| Self::ProfilesUpdate(_)
| Self::ProfilesDelete(_)
| Self::TopicsApply(_)
| Self::TopicsRevert(_) => InterruptContext::NothingChanged,
Self::ProfilesCleanup(_) => InterruptContext::NothingChanged,
Self::TopicsEval(_) => InterruptContext::ScanNotSubmitted,
Self::TopicsSample(_) => InterruptContext::NoDataWritten,
Self::BulkScan(_) => bulk_scan::nothing_submitted(),
Self::ResumePoll(_) => bulk_scan::nothing_changed(),
Self::GroupHelp(_)
| Self::TenantList(_)
| Self::TenantPath(_)
| Self::TenantRead(..)
| Self::TenantGet { .. }
| Self::Doctor(_)
| Self::ProfilesList(..)
| Self::ProfilesGet(..)
| Self::TopicsList(..)
| Self::TopicsGet(..) => InterruptContext::NoDataWritten,
}
}
pub fn debug_policy(&self) -> DebugPolicy {
match self {
Self::Doctor(_)
| Self::Scan(..)
| Self::ProfilesList(..)
| Self::ProfilesGet(..)
| Self::TopicsList(..)
| Self::TopicsGet(..)
| Self::TopicsCreate(_)
| Self::TopicsUpdate(_)
| Self::TopicsDelete(_)
| Self::ProfilesCreate(_)
| Self::ProfilesUpdate(_)
| Self::ProfilesDelete(_)
| Self::TopicsApply(_)
| Self::TopicsRevert(_)
| Self::TopicsEval(_)
| Self::ProfilesCleanup(_)
| Self::BulkScan(_)
| Self::ResumePoll(_) => DebugPolicy::Log,
Self::TopicsSample(_)
| Self::GroupHelp(_)
| Self::TenantList(_)
| Self::TenantPath(_)
| Self::TenantRead(..)
| Self::TenantGet { .. }
| Self::TenantWrite(_)
| Self::TenantImport(_) => DebugPolicy::None,
}
}
}
pub fn plan(cli: &Cli) -> Result<Plan, CliError> {
let explicit = cli.global.output.as_deref();
if let Some(TopCommand::Tenant(crate::cli::TenantGroup {
command: Some(TenantCommand::Import(args)),
})) = &cli.command
{
let format = output::resolve(explicit)?;
if cli.global.tenant.is_some() {
return Err(CliError::usage(
"--tenant cannot be used with 'tenant import'; name the tenant as an argument",
));
}
return Ok(Plan::TenantImport(tenant_import::plan(args, format)?));
}
if let Some(TopCommand::Tenant(crate::cli::TenantGroup {
command: Some(command),
})) = &cli.command
&& let Some(subcommand) = tenant_write::write_command_name(command)
{
output::validate_explicit(explicit)?;
if cli.global.tenant.is_some() {
return Err(CliError::usage(format!(
"--tenant cannot be used with 'tenant {subcommand}'; name the tenant as an argument"
)));
}
return Ok(Plan::TenantWrite(tenant_write::plan(command)?));
}
if is_runtime_write(cli) {
runtime_write_format(cli)?;
}
if bulk_scan_args(cli).is_some()
|| matches!(
&cli.command,
Some(TopCommand::Runtime(crate::cli::RuntimeGroup {
command: Some(RuntimeCommand::ResumePoll(_)),
}))
)
{
output::validate_explicit(explicit)?;
}
registry::validate_tenant_flag(cli.global.tenant.as_deref())?;
let Some(command) = &cli.command else {
return Ok(Plan::GroupHelp(&[]));
};
Ok(match command {
TopCommand::Tenant(group) => match &group.command {
None => Plan::GroupHelp(&["tenant"]),
Some(
TenantCommand::Create(_)
| TenantCommand::Set(_)
| TenantCommand::Unset { .. }
| TenantCommand::Switch { .. }
| TenantCommand::Delete { .. }
| TenantCommand::Import(_),
) => unreachable!("write commands are planned above"),
Some(TenantCommand::List) => Plan::TenantList(output::resolve(explicit)?),
Some(TenantCommand::Path { name }) => {
output::validate_explicit(explicit)?;
Plan::TenantPath(name.clone())
}
Some(TenantCommand::Read { name }) => {
Plan::TenantRead(name.clone(), output::resolve(explicit)?)
}
Some(TenantCommand::Get { name, key }) => {
let key = crate::tenant::config::schema_key(key).ok_or_else(|| {
CliError::usage(format!(
"Unknown configuration key. Supported keys: {}",
crate::tenant::config::KEYS.join(", ")
))
})?;
Plan::TenantGet {
name: name.clone(),
key,
format: output::resolve(explicit)?,
}
}
},
TopCommand::Doctor => Plan::Doctor(output::resolve(explicit)?),
TopCommand::Runtime(group) => match &group.command {
None => Plan::GroupHelp(&["runtime"]),
Some(RuntimeCommand::Scan(args)) => {
let format = output::resolve(explicit)?;
Plan::Scan(scan::validate_input(args)?, format)
}
Some(RuntimeCommand::BulkScan(args)) => {
let format = output::resolve(explicit)?;
Plan::BulkScan(Box::new(bulk_scan::plan(args, format)?))
}
Some(RuntimeCommand::ResumePoll(args)) => {
let format = output::resolve(explicit)?;
Plan::ResumePoll(bulk_scan::plan_resume(args, format)?)
}
Some(RuntimeCommand::Profiles(profiles)) => match &profiles.command {
None => Plan::GroupHelp(&["runtime", "profiles"]),
Some(ProfilesCommand::List(args)) => {
let format = output::validate_explicit(explicit)?;
Plan::ProfilesList(
pagination::parse_list(&args.paging, args.all_versions)?,
format,
)
}
Some(ProfilesCommand::Get(args)) => {
let format = output::validate_explicit(explicit)?;
Plan::ProfilesGet(pagination::parse_get(&args.lookup)?, format)
}
Some(ProfilesCommand::Create(args)) => {
let format = runtime_write_format(cli)?;
Plan::ProfilesCreate(Box::new(profile_write::plan_create(args, format)?))
}
Some(ProfilesCommand::Update(args)) => {
let format = runtime_write_format(cli)?;
Plan::ProfilesUpdate(Box::new(profile_write::plan_update(args, format)?))
}
Some(ProfilesCommand::Delete(args)) => {
output::validate_explicit(explicit)?;
Plan::ProfilesDelete(profile_write::plan_delete(
args,
crate::input::Terminals::detect().interactive(),
)?)
}
Some(ProfilesCommand::Cleanup(args)) => {
let format = runtime_write_format(cli)?;
Plan::ProfilesCleanup(profile_cleanup::plan_cleanup(
args,
format,
crate::input::Terminals::detect().interactive(),
)?)
}
},
Some(RuntimeCommand::Topics(topics)) => match &topics.command {
None => Plan::GroupHelp(&["runtime", "topics"]),
Some(TopicsCommand::List(args)) => {
let format = output::validate_explicit(explicit)?;
Plan::TopicsList(
pagination::parse_list(&args.paging, args.all_versions)?,
format,
)
}
Some(TopicsCommand::Get(args)) => {
let format = output::validate_explicit(explicit)?;
Plan::TopicsGet(pagination::parse_get(&args.lookup)?, format)
}
Some(TopicsCommand::Create(args)) => {
let format = runtime_write_format(cli)?;
Plan::TopicsCreate(topic_write::plan_create(args, format)?)
}
Some(TopicsCommand::Update(args)) => {
let format = runtime_write_format(cli)?;
Plan::TopicsUpdate(topic_write::plan_update(args, format)?)
}
Some(TopicsCommand::Delete(args)) => {
output::validate_explicit(explicit)?;
Plan::TopicsDelete(topic_write::plan_delete(
args,
crate::input::Terminals::detect().interactive(),
)?)
}
Some(TopicsCommand::Apply(args)) => {
let format = runtime_write_format(cli)?;
Plan::TopicsApply(topic_assign::plan_apply(args, format)?)
}
Some(TopicsCommand::Revert(args)) => {
let format = runtime_write_format(cli)?;
Plan::TopicsRevert(topic_assign::plan_revert(
args,
format,
crate::input::Terminals::detect().interactive(),
)?)
}
Some(TopicsCommand::Eval(args)) => {
let format = runtime_write_format(cli)?;
Plan::TopicsEval(topic_eval::plan_eval(args, format)?)
}
Some(TopicsCommand::Sample(args)) => {
output::validate_explicit(explicit)?;
Plan::TopicsSample(topic_eval::plan_sample(args))
}
},
},
})
}
pub async fn dispatch(invocation: &mut Invocation, plan: Plan) -> Result<(), CliError> {
match plan {
Plan::GroupHelp(_) => Ok(()),
Plan::TenantList(format) => tenant::list(invocation, format),
Plan::TenantPath(name) => tenant::path(invocation, name.as_deref()),
Plan::TenantRead(name, format) => tenant::read(invocation, name.as_deref(), format),
Plan::TenantGet { name, key, format } => tenant::get(invocation, &name, key, format),
Plan::Doctor(format) => doctor::run(invocation, format).await,
Plan::Scan(input, format) => scan::run(invocation, &input, format).await,
Plan::ProfilesList(params, format) => profiles::list(invocation, ¶ms, format).await,
Plan::ProfilesGet(params, format) => profiles::get(invocation, ¶ms, format).await,
Plan::TopicsList(params, format) => topics::list(invocation, ¶ms, format).await,
Plan::TopicsGet(params, format) => topics::get(invocation, ¶ms, format).await,
Plan::TenantWrite(write) => tenant_write::run(invocation, write),
Plan::TenantImport(import) => tenant_import::run(invocation, import),
Plan::TopicsCreate(plan) => topic_write::create(invocation, &plan).await,
Plan::TopicsUpdate(plan) => topic_write::update(invocation, &plan).await,
Plan::TopicsDelete(plan) => topic_write::delete(invocation, &plan).await,
Plan::TopicsApply(plan) => topic_assign::apply(invocation, &plan).await,
Plan::TopicsRevert(plan) => topic_assign::revert(invocation, &plan).await,
Plan::TopicsEval(plan) => topic_eval::eval(invocation, &plan).await,
Plan::TopicsSample(plan) => topic_eval::sample(invocation, &plan),
Plan::ProfilesCreate(plan) => profile_write::create(invocation, &plan).await,
Plan::ProfilesUpdate(plan) => profile_write::update(invocation, &plan).await,
Plan::ProfilesDelete(plan) => profile_write::delete(invocation, &plan).await,
Plan::ProfilesCleanup(plan) => profile_cleanup::cleanup(invocation, &plan).await,
Plan::BulkScan(plan) => bulk_scan::run(invocation, &plan).await,
Plan::ResumePoll(plan) => bulk_scan::resume(invocation, &plan).await,
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::FromArgMatches;
fn plan_of(args: &[&str]) -> Result<Plan, CliError> {
let matches = crate::cli::command().try_get_matches_from(args).unwrap();
plan(&Cli::from_arg_matches(&matches).unwrap())
}
fn usage(args: &[&str]) -> String {
match plan_of(args) {
Err(CliError::Usage(message)) => message,
Err(other) => panic!("expected usage error, got {other:?}"),
Ok(_) => panic!("expected usage error"),
}
}
#[test]
fn usage_errors_need_no_tenant() {
assert_eq!(
usage(&[
"airs-cli-rs",
"runtime",
"profiles",
"list",
"--output",
"xml"
]),
"Invalid output format 'xml'. Expected: pretty, table, json"
);
assert_eq!(
usage(&["airs-cli-rs", "doctor", "--output", "yaml"]),
"Invalid output format 'yaml'. Expected: pretty, table, json"
);
assert_eq!(
usage(&["airs-cli-rs", "tenant", "path", "--output", "csv"]),
"Invalid output format 'csv'. Expected: pretty, table, json"
);
assert_eq!(
usage(&["airs-cli-rs", "runtime", "topics", "list", "--limit", "0"]),
"--limit must be a positive integer"
);
assert_eq!(
usage(&[
"airs-cli-rs",
"runtime",
"profiles",
"get",
"x",
"--revision",
"abc"
]),
"--revision must be a non-negative integer"
);
assert!(
usage(&["airs-cli-rs", "tenant", "get", "dev", "nope"]).starts_with(
"Unknown configuration key. Supported keys: airsApiKey, airsApiToken,"
)
);
assert!(
usage(&["airs-cli-rs", "--tenant", "-x", "doctor"]).starts_with("Tenant name must be")
);
assert_eq!(
usage(&["airs-cli-rs", "runtime", "scan", "--profile", "p", ""]),
"AISEC_USER_REQUEST_PAYLOAD_ERROR:At least one of prompt, response, codePrompt, codeResponse, or toolEvent must be provided"
);
}
#[test]
fn unknown_key_wins_over_output_and_path_ignores_a_valid_output() {
assert!(
usage(&[
"airs-cli-rs",
"tenant",
"get",
"dev",
"nope",
"--output",
"xml"
])
.starts_with("Unknown configuration key")
);
assert!(matches!(
plan_of(&["airs-cli-rs", "tenant", "path", "--output", "table"]),
Ok(Plan::TenantPath(None))
));
}
#[test]
fn write_commands_validate_output_then_refuse_tenant() {
assert_eq!(
usage(&[
"airs-cli-rs",
"--tenant",
"-x",
"tenant",
"switch",
"dev",
"--output",
"xml"
]),
"Invalid output format 'xml'. Expected: pretty, table, json"
);
for (sub, args) in [
("create", &["create", "dev"][..]),
("set", &["set", "dev", "defaultOutput", "json"]),
("unset", &["unset", "dev", "defaultOutput"]),
("switch", &["switch", "dev"]),
("delete", &["rm", "dev"]),
] {
let mut full = vec!["airs-cli-rs", "--tenant", "-x", "tenant"];
full.extend_from_slice(args);
assert_eq!(
usage(&full),
format!(
"--tenant cannot be used with 'tenant {sub}'; name the tenant as an argument"
)
);
}
assert_eq!(
usage(&["airs-cli-rs", "--tenant", "dev", "tenant", "create", "_bad"]),
"--tenant cannot be used with 'tenant create'; name the tenant as an argument",
"before the name check"
);
assert_eq!(
usage(&[
"airs-cli-rs",
"tenant",
"create",
"_bad",
"--output",
"table"
]),
"Tenant name must be 1–64 letters, digits, hyphens or underscores and start with a letter/digit"
);
assert!(matches!(
plan_of(&["airs-cli-rs", "tenant", "switch", "dev", "--output", "json"]),
Ok(Plan::TenantWrite(tenant_write::WritePlan::Switch { .. }))
));
assert_eq!(
plan_of(&["airs-cli-rs", "tenant", "switch", "dev"])
.ok()
.unwrap()
.interrupt_context(),
InterruptContext::NoConfigurationChanged
);
}
#[test]
fn runtime_writes_check_formats_before_the_tenant_flag_and_ignore_the_default() {
let create = [
"airs-cli-rs",
"--tenant",
"-bad",
"runtime",
"topics",
"create",
"--name",
"n",
"--description",
"d",
"--examples",
"a",
"b",
];
let mut with_output = create.to_vec();
with_output.extend(["--output", "xml"]);
assert_eq!(
usage(&with_output),
"Invalid output format 'xml'. Expected: pretty, table, json"
);
let mut with_format = create.to_vec();
with_format.extend(["--format", "csv"]);
assert_eq!(
usage(&with_format),
"Invalid output format 'csv'. Expected: pretty, table, json"
);
assert!(usage(&create).starts_with("Tenant name must be"));
let plain = &create[3..];
let mut args = vec!["airs-cli-rs"];
args.extend_from_slice(plain);
args.extend(["--format", "json", "--output", "table"]);
assert!(matches!(
plan_of(&args),
Ok(Plan::TopicsCreate(topic_write::CreatePlan {
format: Format::Table,
..
}))
));
let mut args = vec!["airs-cli-rs"];
args.extend_from_slice(plain);
let plan = plan_of(&args).ok().unwrap();
assert!(matches!(
&plan,
Plan::TopicsCreate(topic_write::CreatePlan {
format: Format::Pretty,
..
})
));
assert_eq!(plan.interrupt_context(), InterruptContext::NothingChanged);
assert_eq!(plan.debug_policy(), DebugPolicy::Log);
assert_eq!(
usage(&[
"airs-cli-rs",
"runtime",
"profiles",
"delete",
"p",
"--force",
"--updated-by",
"a@b",
"--output",
"yaml"
]),
"Invalid output format 'yaml'. Expected: pretty, table, json"
);
}
#[test]
fn topic_assign_and_eval_plans_check_formats_first() {
let apply = [
"airs-cli-rs",
"--tenant",
"-bad",
"runtime",
"topics",
"apply",
"--profile",
"p",
"--name",
"n",
"--intent",
"deny",
"--format",
"xml",
];
assert_eq!(
usage(&apply),
"Invalid output format 'xml'. Expected: pretty, table, json"
);
assert!(usage(&apply[..12]).starts_with("Tenant name must be"));
let mut eval = vec![
"airs-cli-rs",
"runtime",
"topics",
"eval",
"--profile",
"p",
"--prompts",
"x.csv",
"--format",
"json",
];
let plan = plan_of(&eval).ok().unwrap();
assert!(matches!(
&plan,
Plan::TopicsEval(topic_eval::EvalPlan {
format: Format::Json,
concurrency: 5,
rate: None,
..
})
));
assert_eq!(plan.interrupt_context(), InterruptContext::ScanNotSubmitted);
eval.extend(["--output", "table"]);
assert!(matches!(
plan_of(&eval),
Ok(Plan::TopicsEval(topic_eval::EvalPlan {
format: Format::Table,
..
}))
));
let sample = plan_of(&[
"airs-cli-rs",
"runtime",
"topics",
"sample",
"--output",
"json",
]);
assert!(matches!(
&sample,
Ok(Plan::TopicsSample(topic_eval::SamplePlan {
output_file: None
}))
));
assert_eq!(
sample.ok().unwrap().interrupt_context(),
InterruptContext::NoDataWritten
);
}
#[test]
fn groups_without_subcommands_render_help() {
for (args, path) in [
(&["airs-cli-rs"][..], &[][..]),
(&["airs-cli-rs", "--quiet"], &[]),
(&["airs-cli-rs", "runtime"], &["runtime"]),
(
&["airs-cli-rs", "runtime", "topics"],
&["runtime", "topics"],
),
(&["airs-cli-rs", "tenant"], &["tenant"]),
] {
match plan_of(args) {
Ok(Plan::GroupHelp(found)) => assert_eq!(found, path),
_ => panic!("expected group help for {args:?}"),
}
}
}
#[test]
fn debug_policy_logs_only_commands_that_send_requests() {
let policy = |args: &[&str]| plan_of(args).ok().unwrap().debug_policy();
for args in [
&["airs-cli-rs", "doctor"][..],
&["airs-cli-rs", "runtime", "scan", "--profile", "p", "hi"],
&["airs-cli-rs", "runtime", "profiles", "list"],
&["airs-cli-rs", "runtime", "profiles", "get", "x"],
&["airs-cli-rs", "runtime", "topics", "ls"],
&["airs-cli-rs", "runtime", "topics", "get", "x"],
&[
"airs-cli-rs",
"runtime",
"topics",
"apply",
"--profile",
"p",
"--name",
"n",
],
&[
"airs-cli-rs",
"runtime",
"topics",
"revert",
"--profile",
"p",
"--name",
"n",
"--force",
],
&[
"airs-cli-rs",
"runtime",
"topics",
"eval",
"--profile",
"p",
"--prompts",
"p.csv",
],
] {
assert_eq!(policy(args), DebugPolicy::Log, "{args:?}");
}
for args in [
&["airs-cli-rs", "tenant", "list"][..],
&["airs-cli-rs", "tenant", "switch", "dev"],
&["airs-cli-rs", "tenant", "delete", "dev"],
&["airs-cli-rs", "tenant", "create", "dev"],
&["airs-cli-rs", "tenant", "path"],
&["airs-cli-rs", "tenant", "read"],
&["airs-cli-rs", "tenant", "get", "dev", "mgmtTsgId"],
&["airs-cli-rs", "runtime"],
&["airs-cli-rs", "runtime", "topics", "sample"],
] {
assert_eq!(policy(args), DebugPolicy::None, "{args:?}");
}
}
#[test]
fn valid_plans() {
assert!(matches!(
plan_of(&[
"airs-cli-rs",
"runtime",
"profiles",
"ls",
"--output",
"json",
"--all"
]),
Ok(Plan::ProfilesList(
ListParams { all: true, .. },
Some(Format::Json)
))
));
assert!(matches!(
plan_of(&["airs-cli-rs", "tenant", "get", "dev", "mgmtTsgId"]),
Ok(Plan::TenantGet {
key: "mgmtTsgId",
format: Format::Pretty,
..
})
));
assert!(matches!(
plan_of(&["airs-cli-rs", "--output", "json", "doctor"]),
Ok(Plan::Doctor(Format::Json))
));
assert!(matches!(
plan_of(&["airs-cli-rs", "tenant", "list"]),
Ok(Plan::TenantList(Format::Pretty))
));
assert!(matches!(
plan_of(&["airs-cli-rs", "runtime", "topics", "get", "x"]),
Ok(Plan::TopicsGet(_, None))
));
assert!(matches!(
plan_of(&[
"airs-cli-rs",
"runtime",
"scan",
"--profile",
"p",
"hi",
"--output",
"table"
]),
Ok(Plan::Scan(_, Format::Table))
));
}
}