use super::*;
use crate::config::{global_plugin_config_path, project_plugin_config_path};
use nemo_relay::config_editor::{EditorConfig, EditorSchema};
use nemo_relay::observability::plugin_component::{OBSERVABILITY_PLUGIN_KIND, ObservabilityConfig};
use nemo_relay::plugin::{ConfigPolicy, PluginComponentSpec, PluginConfig};
use nemo_relay::plugins::nemo_guardrails::component::{
NEMO_GUARDRAILS_PLUGIN_KIND, NeMoGuardrailsConfig, RemoteBackendConfig,
};
use nemo_relay_adaptive::AdaptiveConfig;
use nemo_relay_adaptive::plugin_component::ADAPTIVE_PLUGIN_KIND;
fn adaptive_component_config(agent_id: &str) -> serde_json::Map<String, Value> {
json!({
"agent_id": agent_id,
"state": {
"backend": {
"kind": "in_memory",
"config": {}
}
},
"telemetry": {
"learners": ["tool_parallelism"]
},
"adaptive_hints": {
"priority": 100,
"break_chain": false,
"inject_header": true,
"inject_body_path": "nvext.agent_hints"
}
})
.as_object()
.unwrap()
.clone()
}
fn guardrails_component_config(config_id: &str) -> serde_json::Map<String, Value> {
json!({
"mode": "remote",
"codec": "openai_chat",
"remote": {
"endpoint": "http://localhost:8000",
"config_id": config_id
}
})
.as_object()
.unwrap()
.clone()
}
#[test]
fn target_scope_defaults_to_user_and_rejects_conflicts() {
assert_eq!(
target_scope(&PluginsEditCommand::default()).unwrap(),
TargetScope::User
);
assert_eq!(
target_scope(&PluginsEditCommand {
project: true,
..PluginsEditCommand::default()
})
.unwrap(),
TargetScope::Project
);
assert_eq!(
target_scope(&PluginsEditCommand {
global: true,
..PluginsEditCommand::default()
})
.unwrap(),
TargetScope::Global
);
let error = target_scope(&PluginsEditCommand {
user: true,
project: true,
..PluginsEditCommand::default()
})
.unwrap_err()
.to_string();
assert!(error.contains("choose only one"), "error was: {error}");
}
#[test]
fn typed_editor_model_contains_observability_sections() {
let schema = ObservabilityConfig::editor_schema();
let atof = schema.field("atof").unwrap().schema().unwrap();
let atif = schema.field("atif").unwrap().schema().unwrap();
let openinference = schema.field("openinference").unwrap().schema().unwrap();
assert!(atof.fields.iter().any(|field| field.name == "mode"));
assert!(
atif.fields
.iter()
.any(|field| field.name == "filename_template")
);
assert!(
openinference
.fields
.iter()
.any(|field| field.name == "endpoint")
);
}
#[test]
fn typed_editor_model_contains_adaptive_options() {
let schema = AdaptiveConfig::editor_schema();
assert!(!schema.fields.iter().any(|field| field.name == "version"));
let agent_id = schema.field("agent_id").unwrap();
assert_eq!(agent_id.label, "fallback_agent_id");
let state = schema.field("state").unwrap().schema().unwrap();
let backend = state.field("backend").unwrap().schema().unwrap();
assert_eq!(
backend.field("kind").unwrap().enum_values,
&["in_memory", "redis"]
);
assert_eq!(backend.field("config").unwrap().kind, EditorFieldKind::Json);
let telemetry = schema.field("telemetry").unwrap().schema().unwrap();
assert_eq!(
telemetry.field("learners").unwrap().kind,
EditorFieldKind::Json
);
let acg = schema.field("acg").unwrap().schema().unwrap();
let thresholds = acg.field("stability_thresholds").unwrap().schema().unwrap();
assert_eq!(
thresholds.field("stable_threshold").unwrap().kind,
EditorFieldKind::Float
);
}
#[test]
fn typed_editor_model_contains_nemo_guardrails_options() {
let schema = NeMoGuardrailsConfig::editor_schema();
assert!(!schema.fields.iter().any(|field| field.name == "version"));
assert_eq!(
schema.field("mode").unwrap().enum_values,
&["remote", "local"]
);
assert_eq!(schema.field("codec").unwrap().kind, EditorFieldKind::Enum);
assert_eq!(
schema.field("input").unwrap().kind,
EditorFieldKind::Boolean
);
assert_eq!(
schema.field("priority").unwrap().kind,
EditorFieldKind::Integer
);
let remote = schema.field("remote").unwrap().schema().unwrap();
assert_eq!(
remote.field("timeout_millis").unwrap().kind,
EditorFieldKind::Integer
);
assert_eq!(
remote.field("headers").unwrap().kind,
EditorFieldKind::StringMap
);
let request_defaults = schema.field("request_defaults").unwrap().schema().unwrap();
let rails = request_defaults.field("rails").unwrap().schema().unwrap();
assert_eq!(
rails.field("tool_input").unwrap().kind,
EditorFieldKind::Json
);
}
#[test]
fn plugin_menu_uses_setup_theme_markers() {
let theme = ColorfulTheme::default();
let lines = render_menu(
&theme,
"plugins.toml",
&[MenuItem::new("First"), MenuItem::new("Second")],
0,
);
let rendered = lines.join("\n");
assert!(rendered.contains('?'));
assert!(rendered.contains('›'));
assert!(rendered.contains('❯'));
assert!(rendered.contains("↑/↓"));
assert!(!rendered.contains("> First"));
}
#[test]
fn plugin_menu_builds_ordered_component_actions() {
let temp = tempfile::tempdir().unwrap();
let config = PluginConfig::default();
let components = editable_components(&config).unwrap();
let (items, actions) = plugin_menu_items(&components, &temp.path().join("plugins.toml"));
assert_eq!(items.len(), actions.len());
assert_eq!(items[0].label, "Toggle Observability component [on]");
assert_eq!(items[1].label, " Edit Observability ATOF");
assert!(items.iter().any(|item| {
item.label
.contains("Toggle NeMo Guardrails component [off]")
}));
assert!(matches!(actions[0], MenuAction::ToggleComponent(0)));
assert!(matches!(
actions[1],
MenuAction::EditField {
component_index: 0,
field_index: 0
}
));
assert!(matches!(actions[actions.len() - 3], MenuAction::Preview));
assert!(matches!(actions[actions.len() - 2], MenuAction::Save));
assert!(matches!(actions[actions.len() - 1], MenuAction::Cancel));
}
#[test]
fn component_enablement_shortcuts_clear_and_reset_differ() {
let config = PluginConfig::default();
let mut components = editable_components(&config).unwrap();
let observability = components
.iter_mut()
.find(|component| component.label() == "Observability")
.unwrap();
observability.set_enabled(false);
apply_component_enablement_shortcut(observability, MenuShortcut::Reset);
assert!(observability.enabled());
apply_component_enablement_shortcut(observability, MenuShortcut::Clear);
assert!(!observability.enabled());
let adaptive = components
.iter_mut()
.find(|component| component.label() == "Adaptive")
.unwrap();
adaptive.set_enabled(true);
apply_component_enablement_shortcut(adaptive, MenuShortcut::Reset);
assert!(!adaptive.enabled());
}
#[test]
fn menu_response_index_tracks_selected_and_shortcut_positions() {
assert_eq!(menu_response_index(&MenuResponse::Selected(3)), Some(3));
assert_eq!(
menu_response_index(&MenuResponse::Shortcut(MenuShortcut::Reset, 4)),
Some(4)
);
assert_eq!(menu_response_index(&MenuResponse::Cancel), None);
}
#[test]
fn plugin_menu_marks_configured_sections_and_fields() {
let mut observability = ObservabilityConfig::default();
let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
let mode = atof.schema().unwrap().field("mode").unwrap();
let output_directory = atof.schema().unwrap().field("output_directory").unwrap();
assert!(!section_configured(&observability, atof));
ensure_section(&mut observability, atof);
assert!(section_configured(&observability, atof));
assert!(!section_field_configured(&observability, atof, mode).unwrap());
assert!(!section_field_configured(&observability, atof, output_directory).unwrap());
set_section_field(&mut observability, atof, "output_directory", json!("logs")).unwrap();
assert!(section_field_configured(&observability, atof, output_directory).unwrap());
assert!(configured_label(true, "Edit ATOF").contains('✓'));
assert!(!configured_label(false, "Edit ATIF").contains('✓'));
}
#[test]
fn editor_model_renders_valid_observability_plugin_config() {
let mut config = PluginConfig::default();
ensure_observability_component(&mut config).unwrap();
let mut observability = component_observability_state(&config).unwrap();
let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
toggle_section(&mut observability.config, atof);
set_section_field(
&mut observability.config,
atof,
"output_directory",
json!("logs"),
)
.unwrap();
set_section_field(
&mut observability.config,
atof,
"filename",
json!("events.jsonl"),
)
.unwrap();
store_observability_state(&mut config, &observability).unwrap();
validate_config(&config).unwrap();
}
#[test]
fn editor_model_adds_disabled_adaptive_component() {
let mut config = PluginConfig::default();
ensure_adaptive_component(&mut config).unwrap();
let component = config
.components
.iter()
.find(|component| component.kind == ADAPTIVE_PLUGIN_KIND)
.unwrap();
assert_eq!(component.kind, ADAPTIVE_PLUGIN_KIND);
assert!(!component.enabled);
assert!(!component.config.contains_key("version"));
assert!(component.config.contains_key("policy"));
}
#[test]
fn editor_model_reads_missing_nemo_guardrails_component_as_disabled_default() {
let config = PluginConfig::default();
let guardrails = component_nemo_guardrails_state(&config).unwrap();
assert!(!guardrails.enabled);
assert!(
!config
.components
.iter()
.any(|component| component.kind == NEMO_GUARDRAILS_PLUGIN_KIND)
);
assert_eq!(guardrails.config.mode, "remote");
assert!(!nemo_guardrails_configured(&guardrails.config));
assert_eq!(
nemo_guardrails_summary(&guardrails),
"component disabled, fields none"
);
assert!(!guardrails.should_store(nemo_guardrails_configured(&guardrails.config)));
}
#[test]
fn editor_save_persists_disabled_nemo_guardrails_policy_only_edits() {
let mut config = PluginConfig::default();
let mut guardrails = component_nemo_guardrails_state(&config).unwrap();
let policy = NeMoGuardrailsConfig::editor_schema()
.field("policy")
.unwrap();
set_section_field(
&mut guardrails.config,
policy,
"unknown_field",
json!("ignore"),
)
.unwrap();
guardrails.mark_config_touched();
assert!(!guardrails.enabled);
assert!(!nemo_guardrails_configured(&guardrails.config));
store_nemo_guardrails_state(&mut config, &guardrails).unwrap();
let component = config
.components
.iter()
.find(|component| component.kind == NEMO_GUARDRAILS_PLUGIN_KIND)
.unwrap();
assert!(!component.enabled);
assert_eq!(component.config["policy"]["unknown_field"], json!("ignore"));
}
#[test]
fn typed_editor_serializes_explicit_observability_overrides() {
let mut observability = ObservabilityConfig::default();
let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
toggle_section(&mut observability, atof);
set_section_field(&mut observability, atof, "output_directory", json!("logs")).unwrap();
let map = observability_config_map(&observability).unwrap();
let atof = map
.get("atof")
.and_then(Value::as_object)
.expect("atof section is serialized");
assert_eq!(atof.get("enabled"), Some(&Value::Bool(true)));
assert_eq!(atof.get("output_directory"), Some(&json!("logs")));
assert_eq!(atof.get("mode"), Some(&json!("append")));
assert!(map.contains_key("policy"));
}
#[test]
fn typed_editor_serializes_disabled_section_override() {
let mut observability = ObservabilityConfig::default();
let atif = ObservabilityConfig::editor_schema().field("atif").unwrap();
toggle_section(&mut observability, atif);
toggle_section(&mut observability, atif);
let map = observability_config_map(&observability).unwrap();
let atif = map
.get("atif")
.and_then(Value::as_object)
.expect("disabled atif section is serialized");
assert_eq!(atif.get("enabled"), Some(&Value::Bool(false)));
assert_eq!(
atif.get("filename_template"),
Some(&json!("nemo-relay-atif-{session_id}.json"))
);
}
#[test]
fn editor_save_preserves_unknown_observability_fields() {
let mut config = PluginConfig {
components: vec![PluginComponentSpec {
kind: OBSERVABILITY_PLUGIN_KIND.to_string(),
enabled: true,
config: json!({
"version": 1,
"future_top_level": "preserve",
"atof": {
"enabled": true,
"output_directory": "old-logs",
"future_atof_field": "preserve"
}
})
.as_object()
.unwrap()
.clone(),
}],
..PluginConfig::default()
};
let mut observability = component_observability_state(&config).unwrap();
let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
remove_section_field(&mut observability.config, atof, "output_directory").unwrap();
set_section_field(
&mut observability.config,
atof,
"filename",
json!("events.jsonl"),
)
.unwrap();
store_observability_state(&mut config, &observability).unwrap();
let component = config
.components
.iter()
.find(|component| component.kind == OBSERVABILITY_PLUGIN_KIND)
.unwrap();
assert_eq!(
component.config.get("future_top_level"),
Some(&json!("preserve"))
);
let atof_config = component
.config
.get("atof")
.and_then(Value::as_object)
.unwrap();
assert_eq!(
atof_config.get("future_atof_field"),
Some(&json!("preserve"))
);
assert_eq!(atof_config.get("filename"), Some(&json!("events.jsonl")));
assert!(!atof_config.contains_key("output_directory"));
}
#[test]
fn editor_save_preserves_unknown_adaptive_fields_and_all_sections() {
let mut config = PluginConfig {
components: vec![PluginComponentSpec {
kind: ADAPTIVE_PLUGIN_KIND.to_string(),
enabled: true,
config: json!({
"version": 1,
"future_top_level": "preserve",
"state": {
"future_state": "preserve",
"backend": {
"kind": "in_memory",
"config": {},
"future_backend": "preserve"
}
}
})
.as_object()
.unwrap()
.clone(),
}],
..PluginConfig::default()
};
let mut adaptive = component_adaptive_state(&config).unwrap();
let schema = AdaptiveConfig::editor_schema();
let state = schema.field("state").unwrap();
let telemetry = schema.field("telemetry").unwrap();
let adaptive_hints = schema.field("adaptive_hints").unwrap();
let tool_parallelism = schema.field("tool_parallelism").unwrap();
let acg = schema.field("acg").unwrap();
set_struct_field(&mut adaptive.config, "agent_id", json!("planner")).unwrap();
set_section_field(
&mut adaptive.config,
state,
"backend",
json!({
"kind": "redis",
"config": {
"url": "redis://127.0.0.1/",
"key_prefix": "adaptive:"
}
}),
)
.unwrap();
set_section_field(
&mut adaptive.config,
telemetry,
"learners",
json!(["tool_parallelism", "acg"]),
)
.unwrap();
set_section_field(
&mut adaptive.config,
telemetry,
"subscriber_name",
json!("adaptive"),
)
.unwrap();
set_section_field(
&mut adaptive.config,
adaptive_hints,
"inject_body_path",
json!("nvext.agent_hints"),
)
.unwrap();
set_section_field(
&mut adaptive.config,
tool_parallelism,
"mode",
json!("inject_hints"),
)
.unwrap();
set_section_field(&mut adaptive.config, acg, "provider", json!("anthropic")).unwrap();
set_section_field(
&mut adaptive.config,
acg,
"stability_thresholds",
json!({
"stable_threshold": 0.9,
"semi_stable_threshold": 0.4,
"min_observations_for_full_confidence": 10
}),
)
.unwrap();
store_adaptive_state(&mut config, &adaptive).unwrap();
let component = config
.components
.iter()
.find(|component| component.kind == ADAPTIVE_PLUGIN_KIND)
.unwrap();
assert!(!component.config.contains_key("version"));
assert_eq!(
component.config.get("future_top_level"),
Some(&json!("preserve"))
);
let state = component.config["state"].as_object().unwrap();
assert_eq!(state.get("future_state"), Some(&json!("preserve")));
let backend = state["backend"].as_object().unwrap();
assert_eq!(backend.get("kind"), Some(&json!("redis")));
assert_eq!(backend.get("future_backend"), Some(&json!("preserve")));
assert_eq!(backend["config"]["key_prefix"], json!("adaptive:"));
assert_eq!(
component.config["telemetry"]["learners"],
json!(["tool_parallelism", "acg"])
);
assert_eq!(
component.config["adaptive_hints"]["inject_body_path"],
json!("nvext.agent_hints")
);
assert_eq!(
component.config["tool_parallelism"]["mode"],
json!("inject_hints")
);
assert_eq!(
component.config["acg"]["stability_thresholds"]["stable_threshold"],
json!(0.9)
);
}
#[test]
fn editor_save_preserves_unknown_nemo_guardrails_fields_and_sections() {
let mut config = PluginConfig {
components: vec![PluginComponentSpec {
kind: NEMO_GUARDRAILS_PLUGIN_KIND.to_string(),
enabled: true,
config: json!({
"version": 1,
"future_top_level": "preserve",
"mode": "remote",
"codec": "openai_chat",
"remote": {
"endpoint": "http://old.example.test",
"config_id": "old",
"future_remote": "preserve"
},
"request_defaults": {
"future_defaults": "preserve",
"rails": {
"input": true,
"future_rails": "preserve"
}
}
})
.as_object()
.unwrap()
.clone(),
}],
..PluginConfig::default()
};
let mut guardrails = component_nemo_guardrails_state(&config).unwrap();
let schema = NeMoGuardrailsConfig::editor_schema();
let remote = schema.field("remote").unwrap();
let request_defaults = schema.field("request_defaults").unwrap();
set_struct_field(&mut guardrails.config, "codec", json!("openai_chat")).unwrap();
set_section_field(
&mut guardrails.config,
remote,
"endpoint",
json!("http://localhost:8000"),
)
.unwrap();
set_section_field(
&mut guardrails.config,
remote,
"config_id",
json!("default"),
)
.unwrap();
set_section_field(
&mut guardrails.config,
request_defaults,
"context",
json!({"tenant": "docs"}),
)
.unwrap();
guardrails.set_enabled(false);
store_nemo_guardrails_state(&mut config, &guardrails).unwrap();
let component = config
.components
.iter()
.find(|component| component.kind == NEMO_GUARDRAILS_PLUGIN_KIND)
.unwrap();
assert!(!component.enabled);
assert!(!component.config.contains_key("version"));
assert_eq!(
component.config.get("future_top_level"),
Some(&json!("preserve"))
);
let remote = component.config["remote"].as_object().unwrap();
assert_eq!(
remote.get("endpoint"),
Some(&json!("http://localhost:8000"))
);
assert_eq!(remote.get("future_remote"), Some(&json!("preserve")));
let request_defaults = component.config["request_defaults"].as_object().unwrap();
assert_eq!(
request_defaults.get("future_defaults"),
Some(&json!("preserve"))
);
assert_eq!(request_defaults["context"], json!({"tenant": "docs"}));
assert_eq!(request_defaults["rails"]["future_rails"], json!("preserve"));
}
#[test]
fn adaptive_config_field_reset_handles_optional_and_default_fields() {
let mut adaptive = AdaptiveConfig {
agent_id: Some("planner".into()),
acg: Some(Default::default()),
..AdaptiveConfig::default()
};
let schema = AdaptiveConfig::editor_schema();
reset_config_field(&mut adaptive, schema.field("agent_id").unwrap()).unwrap();
reset_config_field(&mut adaptive, schema.field("acg").unwrap()).unwrap();
assert!(adaptive.agent_id.is_none());
assert!(adaptive.acg.is_none());
}
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
struct OptionalSectionHarness {
#[serde(default, skip_serializing_if = "Option::is_none")]
optional: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
parent: Option<Value>,
}
fn optional_section_without_default(name: &'static str) -> EditorFieldSpec {
EditorFieldSpec {
name,
label: name,
kind: EditorFieldKind::Section,
enum_values: &[],
optional: true,
nested_schema: None,
nested_default: None,
}
}
fn depth_root_schema() -> &'static EditorSchema {
static FIELDS: [EditorFieldSpec; 1] = [EditorFieldSpec {
name: "middle",
label: "middle",
kind: EditorFieldKind::Section,
enum_values: &[],
optional: false,
nested_schema: Some(depth_middle_schema),
nested_default: None,
}];
static SCHEMA: EditorSchema = EditorSchema { fields: &FIELDS };
&SCHEMA
}
fn depth_middle_schema() -> &'static EditorSchema {
static FIELDS: [EditorFieldSpec; 1] = [EditorFieldSpec {
name: "leaf",
label: "leaf",
kind: EditorFieldKind::Section,
enum_values: &[],
optional: false,
nested_schema: Some(depth_leaf_schema),
nested_default: None,
}];
static SCHEMA: EditorSchema = EditorSchema { fields: &FIELDS };
&SCHEMA
}
fn depth_leaf_schema() -> &'static EditorSchema {
static FIELDS: [EditorFieldSpec; 1] = [EditorFieldSpec {
name: "name",
label: "name",
kind: EditorFieldKind::String,
enum_values: &[],
optional: false,
nested_schema: None,
nested_default: None,
}];
static SCHEMA: EditorSchema = EditorSchema { fields: &FIELDS };
&SCHEMA
}
#[test]
fn reset_section_clears_optional_section_without_default() {
let section = optional_section_without_default("optional");
let mut config = OptionalSectionHarness {
optional: Some(json!({})),
parent: None,
};
reset_section(&mut config, section);
assert!(config.optional.is_none());
}
#[test]
fn nested_edit_empty_optional_section_without_default_clears_field() {
let optional = optional_section_without_default("optional");
let parent = optional_section_without_default("parent");
let child = optional_section_without_default("child");
let mut config = OptionalSectionHarness {
optional: Some(json!({ "old": true })),
parent: Some(json!({
"child": {},
"kept": true
})),
};
store_edited_config_section(&mut config, optional, json!({})).unwrap();
assert!(config.optional.is_none());
store_edited_section_field(&mut config, parent, child, json!({})).unwrap();
let parent = config.parent.as_ref().unwrap().as_object().unwrap();
assert!(!parent.contains_key("child"));
assert_eq!(parent.get("kept"), Some(&json!(true)));
let mut value = json!({ "child": {}, "kept": true });
store_edited_value_section(&mut value, child, json!({}));
let value = value.as_object().unwrap();
assert!(!value.contains_key("child"));
assert_eq!(value.get("kept"), Some(&json!(true)));
}
#[test]
fn recursive_value_defaults_flow_from_parent_objects() {
let middle = depth_root_schema().field("middle").unwrap();
let leaf = depth_middle_schema().field("leaf").unwrap();
let default = json!({
"middle": {
"leaf": {
"name": "from-parent"
}
}
});
let middle_default = value_field_default(Some(&default), middle).unwrap();
assert_eq!(
value_field_default(Some(&middle_default), leaf),
Some(json!({ "name": "from-parent" }))
);
let mut root_value = json!({ "middle": { "leaf": { "name": "custom" } } });
reset_value_field(&mut root_value, middle, Some(&default));
assert_eq!(root_value["middle"]["leaf"]["name"], json!("from-parent"));
let mut middle_value = json!({ "leaf": { "name": "custom" } });
reset_value_field(&mut middle_value, leaf, Some(&middle_default));
assert_eq!(middle_value["leaf"]["name"], json!("from-parent"));
}
#[test]
fn merge_known_editor_object_recurses_through_arbitrary_section_depth() {
let schema = depth_root_schema();
let mut existing = json!({
"middle": {
"leaf": {
"name": "old",
"future_leaf": "preserve"
},
"future_middle": "preserve"
},
"future_root": "preserve"
})
.as_object()
.unwrap()
.clone();
let edited = json!({
"middle": {
"leaf": {
"name": "new"
}
}
})
.as_object()
.unwrap()
.clone();
merge_known_editor_object(&mut existing, edited, &nested_editor_keys(schema), schema);
let middle = existing.get("middle").unwrap().as_object().unwrap();
let leaf = middle.get("leaf").unwrap().as_object().unwrap();
assert_eq!(leaf.get("name"), Some(&json!("new")));
assert_eq!(leaf.get("future_leaf"), Some(&json!("preserve")));
assert_eq!(middle.get("future_middle"), Some(&json!("preserve")));
assert_eq!(existing.get("future_root"), Some(&json!("preserve")));
}
#[test]
fn observability_config_field_reset_clears_optional_section() {
let mut observability = ObservabilityConfig::default();
let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
toggle_section(&mut observability, atof);
reset_config_field(&mut observability, atof).unwrap();
assert!(observability.atof.is_none());
}
#[test]
fn adaptive_summary_tracks_component_and_configured_fields() {
let mut config = PluginConfig::default();
ensure_adaptive_component(&mut config).unwrap();
let mut adaptive = component_adaptive_state(&config).unwrap();
assert_eq!(
adaptive_summary(&adaptive),
"component disabled, fields none"
);
adaptive.set_enabled(true);
set_struct_field(&mut adaptive.config, "agent_id", json!("planner")).unwrap();
let adaptive_hints = AdaptiveConfig::editor_schema()
.field("adaptive_hints")
.unwrap();
set_section_field(
&mut adaptive.config,
adaptive_hints,
"inject_header",
json!(true),
)
.unwrap();
assert_eq!(
adaptive_summary(&adaptive),
"component enabled, fields fallback_agent_id, adaptive_hints"
);
}
#[test]
fn nemo_guardrails_summary_tracks_component_and_configured_fields() {
let config = PluginConfig::default();
let mut guardrails = component_nemo_guardrails_state(&config).unwrap();
assert_eq!(
nemo_guardrails_summary(&guardrails),
"component disabled, fields none"
);
guardrails.set_enabled(true);
set_struct_field(&mut guardrails.config, "codec", json!("openai_chat")).unwrap();
let remote = NeMoGuardrailsConfig::editor_schema()
.field("remote")
.unwrap();
set_section_field(
&mut guardrails.config,
remote,
"endpoint",
json!("http://localhost:8000"),
)
.unwrap();
assert!(nemo_guardrails_configured(&guardrails.config));
assert_eq!(
nemo_guardrails_summary(&guardrails),
"component enabled, fields codec, remote"
);
assert!(guardrails.should_store(nemo_guardrails_configured(&guardrails.config)));
let existing = PluginConfig {
components: vec![PluginComponentSpec {
kind: NEMO_GUARDRAILS_PLUGIN_KIND.to_string(),
enabled: false,
config: guardrails_component_config("existing"),
}],
..PluginConfig::default()
};
let mut existing = component_nemo_guardrails_state(&existing).unwrap();
reset_config_field(
&mut existing.config,
NeMoGuardrailsConfig::editor_schema()
.field("remote")
.unwrap(),
)
.unwrap();
assert!(existing.should_store(nemo_guardrails_configured(&existing.config)));
}
#[test]
fn component_enablement_and_summary_track_config_state() {
let mut config = PluginConfig::default();
ensure_observability_component(&mut config).unwrap();
let mut observability = component_observability_state(&config).unwrap();
assert!(observability.enabled);
assert_eq!(
observability_summary(&observability),
"component enabled, sections none"
);
observability.set_enabled(false);
let atif = ObservabilityConfig::editor_schema().field("atif").unwrap();
toggle_section(&mut observability.config, atif);
assert!(!observability.enabled);
assert_eq!(
observability_summary(&observability),
"component disabled, sections ATIF"
);
}
#[test]
fn reset_selected_field_accounts_for_section_toggle_offset() {
let mut observability = ObservabilityConfig::default();
let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
let fields = atof.schema().unwrap().fields;
set_section_field(&mut observability, atof, "output_directory", json!("logs")).unwrap();
assert!(
section_field_value(&observability, atof, "output_directory")
.unwrap()
.is_some()
);
let output_directory_index = fields
.iter()
.position(|field| field.name == "output_directory")
.unwrap();
assert!(
reset_selected_field(&mut observability, atof, fields, output_directory_index + 1,)
.unwrap()
);
assert_eq!(
section_field_value(&observability, atof, "output_directory").unwrap(),
None
);
assert!(!reset_selected_field(&mut observability, atof, fields, 0).unwrap());
}
#[test]
fn read_plugin_config_handles_missing_and_invalid_files() {
let temp = tempfile::tempdir().unwrap();
let missing = temp.path().join("plugins.toml");
let config = read_plugin_config(&missing).unwrap();
assert!(config.components.is_empty());
std::fs::write(&missing, "components = [\n").unwrap();
let error = read_plugin_config(&missing).unwrap_err().to_string();
assert!(error.contains("invalid plugin TOML"), "error was: {error}");
}
#[test]
fn write_plugin_config_prunes_defaults_and_round_trips() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("plugins.toml");
let mut config = PluginConfig::default();
ensure_observability_component(&mut config).unwrap();
config.components.push(PluginComponentSpec {
kind: ADAPTIVE_PLUGIN_KIND.to_string(),
enabled: true,
config: adaptive_component_config("cli-roundtrip"),
});
config.components.push(PluginComponentSpec {
kind: NEMO_GUARDRAILS_PLUGIN_KIND.to_string(),
enabled: false,
config: guardrails_component_config("cli-roundtrip"),
});
write_plugin_config(&path, &config).unwrap();
let rendered = std::fs::read_to_string(&path).unwrap();
assert!(rendered.contains("kind = \"observability\""));
assert!(rendered.contains("kind = \"adaptive\""));
assert!(rendered.contains("kind = \"nemo_guardrails\""));
assert!(!rendered.contains("enabled = true"));
let round_tripped = read_plugin_config(&path).unwrap();
assert_eq!(round_tripped.components.len(), 3);
assert_eq!(round_tripped.components[0].kind, OBSERVABILITY_PLUGIN_KIND);
let adaptive = round_tripped
.components
.iter()
.find(|component| component.kind == ADAPTIVE_PLUGIN_KIND)
.unwrap();
assert_eq!(
adaptive.config.get("agent_id"),
Some(&json!("cli-roundtrip"))
);
let adaptive_hints = adaptive
.config
.get("adaptive_hints")
.and_then(Value::as_object)
.unwrap();
assert_eq!(
adaptive_hints.get("inject_body_path"),
Some(&json!("nvext.agent_hints"))
);
let guardrails = round_tripped
.components
.iter()
.find(|component| component.kind == NEMO_GUARDRAILS_PLUGIN_KIND)
.unwrap();
assert!(!guardrails.enabled);
assert_eq!(
guardrails.config["remote"]["config_id"],
json!("cli-roundtrip")
);
}
#[test]
fn prune_plugin_defaults_removes_default_policy_and_enabled_true_only() {
let mut value = json!({
"version": 1,
"policy": ConfigPolicy::default(),
"components": [
{ "kind": "observability", "enabled": true, "config": {} },
{ "kind": "other", "enabled": false, "config": {} }
]
});
prune_plugin_defaults(&mut value);
let object = value.as_object().unwrap();
assert!(!object.contains_key("policy"));
let components = object["components"].as_array().unwrap();
assert!(!components[0].as_object().unwrap().contains_key("enabled"));
assert_eq!(components[1]["enabled"], json!(false));
}
#[test]
fn validate_config_reports_plugin_diagnostics() {
let config = PluginConfig {
components: vec![PluginComponentSpec {
kind: OBSERVABILITY_PLUGIN_KIND.to_string(),
enabled: true,
config: json!({
"version": 1,
"atof": {
"enabled": true,
"mode": "not-a-mode"
}
})
.as_object()
.unwrap()
.clone(),
}],
..PluginConfig::default()
};
let error = validate_config(&config).unwrap_err().to_string();
assert!(
error.contains("plugin validation failed"),
"error was: {error}"
);
assert!(error.contains("ATOF mode"), "error was: {error}");
}
#[test]
fn validate_config_accepts_adaptive_component() {
let config = PluginConfig {
components: vec![PluginComponentSpec {
kind: ADAPTIVE_PLUGIN_KIND.to_string(),
enabled: true,
config: adaptive_component_config("cli-validation"),
}],
..PluginConfig::default()
};
validate_config(&config).unwrap();
}
#[test]
fn validate_config_accepts_nemo_guardrails_component() {
let config = PluginConfig {
components: vec![PluginComponentSpec {
kind: NEMO_GUARDRAILS_PLUGIN_KIND.to_string(),
enabled: true,
config: guardrails_component_config("cli-validation"),
}],
..PluginConfig::default()
};
validate_config(&config).unwrap();
}
#[test]
fn nemo_guardrails_config_map_prunes_default_version() {
let map = nemo_guardrails_config_map(&NeMoGuardrailsConfig {
codec: Some("openai_chat".into()),
remote: Some(RemoteBackendConfig {
endpoint: Some("http://localhost:8000".into()),
config_id: Some("default".into()),
..RemoteBackendConfig::default()
}),
..NeMoGuardrailsConfig::default()
})
.unwrap();
assert!(!map.contains_key("version"));
assert_eq!(map.get("codec"), Some(&json!("openai_chat")));
assert_eq!(map["remote"]["config_id"], json!("default"));
}
#[test]
fn display_helpers_render_scalars_json_and_defaults() {
assert_eq!(display_value(&json!("logs")), "logs");
assert_eq!(display_value(&json!(true)), "true");
assert_eq!(display_value(&json!(7)), "7");
assert_eq!(display_value(&json!({ "a": 1 })), r#"{"a":1}"#);
let atof = ObservabilityConfig::editor_schema().field("atof").unwrap();
let mode = atof.schema().unwrap().field("mode").unwrap();
assert_eq!(
display_field_value(atof, mode, &json!("append")),
"append (default)"
);
assert_eq!(
display_field_value(atof, mode, &json!("overwrite")),
"overwrite"
);
}
#[test]
fn parse_float_value_rejects_non_finite_numbers() {
let field = EditorFieldSpec {
name: "stable_threshold",
label: "Stable threshold",
kind: EditorFieldKind::Float,
enum_values: &[],
optional: false,
nested_schema: None,
nested_default: None,
};
assert_eq!(parse_float_value(&field, "0.75").unwrap(), json!(0.75));
for value in ["inf", "-inf", "NaN"] {
let error = parse_float_value(&field, value).unwrap_err().to_string();
assert!(
error.contains("stable_threshold must be a finite number"),
"error was: {error}"
);
assert!(error.contains(value), "error was: {error}");
}
}
#[test]
fn target_path_resolves_project_and_global_without_user_env() {
let cwd = std::env::current_dir().unwrap();
assert_eq!(
target_path(TargetScope::Project).unwrap(),
project_plugin_config_path(&cwd)
);
assert_eq!(
target_path(TargetScope::Global).unwrap(),
global_plugin_config_path()
);
}