use super::*;
use axum::http::HeaderValue;
use serde_json::json;
fn config() -> GatewayConfig {
GatewayConfig {
bind: "127.0.0.1:0".parse().unwrap(),
openai_base_url: "http://openai".into(),
anthropic_base_url: "http://anthropic".into(),
metadata: None,
plugin_config: None,
}
}
fn isolated_config_path(temp: &tempfile::TempDir) -> std::path::PathBuf {
temp.path().join("config.toml")
}
#[test]
fn session_config_prefers_headers_and_parses_json() {
let mut headers = HeaderMap::new();
headers.insert(
"x-nemo-relay-config-profile",
HeaderValue::from_static("profile-a"),
);
headers.insert(
"x-nemo-relay-session-metadata",
HeaderValue::from_static(r#"{"team":"obs"}"#),
);
headers.insert(
"x-nemo-relay-plugin-config",
HeaderValue::from_static(r#"{"components":[]}"#),
);
headers.insert(
"x-nemo-relay-gateway-mode",
HeaderValue::from_static("required"),
);
let session = config().session_config_from_headers(&headers);
assert_eq!(session.profile.as_deref(), Some("profile-a"));
assert_eq!(session.metadata, Some(json!({ "team": "obs" })));
assert_eq!(session.plugin_config, Some(json!({ "components": [] })));
assert_eq!(session.gateway_mode.as_deref(), Some("required"));
}
#[test]
fn session_config_uses_defaults_and_ignores_bad_json() {
let mut headers = HeaderMap::new();
headers.insert(
"x-nemo-relay-session-metadata",
HeaderValue::from_static("not-json"),
);
headers.insert("x-empty", HeaderValue::from_static(""));
let session = config().session_config_from_headers(&headers);
assert_eq!(session.metadata, None);
assert_eq!(header_string(&headers, "x-empty"), None);
}
#[test]
fn agent_and_gateway_mode_arguments_are_stable() {
assert_eq!(CodingAgent::ClaudeCode.hook_path(), "/hooks/claude-code");
assert_eq!(CodingAgent::Codex.hook_path(), "/hooks/codex");
assert_eq!(CodingAgent::Cursor.hook_path(), "/hooks/cursor");
assert_eq!(CodingAgent::Hermes.hook_path(), "/hooks/hermes");
assert_eq!(GatewayMode::HookOnly.as_arg(), "hook-only");
assert_eq!(GatewayMode::Passthrough.as_arg(), "passthrough");
assert_eq!(GatewayMode::Required.as_arg(), "required");
}
#[test]
fn agent_inference_uses_executable_basename() {
assert_eq!(
CodingAgent::infer("/opt/bin/claude"),
Some(CodingAgent::ClaudeCode)
);
assert_eq!(CodingAgent::infer("codex"), Some(CodingAgent::Codex));
assert_eq!(
CodingAgent::infer("cursor-agent"),
Some(CodingAgent::Cursor)
);
assert_eq!(CodingAgent::infer("hermes"), Some(CodingAgent::Hermes));
assert_eq!(CodingAgent::infer("wrapper"), None);
}
#[test]
fn explicit_toml_config_maps_supported_sections() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("config.toml");
std::fs::write(
&path,
r#"
[upstream]
openai_base_url = "http://openai"
anthropic_base_url = "http://anthropic"
[plugins]
config = { components = [] }
[agents.claude]
command = "claude"
[agents.codex]
command = "codex --approval-mode never"
[agents.cursor]
command = "cursor-agent"
patch_restore_hooks = false
[agents.hermes]
command = "hermes --yolo chat"
"#,
)
.unwrap();
let command = RunCommand {
agent: None,
config: Some(path),
openai_base_url: None,
anthropic_base_url: None,
session_metadata: None,
plugin_config: None,
dry_run: false,
print: false,
command: vec![],
};
let resolved = resolve_run_config(&command, None).unwrap();
assert_eq!(resolved.gateway.bind.to_string(), "127.0.0.1:0");
assert_eq!(resolved.gateway.openai_base_url, "http://openai");
assert_eq!(resolved.gateway.anthropic_base_url, "http://anthropic");
assert_eq!(resolved.gateway.metadata, None);
assert_eq!(
resolved.gateway.plugin_config,
Some(json!({ "components": [] }))
);
assert_eq!(
resolved.agents.codex.command.as_deref(),
Some("codex --approval-mode never")
);
assert_eq!(
resolved.agents.hermes.command.as_deref(),
Some("hermes --yolo chat")
);
assert!(!resolved.agents.cursor.patch_restore_hooks);
}
#[test]
fn legacy_observability_config_sections_fail_clearly() {
let temp = tempfile::tempdir().unwrap();
for (name, contents, expected) in [
(
"exporters.toml",
"[exporters]\natof_dir = \"atof\"\n",
"[exporters]",
),
(
"observability.toml",
"[observability]\natif_dir = \"atif\"\n",
"[observability]",
),
(
"openinference.toml",
"[export.openinference]\nendpoint = \"http://localhost:4318\"\n",
"[export.openinference]",
),
] {
let path = temp.path().join(name);
std::fs::write(&path, contents).unwrap();
let command = RunCommand {
agent: None,
config: Some(path),
openai_base_url: None,
anthropic_base_url: None,
session_metadata: None,
plugin_config: None,
dry_run: false,
print: false,
command: vec![],
};
let error = resolve_run_config(&command, None).unwrap_err().to_string();
assert!(error.contains("legacy observability config"));
assert!(error.contains(expected));
assert!(error.contains("plugins.toml"));
assert!(error.contains("nemo-relay plugins edit"));
}
}
#[test]
fn explicit_plugins_toml_maps_root_plugin_config() {
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.toml");
std::fs::write(
&config_path,
r#"
[upstream]
openai_base_url = "http://openai"
"#,
)
.unwrap();
std::fs::write(
temp.path().join("plugins.toml"),
r#"
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 1
[components.config.atof]
enabled = true
output_directory = "atof"
filename = "events.jsonl"
mode = "overwrite"
"#,
)
.unwrap();
let command = RunCommand {
agent: Some(CodingAgent::Codex),
config: Some(config_path),
openai_base_url: None,
anthropic_base_url: None,
session_metadata: None,
plugin_config: None,
dry_run: false,
print: false,
command: vec!["codex".into()],
};
let resolved = resolve_run_config(&command, None).unwrap();
assert_eq!(
resolved.gateway.plugin_config,
Some(json!({
"version": 1,
"components": [
{
"kind": "observability",
"enabled": true,
"config": {
"version": 1,
"atof": {
"enabled": true,
"output_directory": "atof",
"filename": "events.jsonl",
"mode": "overwrite"
}
}
}
]
}))
);
}
#[test]
fn plugins_toml_path_resolution_tracks_config_scope() {
let temp = tempfile::tempdir().unwrap();
let explicit = temp.path().join("custom-config.toml");
assert_eq!(
plugin_config_paths(Some(&explicit)),
vec![temp.path().join("plugins.toml")]
);
let project = temp.path().join("workspace");
let nested = project.join("a/b/c");
std::fs::create_dir_all(project.join(".nemo-relay")).unwrap();
std::fs::create_dir_all(&nested).unwrap();
let plugin_path = project.join(".nemo-relay/plugins.toml");
std::fs::write(&plugin_path, "version = 1").unwrap();
let user_config = temp.path().join("xdg/nemo-relay");
assert_eq!(find_project_plugin_config(&nested), Some(plugin_path));
assert_eq!(
project_plugin_config_path(&nested),
project.join(".nemo-relay/plugins.toml")
);
assert_eq!(
implicit_plugin_config_paths(Some(&nested), Some(user_config.clone())),
vec![
PathBuf::from("/etc/nemo-relay/plugins.toml"),
project.join(".nemo-relay/plugins.toml"),
user_config.join("plugins.toml"),
]
);
std::fs::remove_file(project.join(".nemo-relay/plugins.toml")).unwrap();
std::fs::write(project.join(".nemo-relay/config.toml"), "").unwrap();
assert_eq!(find_project_plugin_config(&nested), None);
assert_eq!(
project_plugin_config_path(&nested),
project.join(".nemo-relay/plugins.toml")
);
}
#[test]
fn discovered_plugins_toml_upserts_components_by_kind() {
let temp = tempfile::tempdir().unwrap();
let project_plugin = temp.path().join("project-plugins.toml");
let user_plugin = temp.path().join("user-plugins.toml");
std::fs::write(
&project_plugin,
r#"
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 1
[components.config.atof]
enabled = true
filename = "project.jsonl"
[[components]]
kind = "adaptive"
enabled = true
[components.config]
mode = "project-only"
"#,
)
.unwrap();
std::fs::write(
&user_plugin,
r#"
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 1
[components.config.atof]
enabled = true
[components.config.atif]
enabled = true
filename_template = "user-{session_id}.json"
[[components]]
kind = "custom"
enabled = true
[components.config]
source = "user"
"#,
)
.unwrap();
let resolved = load_plugin_toml_config_from_paths(vec![project_plugin, user_plugin]).unwrap();
assert_eq!(
resolved.map(|config| config.value),
Some(json!({
"version": 1,
"components": [
{
"kind": "observability",
"enabled": true,
"config": {
"version": 1,
"atof": {
"enabled": true,
"filename": "project.jsonl"
},
"atif": {
"enabled": true,
"filename_template": "user-{session_id}.json"
}
}
},
{
"kind": "adaptive",
"enabled": true,
"config": {
"mode": "project-only"
}
},
{
"kind": "custom",
"enabled": true,
"config": {
"source": "user"
}
}
]
}))
);
}
#[test]
fn discovered_plugins_toml_can_disable_lower_priority_observability_section() {
let temp = tempfile::tempdir().unwrap();
let project_plugin = temp.path().join("project-plugins.toml");
let user_plugin = temp.path().join("user-plugins.toml");
std::fs::write(
&project_plugin,
r#"
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 1
[components.config.atof]
enabled = true
output_directory = "project-atof"
mode = "overwrite"
"#,
)
.unwrap();
std::fs::write(
&user_plugin,
r#"
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 1
[components.config.atof]
enabled = false
mode = "append"
"#,
)
.unwrap();
let resolved = load_plugin_toml_config_from_paths(vec![project_plugin, user_plugin]).unwrap();
assert_eq!(
resolved.map(|config| config.value),
Some(json!({
"version": 1,
"components": [
{
"kind": "observability",
"enabled": true,
"config": {
"version": 1,
"atof": {
"enabled": false,
"output_directory": "project-atof",
"mode": "append"
}
}
}
]
}))
);
}
#[test]
fn plugins_toml_rejects_duplicate_component_kinds_per_file() {
let temp = tempfile::tempdir().unwrap();
let plugin_path = temp.path().join("plugins.toml");
std::fs::write(
&plugin_path,
r#"
version = 1
[[components]]
kind = "observability"
[[components]]
kind = "observability"
"#,
)
.unwrap();
let error = load_plugin_toml_config_from_paths(vec![plugin_path])
.unwrap_err()
.to_string();
assert!(error.contains("duplicate plugin component kind"));
assert!(error.contains("observability"));
}
#[test]
fn plugins_toml_conflicts_with_config_toml_plugins_config() {
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.toml");
std::fs::write(
&config_path,
r#"
[plugins]
config = { version = 1, components = [] }
"#,
)
.unwrap();
std::fs::write(temp.path().join("plugins.toml"), "version = 1\n").unwrap();
let args = ServerArgs {
config: Some(config_path),
..ServerArgs::default()
};
let error = resolve_server_config(&args).unwrap_err().to_string();
assert!(error.contains("plugin config is defined in both"));
assert!(error.contains("config.toml"));
assert!(error.contains("plugins.toml"));
}
#[test]
fn cli_plugin_config_conflicts_with_file_plugin_config() {
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
std::fs::write(temp.path().join("plugins.toml"), "version = 1\n").unwrap();
let command = RunCommand {
agent: Some(CodingAgent::Codex),
config: Some(config_path),
openai_base_url: None,
anthropic_base_url: None,
session_metadata: None,
plugin_config: Some(r#"{"version":1,"components":[]}"#.into()),
dry_run: false,
print: false,
command: vec!["codex".into()],
};
let error = resolve_run_config(&command, None).unwrap_err().to_string();
assert!(error.contains("--plugin-config"));
assert!(error.contains("file configuration"));
}
#[test]
fn cli_run_overrides_config_values() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("config.toml");
std::fs::write(
&path,
r#"
[upstream]
openai_base_url = "http://file-openai"
"#,
)
.unwrap();
let command = RunCommand {
agent: Some(CodingAgent::Codex),
config: Some(path),
openai_base_url: Some("http://cli-openai".into()),
anthropic_base_url: None,
session_metadata: Some(r#"{"team":"cli"}"#.into()),
plugin_config: None,
dry_run: false,
print: false,
command: vec!["codex".into()],
};
let resolved = resolve_run_config(&command, None).unwrap();
assert_eq!(resolved.gateway.openai_base_url, "http://cli-openai");
assert_eq!(resolved.gateway.metadata, Some(json!({ "team": "cli" })));
}
#[test]
fn run_inherits_top_level_server_flags_when_subcommand_flags_are_absent() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("config.toml");
std::fs::write(
&path,
r#"
[upstream]
openai_base_url = "http://file-openai"
"#,
)
.unwrap();
let server = ServerArgs {
config: Some(path),
openai_base_url: Some("http://top-level-openai".into()),
..ServerArgs::default()
};
let command = RunCommand {
agent: Some(CodingAgent::Codex),
config: None,
openai_base_url: None,
anthropic_base_url: None,
session_metadata: None,
plugin_config: None,
dry_run: false,
print: false,
command: vec!["codex".into()],
};
let resolved = resolve_run_config(&command, Some(&server)).unwrap();
assert_eq!(resolved.gateway.openai_base_url, "http://top-level-openai");
}
#[test]
fn run_plugin_config_overrides_inherited_top_level_plugin_config() {
let temp = tempfile::tempdir().unwrap();
let server = ServerArgs {
config: Some(isolated_config_path(&temp)),
plugin_config: Some(r#"{"components":["top-level"]}"#.into()),
..ServerArgs::default()
};
let command = RunCommand {
agent: Some(CodingAgent::Codex),
config: None,
openai_base_url: None,
anthropic_base_url: None,
session_metadata: None,
plugin_config: Some(r#"{"components":["run"]}"#.into()),
dry_run: false,
print: false,
command: vec!["codex".into()],
};
let resolved = resolve_run_config(&command, Some(&server)).unwrap();
assert_eq!(
resolved.gateway.plugin_config,
Some(json!({ "components": ["run"] }))
);
}
#[test]
fn server_resolution_applies_all_server_overrides() {
let temp = tempfile::tempdir().unwrap();
let args = ServerArgs {
config: Some(isolated_config_path(&temp)),
bind: Some("127.0.0.1:0".parse().unwrap()),
openai_base_url: Some("http://cli-openai".into()),
anthropic_base_url: Some("http://cli-anthropic".into()),
plugin_config: Some(r#"{"version":1,"components":[]}"#.into()),
};
let resolved = resolve_server_config(&args).unwrap();
assert_eq!(resolved.gateway.bind.to_string(), "127.0.0.1:0");
assert_eq!(resolved.gateway.openai_base_url, "http://cli-openai");
assert_eq!(resolved.gateway.anthropic_base_url, "http://cli-anthropic");
assert_eq!(
resolved.gateway.plugin_config,
Some(json!({ "version": 1, "components": [] }))
);
assert!(args.requested_daemon_mode());
}
#[test]
fn run_resolution_applies_all_run_overrides() {
let temp = tempfile::tempdir().unwrap();
let command = RunCommand {
agent: Some(CodingAgent::Codex),
config: Some(isolated_config_path(&temp)),
openai_base_url: Some("http://run-openai".into()),
anthropic_base_url: Some("http://run-anthropic".into()),
session_metadata: Some(r#"{"team":"run"}"#.into()),
plugin_config: Some(r#"{"components":["x"]}"#.into()),
dry_run: false,
print: false,
command: vec!["codex".into()],
};
let resolved = resolve_run_config(&command, None).unwrap();
assert_eq!(resolved.gateway.openai_base_url, "http://run-openai");
assert_eq!(resolved.gateway.anthropic_base_url, "http://run-anthropic");
assert_eq!(resolved.gateway.metadata, Some(json!({ "team": "run" })));
assert_eq!(
resolved.gateway.plugin_config,
Some(json!({ "components": ["x"] }))
);
}
#[test]
fn malformed_shared_config_reports_context() {
let temp = tempfile::tempdir().unwrap();
let invalid_toml = temp.path().join("invalid.toml");
std::fs::write(&invalid_toml, "server = [").unwrap();
let args = ServerArgs {
config: Some(invalid_toml),
..ServerArgs::default()
};
let error = resolve_server_config(&args).unwrap_err().to_string();
assert!(error.contains("invalid TOML"));
let invalid_shape = temp.path().join("invalid-shape.toml");
std::fs::write(&invalid_shape, "upstream = \"not-a-table\"").unwrap();
let args = ServerArgs {
config: Some(invalid_shape),
..ServerArgs::default()
};
let error = resolve_server_config(&args).unwrap_err().to_string();
assert!(error.contains("invalid gateway configuration shape"));
let plugin_config = temp.path().join("config-with-invalid-plugins.toml");
std::fs::write(&plugin_config, "").unwrap();
std::fs::write(temp.path().join("plugins.toml"), "version = [").unwrap();
let args = ServerArgs {
config: Some(plugin_config),
..ServerArgs::default()
};
let error = resolve_server_config(&args).unwrap_err().to_string();
assert!(error.contains("invalid plugin TOML"));
}
#[test]
fn recursive_toml_merge_replaces_scalars_and_preserves_tables() {
let mut left: toml::Value = r#"
[upstream]
openai_base_url = "http://old"
anthropic_base_url = "http://anthropic"
[plugins.config]
version = 1
policy = { unknown_component = "warn", unknown_field = "warn" }
"#
.parse::<toml::Table>()
.map(toml::Value::Table)
.unwrap();
let right: toml::Value = r#"
[upstream]
openai_base_url = "http://new"
[plugins.config.policy]
unknown_component = "error"
"#
.parse::<toml::Table>()
.map(toml::Value::Table)
.unwrap();
merge_toml(&mut left, right);
assert_eq!(
left["upstream"]["openai_base_url"].as_str(),
Some("http://new")
);
assert_eq!(
left["upstream"]["anthropic_base_url"].as_str(),
Some("http://anthropic")
);
assert_eq!(
left["plugins"]["config"]["policy"]["unknown_component"].as_str(),
Some("error")
);
assert_eq!(
left["plugins"]["config"]["policy"]["unknown_field"].as_str(),
Some("warn")
);
}