Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/cli/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ pub(crate) enum NormalizedEvent {
HookMark(SessionEvent),
}

#[cfg(test)]
#[path = "../../tests/coverage/shared/events_tests.rs"]
mod tests;

impl NormalizedEvent {
// Extracts the routing session id regardless of normalized event kind. Keeping this on the
// enum lets the session manager group events before it needs to inspect lifecycle semantics.
Expand Down
55 changes: 55 additions & 0 deletions crates/cli/tests/coverage/shared/events_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

use serde_json::json;

use super::AgentKind;
use super::json_path::{string_at, string_at_any, value_at, value_at_any};

#[test]
fn agent_kinds_use_stable_runtime_metadata_names() {
assert_eq!(AgentKind::Codex.as_str(), "codex");
assert_eq!(AgentKind::ClaudeCode.as_str(), "claude-code");
assert_eq!(AgentKind::Hermes.as_str(), "hermes");
assert_eq!(AgentKind::Gateway.as_str(), "gateway");
}

#[test]
fn values_follow_nested_paths_and_return_owned_data() {
let payload = json!({"request": {"id": "request-1", "enabled": true}});

let extracted = value_at(&payload, &["request", "id"]);
assert_eq!(extracted, Some(json!("request-1")));
assert_eq!(value_at(&payload, &["request", "missing"]), None);
assert_eq!(
value_at_any(&payload, &[&["missing"], &["request", "enabled"]]),
Some(json!(true))
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
assert_eq!(
value_at_any(&payload, &[&["request", "id"], &["request", "enabled"]]),
Some(json!("request-1"))
);
}

#[test]
fn string_helpers_accept_scalars_and_skip_empty_or_structured_values() {
let payload = json!({
"empty": "",
"number": 42,
"enabled": false,
"object": {"id": "nested"}
});

assert_eq!(string_at(&payload, &["number"]), Some("42".into()));
assert_eq!(string_at(&payload, &["enabled"]), Some("false".into()));
assert_eq!(string_at(&payload, &["empty"]), None);
assert_eq!(string_at(&payload, &["object"]), None);
assert_eq!(
string_at_any(&payload, &[&["empty"], &["object"], &["object", "id"]]),
Some("nested".into())
);
assert_eq!(
string_at_any(&payload, &[&["object", "id"], &["number"]]),
Some("nested".into())
);
}
48 changes: 48 additions & 0 deletions crates/cli/tests/coverage/shared/file_io_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,54 @@ fn lock_attempts_distinguish_contention_from_errors() {
fs2::FileExt::unlock(&waiter).unwrap();
}

#[test]
fn bounded_file_reads_stream_regular_content_and_reject_directories() {
let directory = tempdir().unwrap();
let path = directory.path().join("payload.json");
std::fs::write(&path, b"relay payload").unwrap();

assert_eq!(
crate::filesystem::bounded::read_bounded_regular_file(&path, "payload").unwrap(),
b"relay payload"
);

let mut chunks = Vec::new();
crate::filesystem::bounded::stream_bounded_regular_file(&path, "payload", |chunk| {
chunks.extend_from_slice(chunk);
})
.unwrap();
assert_eq!(chunks, b"relay payload");

let error = crate::filesystem::bounded::read_bounded_regular_file(directory.path(), "payload")
.unwrap_err();
assert!(error.contains("must be a regular file"), "{error}");
}

#[test]
fn backups_and_snapshots_restore_original_or_missing_files() {
let directory = tempdir().unwrap();
let path = directory.path().join("relay.toml");
std::fs::write(&path, b"original").unwrap();

backup(&path).unwrap();
std::fs::write(&path, b"changed").unwrap();
backup(&path).unwrap();
assert_eq!(std::fs::read(backup_path(&path)).unwrap(), b"original");
remove_backup(&path).unwrap();
remove_backup(&path).unwrap();

let existing = snapshot_optional_file(&path).unwrap();
std::fs::remove_file(&path).unwrap();
restore_file_snapshot(&existing).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"changed");

let missing = directory.path().join("missing.toml");
let absent = snapshot_optional_file(&missing).unwrap();
std::fs::write(&missing, b"temporary").unwrap();
restore_file_snapshot(&absent).unwrap();
assert!(!missing.exists());
}

#[cfg(unix)]
#[test]
fn private_atomic_write_ignores_a_permissive_umask() {
Expand Down
30 changes: 30 additions & 0 deletions crates/cli/tests/coverage/shared/installer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,36 @@ fn verified_hook_response_rejects_invalid_status_and_fail_open_http_errors() {
handle_hook_forward_status(reqwest::StatusCode::BAD_GATEWAY, String::new(), false).unwrap();
}

#[test]
fn hook_response_statuses_preserve_guardrail_rejections_and_fail_closed_errors() {
let rejection = handle_hook_forward_status(
reqwest::StatusCode::FORBIDDEN,
r#"{"error":{"type":"nemo_relay_guardrail_rejected","reason":"policy denied"}}"#.into(),
false,
)
.unwrap_err()
.to_string();
assert!(rejection.contains("policy denied"), "{rejection}");

let fallback = handle_hook_forward_status(
reqwest::StatusCode::BAD_REQUEST,
r#"{"error":{"type":"nemo_relay_guardrail_rejected","message":"fallback"}}"#.into(),
false,
)
.unwrap_err()
.to_string();
assert!(fallback.contains("fallback"), "{fallback}");

let error = handle_hook_forward_status(
reqwest::StatusCode::BAD_GATEWAY,
"not a guardrail response".into(),
true,
)
.unwrap_err()
.to_string();
assert!(error.contains("HTTP 502"), "{error}");
}

#[test]
fn windows_hook_decoder_rejects_unsafe_odd_and_trailing_argument_envelopes() {
const SEPARATOR: &str = " -NoLogo -NoProfile -NonInteractive -EncodedCommand ";
Expand Down
59 changes: 2 additions & 57 deletions crates/core/src/plugin/dynamic/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,60 +397,5 @@ impl Drop for PluginHostActivation {
}

#[cfg(test)]
mod tests {
use super::*;
use crate::plugin::{PLUGIN_HANDLERS, PLUGIN_MUTATION_OWNER, PluginMutationOwner};

struct PoisonedRegistryCleanup;

impl Drop for PoisonedRegistryCleanup {
fn drop(&mut self) {
PLUGIN_HANDLERS.clear_poison();
if let Ok(mut owner) = PLUGIN_MUTATION_OWNER.lock() {
*owner = PluginMutationOwner::Idle;
}
}
}

#[test]
fn unsafe_kind_deregistration_retains_runtime_and_owner() {
let _guard = crate::shared_runtime::runtime_owner_test_mutex()
.lock()
.unwrap_or_else(|error| error.into_inner());
let _cleanup = PoisonedRegistryCleanup;
let claim = acquire_plugin_host_lease().expect("fixture host should acquire the owner");
let owner_id = claim.owner_id();
let mut activation = PluginHostActivation {
active: true,
native: Some(NativePluginActivation::with_plugin_kind_for_test(
"fixture.poisoned",
)),
#[cfg(feature = "worker-grpc")]
worker: None,
claim: Some(claim),
};

std::thread::spawn(|| {
let _registry = PLUGIN_HANDLERS.write().unwrap();
panic!("poison plugin registry for teardown test");
})
.join()
.expect_err("fixture registry writer should panic");

let error = activation
.clear_inner()
.expect_err("an uncertain kind deregistration must retain the activation")
.to_string();
assert!(error.contains("plugin registry lock poisoned"), "{error}");
assert!(error.contains("activation owner were retained"), "{error}");
assert!(!activation.is_active());
assert_eq!(
*PLUGIN_MUTATION_OWNER.lock().unwrap(),
PluginMutationOwner::Host(owner_id)
);
assert!(matches!(
acquire_plugin_host_lease(),
Err(crate::plugin::PluginError::Conflict(_))
));
}
}
#[path = "../../../tests/unit/plugin_dynamic_host_tests.rs"]
mod tests;
23 changes: 23 additions & 0 deletions crates/core/tests/coverage/error_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,26 @@ fn test_error_debug() {
let debug = format!("{e:?}");
assert!(debug.contains("AlreadyExists"));
}

#[test]
fn upstream_failures_classify_retryability_and_render_status() {
use std::collections::BTreeMap;

let retryable = UpstreamFailure {
status: Some(503),
body: "temporarily unavailable".into(),
headers: BTreeMap::new(),
class: UpstreamFailureClass::RetryableStatus,
};
assert!(retryable.is_retryable());
assert!(retryable.to_string().contains("HTTP 503"));

let rejected = UpstreamFailure {
status: None,
body: "invalid API key".into(),
headers: BTreeMap::new(),
class: UpstreamFailureClass::Authentication,
};
assert!(!rejected.is_retryable());
assert!(rejected.to_string().contains("transport failure"));
}
Comment thread
willkill07 marked this conversation as resolved.
116 changes: 116 additions & 0 deletions crates/core/tests/unit/plugin_dynamic_host_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

use super::*;
use crate::plugin::{PLUGIN_HANDLERS, PLUGIN_MUTATION_OWNER, PluginMutationOwner};
use serde_json::{Map, Value as Json};

struct PoisonedRegistryCleanup;

impl Drop for PoisonedRegistryCleanup {
fn drop(&mut self) {
PLUGIN_HANDLERS.clear_poison();
if let Ok(mut owner) = PLUGIN_MUTATION_OWNER.lock() {
*owner = PluginMutationOwner::Idle;
}
}
}

#[test]
fn unsafe_kind_deregistration_retains_runtime_and_owner() {
let _guard = crate::shared_runtime::runtime_owner_test_mutex()
.lock()
.unwrap_or_else(|error| error.into_inner());
let _cleanup = PoisonedRegistryCleanup;
let claim = acquire_plugin_host_lease().expect("fixture host should acquire the owner");
let owner_id = claim.owner_id();
let mut activation = PluginHostActivation {
active: true,
native: Some(NativePluginActivation::with_plugin_kind_for_test(
"fixture.poisoned",
)),
#[cfg(feature = "worker-grpc")]
worker: None,
claim: Some(claim),
};

std::thread::spawn(|| {
let _registry = PLUGIN_HANDLERS.write().unwrap();
panic!("poison plugin registry for teardown test");
})
.join()
.expect_err("fixture registry writer should panic");

let error = activation
.clear_inner()
.expect_err("an uncertain kind deregistration must retain the activation")
.to_string();
assert!(error.contains("plugin registry lock poisoned"), "{error}");
assert!(error.contains("activation owner were retained"), "{error}");
assert!(!activation.is_active());
assert_eq!(
*PLUGIN_MUTATION_OWNER.lock().unwrap(),
PluginMutationOwner::Host(owner_id)
);
assert!(matches!(
acquire_plugin_host_lease(),
Err(crate::plugin::PluginError::Conflict(_))
));
}

#[test]
fn dynamic_plugin_specs_require_unique_nonempty_input() {
let empty = validate_dynamic_plugin_specs(&[]).unwrap_err().to_string();
assert!(
empty.contains("requires at least one dynamic plugin"),
"{empty}"
);

let duplicate = DynamicPluginActivationSpec {
plugin_id: "fixture.duplicate".into(),
kind: DynamicPluginKind::RustDynamic,
manifest_ref: "relay-plugin.toml".into(),
environment_ref: None,
config: Map::new(),
};
let error = validate_dynamic_plugin_specs(&[duplicate.clone(), duplicate])
.unwrap_err()
.to_string();
assert!(error.contains("duplicate dynamic plugin id"), "{error}");
}

#[test]
fn plugin_error_context_preserves_each_error_class() {
use crate::plugin::PluginError;

let serialization = serde_json::from_str::<Json>("{").unwrap_err();
let errors = [
PluginError::InvalidConfig("invalid".into()),
PluginError::Conflict("conflict".into()),
PluginError::NotFound("missing".into()),
PluginError::Serialization(serialization),
PluginError::Internal("internal".into()),
PluginError::RegistrationFailed("registration".into()),
];

for error in errors {
let message = plugin_error_context("dynamic load", error).to_string();
assert!(message.contains("dynamic load"), "{message}");
}
}

#[test]
fn retained_runtime_errors_include_cleanup_details_when_available() {
let default_error = retained_runtime_error(Vec::new()).to_string();
assert!(
default_error.contains("teardown was incomplete"),
"{default_error}"
);

let detailed_error =
retained_runtime_error(vec!["registry remained active".into()]).to_string();
assert!(
detailed_error.contains("registry remained active"),
"{detailed_error}"
);
}
Loading
Loading