use clap::Parser;
use std::ffi::OsString;
use super::*;
use crate::config::{
CompletionsCommand, PluginsCommand, PluginsEditCommand, PluginsInspectCommand,
PluginsListCommand, PluginsSubcommand, PluginsValidateCommand, PricingSubcommand,
PricingValidateCommand, ServerArgs,
};
struct EnvScope {
_cwd_guard: Option<crate::test_support::CwdTestScope>,
_guard: std::sync::MutexGuard<'static, ()>,
values: Vec<(&'static str, Option<OsString>)>,
}
impl EnvScope {
fn hermetic(temp: &tempfile::TempDir) -> Self {
let xdg = temp.path().join("xdg");
std::fs::create_dir_all(&xdg).unwrap();
Self::set_with_cwd_guard(
&[
("HOME", Some(temp.path().as_os_str())),
("XDG_CONFIG_HOME", Some(xdg.as_os_str())),
],
Some(crate::test_support::CwdTestScope::locked()),
)
}
fn set_with_cwd_guard(
values: &[(&'static str, Option<&std::ffi::OsStr>)],
cwd_guard: Option<crate::test_support::CwdTestScope>,
) -> Self {
let guard = crate::test_support::ENV_TEST_LOCK
.lock()
.unwrap_or_else(|error| error.into_inner());
let previous = values
.iter()
.map(|(key, _)| (*key, std::env::var_os(key)))
.collect::<Vec<_>>();
for (key, value) in values {
unsafe {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
Self {
_cwd_guard: cwd_guard,
_guard: guard,
values: previous,
}
}
}
impl Drop for EnvScope {
fn drop(&mut self) {
for (key, value) in self.values.drain(..) {
unsafe {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
}
}
#[test]
fn completions_helper_reports_missing_shell_and_generates_requested_shell() {
let error = generate_completions_to(None, &mut Vec::new())
.unwrap_err()
.to_string();
assert!(error.contains("missing shell argument"));
let mut output = Vec::new();
generate_completions_to(Some(clap_complete::Shell::Bash), &mut output).unwrap();
let script = String::from_utf8(output).unwrap();
assert!(script.contains("_nemo-relay"));
}
#[test]
fn safe_dispatch_helpers_cover_completions_and_plugins_paths() {
let temp = tempfile::tempdir().unwrap();
let _env = EnvScope::hermetic(&temp);
let config_path = temp.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let server = ServerArgs {
config: Some(config_path),
..ServerArgs::default()
};
assert_eq!(
run_completions(CompletionsCommand {
shell: Some(clap_complete::Shell::Bash),
install: false,
})
.unwrap(),
ExitCode::SUCCESS
);
let error = run_plugins(
PluginsCommand {
command: PluginsSubcommand::Edit(PluginsEditCommand::default()),
},
&server,
)
.unwrap_err()
.to_string();
assert!(error.contains("interactive terminal") || error.contains("TTY"));
assert_eq!(
run_plugins(
PluginsCommand {
command: PluginsSubcommand::List(PluginsListCommand::default()),
},
&server
)
.unwrap(),
ExitCode::SUCCESS
);
assert_eq!(
run_plugins(
PluginsCommand {
command: PluginsSubcommand::Inspect(PluginsInspectCommand {
id: "missing.plugin".into(),
json: false,
}),
},
&server,
)
.unwrap(),
ExitCode::from(2)
);
assert_eq!(
run_plugins(
PluginsCommand {
command: PluginsSubcommand::Validate(PluginsValidateCommand {
target: "missing.plugin".into(),
json: false,
}),
},
&server,
)
.unwrap(),
ExitCode::from(2)
);
assert_eq!(
run_plugins(
PluginsCommand {
command: PluginsSubcommand::List(PluginsListCommand {
all: false,
json: false,
}),
},
&server
)
.unwrap(),
ExitCode::SUCCESS
);
}
#[test]
fn safe_dispatch_plugin_json_errors_return_exit_codes() {
let temp = tempfile::tempdir().unwrap();
let _env = EnvScope::hermetic(&temp);
let config_path = temp.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let server = ServerArgs {
config: Some(config_path),
..ServerArgs::default()
};
assert_eq!(
run_plugins(
PluginsCommand {
command: PluginsSubcommand::Inspect(PluginsInspectCommand {
id: "missing.plugin".into(),
json: true,
}),
},
&server,
)
.unwrap(),
ExitCode::from(2)
);
assert_eq!(
run_plugins(
PluginsCommand {
command: PluginsSubcommand::Validate(PluginsValidateCommand {
target: "missing.plugin".into(),
json: true,
}),
},
&server,
)
.unwrap(),
ExitCode::from(2)
);
}
#[tokio::test]
async fn run_command_dispatches_safe_plugin_and_install_paths() {
let cli = Cli::try_parse_from(["nemo-relay", "plugin-shim", "install", "hermes"]).unwrap();
let error = run_command(cli.command.unwrap(), &cli.server)
.await
.unwrap_err()
.to_string();
assert!(error.contains("plugin install supports codex"));
let cli = Cli::try_parse_from(["nemo-relay", "plugin-shim", "uninstall", "hermes"]).unwrap();
let error = run_command(cli.command.unwrap(), &cli.server)
.await
.unwrap_err()
.to_string();
assert!(error.contains("plugin uninstall supports codex"));
let dir = tempfile::tempdir().unwrap();
let install_dir = dir.path().join("plugin-install");
let install_dir_arg = install_dir.to_string_lossy().to_string();
let cli = Cli::try_parse_from([
"nemo-relay",
"install",
"codex",
"--install-dir",
install_dir_arg.as_str(),
"--dry-run",
"--skip-doctor",
])
.unwrap();
assert_eq!(
run_command(cli.command.unwrap(), &cli.server)
.await
.unwrap(),
ExitCode::SUCCESS
);
let cli = Cli::try_parse_from([
"nemo-relay",
"uninstall",
"codex",
"--install-dir",
install_dir_arg.as_str(),
"--dry-run",
])
.unwrap();
assert_eq!(
run_command(cli.command.unwrap(), &cli.server)
.await
.unwrap(),
ExitCode::SUCCESS
);
}
#[test]
fn pricing_validate_dispatch_covers_success_read_and_parse_errors() {
let dir = tempfile::tempdir().unwrap();
let valid = dir.path().join("pricing.json");
std::fs::write(
&valid,
serde_json::json!({
"version": 1,
"entries": [{
"provider": "test",
"model_id": "model",
"pricing_as_of": "2026-06-04",
"pricing_source": "unit-test",
"rates": {
"input_per_million": 1.0,
"output_per_million": 2.0
},
"prompt_cache": {
"read_accounting": "separate"
}
}]
})
.to_string(),
)
.unwrap();
assert_eq!(
run_pricing(PricingCommand {
command: PricingSubcommand::Validate(PricingValidateCommand {
path: valid.clone(),
}),
})
.unwrap(),
ExitCode::SUCCESS
);
let missing = run_pricing(PricingCommand {
command: PricingSubcommand::Validate(PricingValidateCommand {
path: dir.path().join("missing.json"),
}),
})
.unwrap_err()
.to_string();
assert!(missing.contains("could not read model pricing catalog"));
let invalid = dir.path().join("invalid.json");
std::fs::write(&invalid, "{\"version\":2,\"entries\":[]}").unwrap();
let invalid_error = run_pricing(PricingCommand {
command: PricingSubcommand::Validate(PricingValidateCommand { path: invalid }),
})
.unwrap_err()
.to_string();
assert!(invalid_error.contains("invalid model pricing catalog"));
}