From d7510e6771abd40a6536b1da486dd3c1fe06fad8 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Thu, 6 Aug 2026 15:38:20 +0000 Subject: [PATCH 1/2] test(errors): assert typed refusal variants over Display substrings AI Assistance: Claude (Fable 5) used for the Refusal variant matcher and the assertion migration. --- crates/nexum-module-macros/src/lib.rs | 3 + crates/nexum-runtime/src/addons.rs | 8 +- crates/nexum-runtime/src/builder.rs | 67 ++++++++++--- crates/nexum-runtime/src/digest.rs | 1 + crates/nexum-runtime/src/engine_config.rs | 12 ++- crates/nexum-runtime/src/host/extension.rs | 2 +- crates/nexum-runtime/src/manifest/load.rs | 3 + crates/nexum-runtime/src/supervisor/load.rs | 2 +- crates/nexum-runtime/src/supervisor/mod.rs | 4 +- .../nexum-runtime/src/supervisor/prepass.rs | 2 +- .../src/supervisor/tests/boot_refusals.rs | 93 ++++++++++++------- .../src/supervisor/tests/chain_gate.rs | 30 ++++++ .../src/supervisor/tests/digest.rs | 18 ++-- .../src/supervisor/tests/ledger.rs | 51 ++++++---- .../src/supervisor/tests/lifecycle.rs | 5 +- .../nexum-runtime/src/supervisor/tests/mod.rs | 4 +- crates/nexum-runtime/src/test_utils/mod.rs | 10 +- .../nexum-runtime/src/test_utils/scenario.rs | 78 ++++++++++++++-- crates/nexum-sdk/src/chain/transport.rs | 2 + 19 files changed, 301 insertions(+), 94 deletions(-) diff --git a/crates/nexum-module-macros/src/lib.rs b/crates/nexum-module-macros/src/lib.rs index ddac670..a324de9 100644 --- a/crates/nexum-module-macros/src/lib.rs +++ b/crates/nexum-module-macros/src/lib.rs @@ -432,17 +432,20 @@ mod tests { #[test] fn empty_subscribes_is_rejected() { let err = parse_args(quote! { subscribes() }).err().unwrap(); + // Foreign syn::Error; pins our macro message. assert!(err.to_string().contains("at least one event type"), "{err}"); } #[test] fn unknown_argument_is_rejected() { let err = parse_args(quote! { emits(Foo) }).err().unwrap(); + // Foreign syn::Error; pins our macro message. assert!( err.to_string().contains("subscribes(EventType, ...)"), "{err}" ); let err = parse_args(quote! { subscribes(Foo), extra }).err().unwrap(); + // Foreign syn::Error; pins our macro message. assert!(err.to_string().contains("unexpected tokens"), "{err}"); } diff --git a/crates/nexum-runtime/src/addons.rs b/crates/nexum-runtime/src/addons.rs index d977fc7..b073e0a 100644 --- a/crates/nexum-runtime/src/addons.rs +++ b/crates/nexum-runtime/src/addons.rs @@ -103,6 +103,12 @@ mod tests { Ok(_) => panic!("invalid bind_addr must not install"), Err(err) => err, }; - assert!(err.to_string().contains("bind_addr"), "{err}"); + assert!( + matches!( + err.downcast_ref::(), + Some(PrometheusError::BindAddr { addr, .. }) if addr == "not-a-socket-addr" + ), + "{err:#}" + ); } } diff --git a/crates/nexum-runtime/src/builder.rs b/crates/nexum-runtime/src/builder.rs index 5358fb8..798c39e 100644 --- a/crates/nexum-runtime/src/builder.rs +++ b/crates/nexum-runtime/src/builder.rs @@ -676,6 +676,7 @@ mod tests { use crate::host::state::HostState; use crate::manifest::NamespaceCaps; use crate::preset::{CoreRuntime, Runtime as RuntimePreset}; + use crate::supervisor::prepass::BootRefusal; use crate::test_utils::clock::ManualClock; use crate::test_utils::wasm::workspace_root; use crate::test_utils::{Prebuilt, TestManifest, example_wasm_or_skip, module_wasm_or_skip}; @@ -697,7 +698,13 @@ mod tests { Ok(_) => panic!("default config declares no modules; launch must bail"), Err(err) => err, }; - assert!(err.to_string().contains("no modules to run"), "{err}"); + assert!( + matches!( + err.downcast_ref::(), + Some(LaunchRefusal::NothingToRun) + ), + "{err:#}" + ); } /// Counts linker hook runs. @@ -779,7 +786,13 @@ mod tests { Ok(_) => panic!("default config declares no modules; launch must bail"), Err(err) => err, }; - assert!(err.to_string().contains("no modules to run"), "{err}"); + assert!( + matches!( + err.downcast_ref::(), + Some(LaunchRefusal::NothingToRun) + ), + "{err:#}" + ); assert_eq!(preset_linked.load(Ordering::SeqCst), 1, "preset extension"); assert_eq!( appended_linked.load(Ordering::SeqCst), @@ -839,7 +852,13 @@ mod tests { Ok(_) => panic!("default config declares no modules; launch must bail"), Err(err) => err, }; - assert!(err.to_string().contains("no modules to run"), "{err}"); + assert!( + matches!( + err.downcast_ref::(), + Some(LaunchRefusal::NothingToRun) + ), + "{err:#}" + ); seen.get().expect("clock attached before boot").clone() } @@ -979,7 +998,13 @@ mod tests { Ok(_) => panic!("default config declares no modules; launch must bail"), Err(err) => err, }; - assert!(err.to_string().contains("no modules to run"), "{err}"); + assert!( + matches!( + err.downcast_ref::(), + Some(LaunchRefusal::NothingToRun) + ), + "{err:#}" + ); assert_eq!( built.load(Ordering::SeqCst), 1, @@ -1059,7 +1084,13 @@ mod tests { Ok(_) => panic!("init-failing module must abort launch"), Err(err) => err, }; - assert!(err.to_string().contains("failed initialisation"), "{err}"); + assert!( + matches!( + err.downcast_ref::(), + Some(LaunchRefusal::AllDeadOverride { modules: 1 }) + ), + "{err:#}" + ); } #[tokio::test] @@ -1091,11 +1122,13 @@ mod tests { Ok(_) => panic!("an unconfigured chain subscription must abort launch"), Err(err) => err, }; - let msg = format!("{err:#}"); assert!( - msg.contains("module example subscribes to chain 424242") - && msg.contains("[chains.424242]"), - "the launch error is the boot-time chain refusal: {msg}", + matches!( + err.downcast_ref::(), + Some(BootRefusal::UnconfiguredChain { name, chain_id: 424_242, .. }) + if name == "example" + ), + "the launch error is the boot-time chain refusal: {err:#}", ); } @@ -1145,7 +1178,13 @@ mod tests { Ok(_) => panic!("no modules configured; launch must bail"), Err(err) => err, }; - assert!(err.to_string().contains("no modules to run"), "{err}"); + assert!( + matches!( + err.downcast_ref::(), + Some(LaunchRefusal::NothingToRun) + ), + "{err:#}" + ); assert_eq!( calls.load(Ordering::SeqCst), 1, @@ -1236,7 +1275,13 @@ mod tests { .wait() .await .expect_err("aborted task surfaces an error"); - assert!(err.to_string().contains("terminated abnormally"), "{err}"); + assert!( + matches!( + err.downcast_ref::(), + Some(LaunchRefusal::EventLoopGone) + ), + "{err:#}" + ); } /// Dropping the handle without `wait` still drains the event loop. diff --git a/crates/nexum-runtime/src/digest.rs b/crates/nexum-runtime/src/digest.rs index b8940b3..038e473 100644 --- a/crates/nexum-runtime/src/digest.rs +++ b/crates/nexum-runtime/src/digest.rs @@ -193,6 +193,7 @@ mod tests { actual, }; let msg = err.to_string(); + // Operator wording pin. assert!(msg.contains("modules/example.wasm"), "{msg}"); assert!(msg.contains(&declared.to_string()), "{msg}"); assert!(msg.contains(&actual.to_string()), "{msg}"); diff --git a/crates/nexum-runtime/src/engine_config.rs b/crates/nexum-runtime/src/engine_config.rs index 4ab3115..0362f09 100644 --- a/crates/nexum-runtime/src/engine_config.rs +++ b/crates/nexum-runtime/src/engine_config.rs @@ -836,6 +836,7 @@ request_timeout_secs = 0 "#, ) .expect_err("a zero timeout must not parse"); + // Foreign toml::de::Error; pins our serde message threaded through it. assert!( err.to_string() .contains("request_timeout_secs must not be 0"), @@ -854,7 +855,8 @@ rpc_url = "wss://example.test/x" "#, ) .expect_err("bogus chain key must not parse"); - assert!(!err.to_string().is_empty()); + // Foreign toml::de::Error; pins that it names the offending key. + assert!(err.to_string().contains("bogus"), "{err}"); } #[test] @@ -1337,9 +1339,11 @@ key = "value" // environment. Use a guaranteed-unique prefix. let err = substitute_env_vars(r#"x = "${NEXUM_TEST_DEFINITELY_UNSET_VAR_XYZ}""#).unwrap_err(); - let msg = err.to_string(); - assert!(msg.contains("NEXUM_TEST_DEFINITELY_UNSET_VAR_XYZ")); - assert!(msg.contains("not set")); + assert!( + matches!(&err, EnvVarError::Missing { name } + if name == "NEXUM_TEST_DEFINITELY_UNSET_VAR_XYZ"), + "{err}" + ); } #[test] diff --git a/crates/nexum-runtime/src/host/extension.rs b/crates/nexum-runtime/src/host/extension.rs index 14c31c8..aba7616 100644 --- a/crates/nexum-runtime/src/host/extension.rs +++ b/crates/nexum-runtime/src/host/extension.rs @@ -365,6 +365,6 @@ mod tests { ext("acme", Arc::new(Clockwork)), ]) .expect_err("duplicate namespace"); - assert!(err.to_string().contains("acme"), "{err}"); + assert_eq!(err.namespace, "acme"); } } diff --git a/crates/nexum-runtime/src/manifest/load.rs b/crates/nexum-runtime/src/manifest/load.rs index aef5b75..23036fd 100644 --- a/crates/nexum-runtime/src/manifest/load.rs +++ b/crates/nexum-runtime/src/manifest/load.rs @@ -175,6 +175,7 @@ event_signature = "0x00000000000000000000000000000000000000000000000000000000dea chain_id = 1\n{field}\n" ); let err = toml::from_str::(&toml).expect_err("malformed hex"); + // Foreign toml::de::Error; pins our hex message threaded through it. assert!(err.to_string().contains(detail), "{err}"); } } @@ -322,6 +323,7 @@ kind = "acme-status" scope = 7 "#; let err = toml::from_str::(toml).expect_err("non-string filter"); + // Foreign toml::de::Error; pins our filter message threaded through it. assert!(err.to_string().contains("must be a string"), "{err}"); } @@ -588,6 +590,7 @@ max_state_bytes = 52428800 let err = load(&path, &CapabilityRegistry::core()).unwrap_err(); assert!(matches!(err, ParseError::MissingCapabilities), "{err:?}"); let msg = err.to_string(); + // Operator wording pin. assert!(msg.contains("[capabilities]"), "{msg}"); assert!(msg.contains("required = []"), "{msg}"); } diff --git a/crates/nexum-runtime/src/supervisor/load.rs b/crates/nexum-runtime/src/supervisor/load.rs index 9cd60e6..06c9764 100644 --- a/crates/nexum-runtime/src/supervisor/load.rs +++ b/crates/nexum-runtime/src/supervisor/load.rs @@ -38,7 +38,7 @@ use crate::runtime::dispatch_rate::TokenBucket; /// Admission refusals ahead of instantiation; the wording is operator-pinned. #[derive(Debug, ThisError, IntoStaticStr)] #[strum(serialize_all = "snake_case")] -pub(super) enum LoadRefusal { +pub(crate) enum LoadRefusal { #[error("{owner} declares manifest section [{section}]; no wired extension claims it")] SectionUnclaimed { owner: String, section: String }, #[error("extension namespace {namespace} is claimed twice")] diff --git a/crates/nexum-runtime/src/supervisor/mod.rs b/crates/nexum-runtime/src/supervisor/mod.rs index b2f11bf..4eaf26f 100644 --- a/crates/nexum-runtime/src/supervisor/mod.rs +++ b/crates/nexum-runtime/src/supervisor/mod.rs @@ -6,8 +6,8 @@ mod artifact; mod cursors; mod dispatch; mod lifecycle; -mod load; -mod prepass; +pub(crate) mod load; +pub(crate) mod prepass; mod role; mod store; mod subscriptions; diff --git a/crates/nexum-runtime/src/supervisor/prepass.rs b/crates/nexum-runtime/src/supervisor/prepass.rs index c0101b1..b777b80 100644 --- a/crates/nexum-runtime/src/supervisor/prepass.rs +++ b/crates/nexum-runtime/src/supervisor/prepass.rs @@ -17,7 +17,7 @@ use crate::manifest::{self, CapabilityRegistry, LoadedManifest, ParseError, Subs /// Refusals before any compile; the wording is operator-pinned. #[derive(Debug, Error, IntoStaticStr)] #[strum(serialize_all = "snake_case")] -pub(super) enum BootRefusal { +pub(crate) enum BootRefusal { #[error( "name {name} is claimed twice: {held_role} {} and {role} {}; \ [module].name must be unique across [[modules]] and [[adapters]]", diff --git a/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs b/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs index 0cf4b16..aa2c2a6 100644 --- a/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs +++ b/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs @@ -57,24 +57,25 @@ async fn boot_rejects_provider_whose_manifest_is_an_event_module() { .adapter(TestManifest::new("acme").kind("event-module")) .expect_refusal() .await - .names("acme-adapter"); + .variant::(|e| { + matches!(e, LoadRefusal::WorkerKindAdapter { registered, .. } + if registered == &["acme-adapter"]) + }); } /// The refusal names the registered kinds. #[tokio::test] async fn boot_rejects_an_unregistered_provider_kind() { - let refusal = BootScenario::over(mock_components()) + BootScenario::over(mock_components()) .extensions(acme_extensions()) .adapter(TestManifest::new("bad").kind("gadget")) .expect_refusal() - .await; - assert!(matches!( - refusal.root::(), - Some(LoadRefusal::UnregisteredKind { kind, registered, .. }) - if kind == "gadget" && registered == &["acme-adapter"] - )); - // Operator wording pin. - refusal + .await + .variant::(|e| { + matches!(e, LoadRefusal::UnregisteredKind { kind, registered, .. } + if kind == "gadget" && registered == &["acme-adapter"]) + }) + // Operator wording pin. .names("unregistered provider kind gadget") .names("acme-adapter"); } @@ -94,6 +95,8 @@ async fn boot_admits_a_registered_provider_kind_past_the_kind_gate() { ) .expect_refusal() .await + .variant::(|e| e.kind() == std::io::ErrorKind::NotFound) + // Operator wording pin. .names("read component") .names("missing-acme") .lacks("requires a module.toml"); @@ -103,19 +106,21 @@ async fn boot_admits_a_registered_provider_kind_past_the_kind_gate() { /// the boot before any entry loads. #[tokio::test] async fn boot_refuses_a_provider_kind_without_a_host_service() { - let refusal = BootScenario::over(mock_components()) + BootScenario::over(mock_components()) .extensions(serviceless_acme_extensions()) .expect_refusal() - .await; - assert!(matches!( - refusal.root::(), - Some(LoadRefusal::ServicelessKind { - namespace: "acme", - kind: "acme-adapter" + .await + .variant::(|e| { + matches!( + e, + LoadRefusal::ServicelessKind { + namespace: "acme", + kind: "acme-adapter" + } + ) }) - )); - // Operator wording pin. - refusal.names("extension acme registers provider kind acme-adapter without a host service"); + // Operator wording pin. + .names("extension acme registers provider kind acme-adapter without a host service"); } /// Provider kinds come only from `engine.toml`, so single boot skips the @@ -149,7 +154,8 @@ async fn boot_single_skips_the_provider_kind_service_gate() { .err() .expect("a missing manifest must refuse the boot"); Refusal::from(err) - .names("no module.toml") + .variant::(|e| matches!(e, BootRefusal::ManifestMissing { .. })) + // Operator wording pin. .lacks("without a host service"); } @@ -168,7 +174,9 @@ async fn boot_refuses_an_undeclared_extension_subscription_kind() { ) .expect_refusal() .await - .names("unknown event kind acme-status"); + .variant::( + |e| matches!(e, LoadRefusal::UnknownEventKind { kind, .. } if kind == "acme-status"), + ); } /// No wasm needs to exist; the refusal precedes compile and carries the @@ -181,8 +189,11 @@ async fn boot_refuses_a_component_without_module_toml() { .module(Entry::new(ManifestSource::Beside).wasm(orphan)) .expect_refusal() .await - .names("no module.toml") - .names("orphan.wasm") + .variant::(|e| { + matches!(e, BootRefusal::ManifestMissing { component } + if component.ends_with("orphan.wasm")) + }) + // Operator wording pin. .names("required = []") .lacks("compile"); } @@ -195,8 +206,10 @@ async fn boot_refuses_a_nonexistent_explicit_manifest_path() { .module(missing) .expect_refusal() .await - .names("modle.toml") - .names("not found"); + .variant::(|e| { + matches!(e, BootRefusal::ManifestNotFound { manifest, .. } + if manifest.ends_with("modle.toml")) + }); } /// Operator `http_allow` must not stand in for the component's own @@ -212,7 +225,10 @@ async fn boot_refuses_a_capsless_manifest_before_any_other_gate() { .adapter(Entry::new(provider.to_owned()).http_allow(["api.acme.example"])) .expect_refusal() .await - .names("no [capabilities] section") + .variant::(|e| { + matches!(e, BootRefusal::Manifest(ParseError::MissingCapabilities)) + }) + // Operator wording pin. .names("required = []") .lacks("no wired extension claims") .lacks("compile"); @@ -224,7 +240,10 @@ async fn boot_refuses_a_capsless_manifest_before_any_other_gate() { .module(module.to_owned()) .expect_refusal() .await - .names("no [capabilities] section") + .variant::(|e| { + matches!(e, BootRefusal::Manifest(ParseError::MissingCapabilities)) + }) + // Operator wording pin. .names("required = []") .lacks("unknown event kind") .lacks("no wired extension claims") @@ -242,7 +261,8 @@ async fn boot_refuses_a_blank_manifest_name_for_both_roles() { .adapter(TestManifest::new("").kind("acme-adapter").cap("chain")) .expect_refusal() .await - .names("[module].name") + .variant::(|e| matches!(e, BootRefusal::Manifest(ParseError::BlankModuleName))) + // Operator wording pin. .lacks("claimed twice") .lacks("read component") .lacks("compile"); @@ -252,7 +272,10 @@ async fn boot_refuses_a_blank_manifest_name_for_both_roles() { .module(TestManifest::new(blank).cap("logging")) .expect_refusal() .await - .names("[module].name") + .variant::(|e| { + matches!(e, BootRefusal::Manifest(ParseError::BlankModuleName)) + }) + // Operator wording pin. .lacks("claimed twice") .lacks("read component") .lacks("compile"); @@ -276,8 +299,9 @@ async fn boot_denies_an_undeclared_chain_import_for_balance_tracker() { ) .expect_refusal() .await - .names("capability violation") - .names("nexum:host/chain"); + .variant::( + |e| matches!(e, CapabilityError::Undeclared(v) if v.capability == "chain"), + ); } /// The example component's only gated import is `logging`, so the refusal @@ -293,6 +317,7 @@ async fn boot_denies_an_undeclared_logging_import_for_a_provider() { .adapter(Entry::new(TestManifest::new("acme").kind("acme-adapter").cap("chain")).wasm(wasm)) .expect_refusal() .await - .names("capability violation") - .names("nexum:host/logging"); + .variant::( + |e| matches!(e, CapabilityError::Undeclared(v) if v.capability == "logging"), + ); } diff --git a/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs b/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs index 95d597c..ee2cadd 100644 --- a/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs +++ b/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs @@ -31,6 +31,11 @@ async fn boot_refuses_a_subscription_on_an_unconfigured_chain() { .module(manifest) .expect_refusal() .await + .variant::(|e| { + matches!(e, BootRefusal::UnconfiguredChain { noun: "module", name, chain_id: 424_242, .. } + if name == "example") + }) + // Operator wording pin. .names("module example subscribes to chain 424242") .names("[chains.424242]") .names("configured chains: 1, 100, 11155111") @@ -54,6 +59,11 @@ async fn boot_single_refuses_a_subscription_on_an_unconfigured_chain() { .err() .expect("an unconfigured chain must refuse the boot"), ) + .variant::(|e| { + matches!(e, BootRefusal::UnconfiguredChain { noun: "module", name, chain_id: 424_242, .. } + if name == "gated") + }) + // Operator wording pin. .names("module gated subscribes to chain 424242") .names("configured chains: 1, 100, 11155111") .lacks("compile"); @@ -73,6 +83,11 @@ async fn boot_refuses_an_adapter_subscription_on_an_unconfigured_chain() { ) .expect_refusal() .await + .variant::(|e| { + matches!(e, BootRefusal::UnconfiguredChain { noun: "adapter", name, chain_id: 424_242, .. } + if name == "feed") + }) + // Operator wording pin. .names("load provider") .names("adapter feed subscribes to chain 424242") .names("[chains.424242]") @@ -104,6 +119,8 @@ async fn boot_refuses_an_invalid_chain_log_filter() { .module(manifest) .expect_refusal() .await + .variant::(|e| matches!(e, BootRefusal::Manifest(ParseError::Toml(_)))) + // Operator wording pin. .names("load module") .names("manifest: parse") .names(detail) @@ -158,6 +175,8 @@ async fn boot_admits_a_block_subscription_on_a_configured_chain_past_the_chain_g .module(TestManifest::new("example").cap("logging").block_sub(1)) .expect_refusal() .await + .variant::(|e| e.kind() == std::io::ErrorKind::NotFound) + // Operator wording pin. .names("read component") .lacks("subscribes to chain"); } @@ -178,6 +197,11 @@ async fn an_unconfigured_chain_refuses_boot_before_an_earlier_module_loads() { ) .expect_refusal() .await + .variant::(|e| { + matches!(e, BootRefusal::UnconfiguredChain { noun: "module", name, chain_id: 424_242, .. } + if name == "example") + }) + // Operator wording pin. .names("load module") .names("second.wasm") .names("module example subscribes to chain 424242") @@ -196,6 +220,11 @@ async fn boot_refusal_names_the_missing_engine_toml_on_the_defaulted_path() { ) .expect_refusal() .await + .variant::(|e| { + matches!(e, BootRefusal::UnconfiguredChainDefaulted { noun: "module", name, chain_id: 424_242 } + if name == "example") + }) + // Operator wording pin. .names("no engine.toml was found") .names("[chains.424242]") .lacks("configured chains:"); @@ -215,6 +244,7 @@ fn configured_chains_normalise_named_and_numeric_spellings() { fn unconfigured_chain_message_says_none_when_engine_toml_declares_no_chains() { let chains = ConfiguredChains::from_config(&EngineConfig::default()); let msg = unconfigured_chain(Role::Module, "example", 424_242, &chains).to_string(); + // Operator wording pin. assert!(msg.contains("configured chains: none"), "{msg}"); assert!(!msg.contains("no engine.toml was found"), "{msg}"); } diff --git a/crates/nexum-runtime/src/supervisor/tests/digest.rs b/crates/nexum-runtime/src/supervisor/tests/digest.rs index e96071c..0813131 100644 --- a/crates/nexum-runtime/src/supervisor/tests/digest.rs +++ b/crates/nexum-runtime/src/supervisor/tests/digest.rs @@ -34,6 +34,7 @@ fn read_verified_component_rejects_a_mismatched_digest() { ContentDigest::of_bytes(b"not the pinned bytes"), ); Refusal::from(err) + // Operator wording pin. .names("component digest mismatch") .lacks("compile"); } @@ -49,7 +50,8 @@ fn read_verified_component_requires_a_digest_when_the_flag_is_set() { .err() .expect("an unpinned artifact must refuse under the flag"); Refusal::from(err) - .names("require_component_digest") + .variant::(|e| matches!(e, LoadRefusal::DigestUnpinned { .. })) + // Operator wording pin. .lacks("compile"); } @@ -137,7 +139,8 @@ async fn boot_single_refuses_a_mismatched_component_digest() { let (_store, result) = try_boot_single(&wasm, Some(&manifest), false, None).await; Refusal::from(result.err().expect("a stale pin must refuse the boot")) - .names("component digest mismatch") + .variant::(|_| true) + // Operator wording pin. .lacks("compile"); } @@ -154,7 +157,8 @@ async fn boot_single_requires_a_digest_when_the_engine_flag_is_set() { .err() .expect("an unpinned manifest must refuse under the flag"), ) - .names("require_component_digest") + .variant::(|e| matches!(e, LoadRefusal::DigestUnpinned { .. })) + // Operator wording pin. .lacks("compile"); } @@ -192,7 +196,8 @@ async fn boot_refuses_a_provider_with_a_mismatched_digest() { ) .expect_refusal() .await - .names("component digest mismatch") + .variant::(|_| true) + // Operator wording pin. .lacks("compile"); } @@ -207,7 +212,7 @@ async fn boot_requires_a_provider_digest_when_the_engine_flag_is_set() { .adapter(Entry::new(TestManifest::new("acme").kind("acme-adapter").cap("chain")).wasm(wasm)) .expect_refusal() .await - .names("require_component_digest"); + .variant::(|e| matches!(e, LoadRefusal::DigestUnpinned { .. })); } #[tokio::test] @@ -219,6 +224,7 @@ async fn boot_requires_a_module_digest_when_the_engine_flag_is_set() { .module(Entry::new(TestManifest::new("unpinned")).wasm(wasm)) .expect_refusal() .await - .names("require_component_digest") + .variant::(|e| matches!(e, LoadRefusal::DigestUnpinned { .. })) + // Operator wording pin. .lacks("compile"); } diff --git a/crates/nexum-runtime/src/supervisor/tests/ledger.rs b/crates/nexum-runtime/src/supervisor/tests/ledger.rs index f9ade48..e394341 100644 --- a/crates/nexum-runtime/src/supervisor/tests/ledger.rs +++ b/crates/nexum-runtime/src/supervisor/tests/ledger.rs @@ -33,8 +33,11 @@ fn extension_sections_must_be_claimed() { sections.insert("venu".into(), toml::Value::Boolean(true)); let err = enforce_extension_sections("keeper", §ions, &extensions) .expect_err("unclaimed section"); - assert!(err.to_string().contains("[venu]"), "{err}"); - assert!(err.to_string().contains("keeper"), "{err}"); + assert!( + matches!(&err, LoadRefusal::SectionUnclaimed { owner, section } + if owner == "keeper" && section == "venu"), + "{err}" + ); } /// Two extensions colliding on a subscription kind or a manifest section @@ -89,14 +92,20 @@ fn extension_claims_must_be_unique() { ext("b", &["orders"], &["pool"]), ]) .expect_err("duplicate subscription kind"); - assert!(err.to_string().contains("orders"), "{err}"); + assert!( + matches!(&err, LoadRefusal::SubscriptionKindClaimed { kind } if *kind == "orders"), + "{err}" + ); let err = enforce_extension_uniqueness(&[ ext("a", &["orders"], &["venue"]), ext("b", &["fills"], &["venue"]), ]) .expect_err("duplicate manifest section"); - assert!(err.to_string().contains("[venue]"), "{err}"); + assert!( + matches!(&err, LoadRefusal::SectionClaimed { section } if *section == "venue"), + "{err}" + ); } #[test] @@ -116,14 +125,12 @@ fn claim_namespace_rejects_cross_role_duplicate_with_both_paths() { Path::new("adapters/impostor.wasm"), ) .expect_err("cross-role duplicate must be refused"); - let msg = format!("{err:#}"); - assert!( - msg.contains("module") && msg.contains("adapter"), - "the refusal names both roles: {msg}", - ); assert!( - msg.contains("modules/price-alert.wasm") && msg.contains("adapters/impostor.wasm"), - "the refusal names both claimant paths: {msg}", + matches!(&err, BootRefusal::NamespaceClaimed { name, held_role: "module", held, role: "adapter", path } + if name == "price-alert" + && held.as_path() == Path::new("modules/price-alert.wasm") + && path.as_path() == Path::new("adapters/impostor.wasm")), + "the refusal names both claimants: {err}", ); } @@ -155,11 +162,13 @@ async fn boot_rejects_duplicate_names_across_and_within_roles() { .module(Entry::new(TestManifest::new("dup").cap("logging")).wasm(module_wasm)) .expect_refusal() .await - .names("name dup is claimed twice") - .names("adapter") - .names("module") - .names("missing-adapter.wasm") - .names("missing-module.wasm") + .variant::(|e| { + matches!(e, BootRefusal::NamespaceClaimed { name, held_role: "adapter", held, role: "module", path } + if name == "dup" + && held.ends_with("missing-adapter.wasm") + && path.ends_with("missing-module.wasm")) + }) + // Operator wording pin. .lacks("compile"); let scenario = BootScenario::new(); @@ -172,8 +181,12 @@ async fn boot_rejects_duplicate_names_across_and_within_roles() { .module(Entry::new(TestManifest::new("dup").cap("logging")).wasm(second)) .expect_refusal() .await - .names("name dup is claimed twice") - .names("missing-a.wasm") - .names("missing-b.wasm") + .variant::(|e| { + matches!(e, BootRefusal::NamespaceClaimed { name, held, path, .. } + if name == "dup" + && held.ends_with("missing-a.wasm") + && path.ends_with("missing-b.wasm")) + }) + // Operator wording pin. .lacks("compile"); } diff --git a/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs b/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs index eae6eb1..5c3b956 100644 --- a/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs +++ b/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs @@ -769,5 +769,8 @@ async fn a_hanging_provider_install_refuses_the_boot_by_deadline() { INSTALL_DEADLINE, "the install rode the configured deadline, not another timer", ); - refusal.names("did not install in time"); + refusal + .variant::(|_| true) + // Operator wording pin. + .names("did not install in time"); } diff --git a/crates/nexum-runtime/src/supervisor/tests/mod.rs b/crates/nexum-runtime/src/supervisor/tests/mod.rs index fc30444..ae15567 100644 --- a/crates/nexum-runtime/src/supervisor/tests/mod.rs +++ b/crates/nexum-runtime/src/supervisor/tests/mod.rs @@ -20,7 +20,7 @@ use super::artifact::read_verified_component; use super::cursors::{ chainlog_cursor_key, commit_chain_log_cursor, progress_key, read_chain_log_cursor, }; -use super::dispatch::with_dispatch_deadline; +use super::dispatch::{DeadlineExceeded, with_dispatch_deadline}; use super::prepass::{NamespaceLedger, claim_namespace, unconfigured_chain}; use super::store::resolve_module_limits; use super::subscriptions::build_alloy_filter; @@ -31,7 +31,7 @@ use crate::engine_config::ModuleLimits; use crate::host::extension::{HostService, Installed, ProviderInstance, ProviderKind}; use crate::host::logs::LogSource; use crate::host::provider_pool::ProviderPool; -use crate::manifest::{self, CapabilityRegistry, ResourceSection}; +use crate::manifest::{self, CapabilityRegistry, ParseError, ResourceSection}; use crate::preset::CoreRuntime; use crate::test_utils::{ BootScenario, Entry, ManifestSource, Refusal, TestManifest, example_wasm_or_skip, diff --git a/crates/nexum-runtime/src/test_utils/mod.rs b/crates/nexum-runtime/src/test_utils/mod.rs index 7264fda..d50a9a1 100644 --- a/crates/nexum-runtime/src/test_utils/mod.rs +++ b/crates/nexum-runtime/src/test_utils/mod.rs @@ -102,7 +102,7 @@ mod tests { use alloy_chains::Chain; use futures::StreamExt as _; - use crate::builder::RuntimeBuilder; + use crate::builder::{LaunchRefusal, RuntimeBuilder}; use crate::engine_config::EngineConfig; use crate::host::component::{ChainMethod, ComponentsBuilder, StateHandle, StateStore}; @@ -129,7 +129,13 @@ mod tests { Ok(_) => panic!("default config declares no modules; launch must bail"), Err(err) => err, }; - assert!(err.to_string().contains("no modules to run"), "{err}"); + assert!( + matches!( + err.downcast_ref::(), + Some(LaunchRefusal::NothingToRun) + ), + "{err:#}" + ); // The fake actually serves and records, independent of the launch. let body = pool diff --git a/crates/nexum-runtime/src/test_utils/scenario.rs b/crates/nexum-runtime/src/test_utils/scenario.rs index 7053a38..40890a5 100644 --- a/crates/nexum-runtime/src/test_utils/scenario.rs +++ b/crates/nexum-runtime/src/test_utils/scenario.rs @@ -325,6 +325,23 @@ impl Refusal { self.0.chain().find_map(|cause| cause.downcast_ref::()) } + /// Assert the chain carries an `E` matching `pred`. + #[track_caller] + pub fn variant(self, pred: impl FnOnce(&E) -> bool) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + match self.root::() { + None => panic!( + "refusal carries no {}: {}", + std::any::type_name::(), + self.chain(), + ), + Some(root) => assert!(pred(root), "refusal variant mismatch: {}", self.chain()), + } + self + } + /// Assert the refusal names `needle` somewhere in its context chain. #[track_caller] pub fn names(self, needle: &str) -> Self { @@ -354,7 +371,9 @@ impl Refusal { mod tests { use super::*; use crate::host::extension::HostWallClock; - use crate::manifest::NamespaceCaps; + use crate::manifest::{NamespaceCaps, ParseError}; + use crate::supervisor::load::LoadRefusal; + use crate::supervisor::prepass::BootRefusal; use crate::test_utils::{example_wasm_or_skip, module_wasm_or_skip}; /// Claims the `[acme]` manifest section and nothing else. @@ -527,8 +546,11 @@ mod tests { .module(TestManifest::new("bad").cap("telepathy")) .expect_refusal() .await - .names("unknown capability") - .names("telepathy") + .variant::(|e| { + matches!(e, BootRefusal::Manifest(ParseError::UnknownCapability { name, .. }) + if name == "telepathy") + }) + // Operator wording pin. .lacks("compile"); } @@ -538,7 +560,10 @@ mod tests { .adapter(TestManifest::new("feed").kind("acme-feed")) .expect_refusal() .await - .names("unregistered provider kind acme-feed") + .variant::( + |e| matches!(e, LoadRefusal::UnregisteredKind { kind, .. } if kind == "acme-feed"), + ) + // Operator wording pin. .lacks("compile"); } @@ -550,8 +575,11 @@ mod tests { .module(Entry::new(ManifestSource::Beside).wasm(orphan)) .expect_refusal() .await - .names("no module.toml") - .names("orphan.wasm") + .variant::(|e| { + matches!(e, BootRefusal::ManifestMissing { component } + if component.ends_with("orphan.wasm")) + }) + // Operator wording pin. .lacks("compile"); } @@ -563,8 +591,11 @@ mod tests { .module(missing) .expect_refusal() .await - .names("modle.toml") - .names("not found") + .variant::(|e| { + matches!(e, BootRefusal::ManifestNotFound { manifest, .. } + if manifest.ends_with("modle.toml")) + }) + // Operator wording pin. .lacks("compile"); } @@ -574,7 +605,11 @@ mod tests { .module(acme_section_manifest()) .expect_refusal() .await - .names("no wired extension claims it") + .variant::(|e| { + matches!(e, LoadRefusal::SectionUnclaimed { owner, section } + if owner == "acme-user" && section == "acme") + }) + // Operator wording pin. .lacks("read component"); BootScenario::new() @@ -582,6 +617,8 @@ mod tests { .module(acme_section_manifest()) .expect_refusal() .await + .variant::(|e| e.kind() == std::io::ErrorKind::NotFound) + // Operator wording pin. .names("read component") .lacks("no wired extension claims it"); } @@ -691,4 +728,27 @@ mod tests { fn lacks_panics_on_a_present_needle() { Refusal(anyhow::anyhow!("boom")).lacks("boom"); } + + fn not_found() -> anyhow::Error { + std::io::Error::new(std::io::ErrorKind::NotFound, "gone").into() + } + + #[test] + fn variant_finds_the_typed_root_under_context_wraps() { + Refusal(not_found().context("outer context")) + .variant::(|e| e.kind() == std::io::ErrorKind::NotFound); + } + + #[test] + #[should_panic(expected = "refusal carries no")] + fn variant_panics_on_an_absent_type() { + Refusal(anyhow::anyhow!("boom")).variant::(|_| true); + } + + #[test] + #[should_panic(expected = "refusal variant mismatch")] + fn variant_panics_on_a_failed_predicate() { + Refusal(not_found()) + .variant::(|e| e.kind() == std::io::ErrorKind::PermissionDenied); + } } diff --git a/crates/nexum-sdk/src/chain/transport.rs b/crates/nexum-sdk/src/chain/transport.rs index ee2ef28..4636432 100644 --- a/crates/nexum-sdk/src/chain/transport.rs +++ b/crates/nexum-sdk/src/chain/transport.rs @@ -191,6 +191,7 @@ mod tests { panic!("expected failure, got {resp:?}"); }; assert_eq!(err.code, -32601); + // Operator wording pin. assert!(err.message.contains("eth_sendRawTransaction")); } @@ -222,6 +223,7 @@ mod tests { let TransportError::Transport(kind) = err else { panic!("expected transport kind, got {err:?}"); }; + // Foreign alloy TransportErrorKind; pins its rendered copy. assert!(kind.to_string().contains("timeout")); } From 7102fad4c0b871207221fd57dd4701e5adf0613d Mon Sep 17 00:00:00 2001 From: mfw78 Date: Thu, 6 Aug 2026 16:03:18 +0000 Subject: [PATCH 2/2] test(errors): assert the carried fields on the typed refusals AI Assistance: Claude (Opus 5) used for the red-team pass and the assertion strengthening. --- crates/nexum-runtime/src/addons.rs | 9 +-- crates/nexum-runtime/src/builder.rs | 74 ++++--------------- .../src/supervisor/tests/boot_refusals.rs | 17 ++--- .../src/supervisor/tests/digest.rs | 15 ++-- .../src/supervisor/tests/ledger.rs | 4 +- crates/nexum-runtime/src/test_utils/mod.rs | 9 +-- .../nexum-runtime/src/test_utils/scenario.rs | 24 ++---- 7 files changed, 45 insertions(+), 107 deletions(-) diff --git a/crates/nexum-runtime/src/addons.rs b/crates/nexum-runtime/src/addons.rs index b073e0a..54038c8 100644 --- a/crates/nexum-runtime/src/addons.rs +++ b/crates/nexum-runtime/src/addons.rs @@ -90,6 +90,7 @@ impl RuntimeAddOn for PrometheusAddOn { mod tests { use super::*; use crate::engine_config::MetricsSection; + use crate::test_utils::Refusal; /// An enabled exporter with an unparseable bind address fails at install. #[test] @@ -103,12 +104,8 @@ mod tests { Ok(_) => panic!("invalid bind_addr must not install"), Err(err) => err, }; - assert!( - matches!( - err.downcast_ref::(), - Some(PrometheusError::BindAddr { addr, .. }) if addr == "not-a-socket-addr" - ), - "{err:#}" + Refusal::from(err).variant::( + |e| matches!(e, PrometheusError::BindAddr { addr, .. } if addr == "not-a-socket-addr"), ); } } diff --git a/crates/nexum-runtime/src/builder.rs b/crates/nexum-runtime/src/builder.rs index 798c39e..58a230f 100644 --- a/crates/nexum-runtime/src/builder.rs +++ b/crates/nexum-runtime/src/builder.rs @@ -679,7 +679,9 @@ mod tests { use crate::supervisor::prepass::BootRefusal; use crate::test_utils::clock::ManualClock; use crate::test_utils::wasm::workspace_root; - use crate::test_utils::{Prebuilt, TestManifest, example_wasm_or_skip, module_wasm_or_skip}; + use crate::test_utils::{ + Prebuilt, Refusal, TestManifest, example_wasm_or_skip, module_wasm_or_skip, + }; use wasmtime::component::Linker; /// The preset shortcut reaches the supervisor boot, which bails on the @@ -698,13 +700,7 @@ mod tests { Ok(_) => panic!("default config declares no modules; launch must bail"), Err(err) => err, }; - assert!( - matches!( - err.downcast_ref::(), - Some(LaunchRefusal::NothingToRun) - ), - "{err:#}" - ); + Refusal::from(err).variant::(|e| matches!(e, LaunchRefusal::NothingToRun)); } /// Counts linker hook runs. @@ -786,13 +782,7 @@ mod tests { Ok(_) => panic!("default config declares no modules; launch must bail"), Err(err) => err, }; - assert!( - matches!( - err.downcast_ref::(), - Some(LaunchRefusal::NothingToRun) - ), - "{err:#}" - ); + Refusal::from(err).variant::(|e| matches!(e, LaunchRefusal::NothingToRun)); assert_eq!(preset_linked.load(Ordering::SeqCst), 1, "preset extension"); assert_eq!( appended_linked.load(Ordering::SeqCst), @@ -852,13 +842,7 @@ mod tests { Ok(_) => panic!("default config declares no modules; launch must bail"), Err(err) => err, }; - assert!( - matches!( - err.downcast_ref::(), - Some(LaunchRefusal::NothingToRun) - ), - "{err:#}" - ); + Refusal::from(err).variant::(|e| matches!(e, LaunchRefusal::NothingToRun)); seen.get().expect("clock attached before boot").clone() } @@ -998,13 +982,7 @@ mod tests { Ok(_) => panic!("default config declares no modules; launch must bail"), Err(err) => err, }; - assert!( - matches!( - err.downcast_ref::(), - Some(LaunchRefusal::NothingToRun) - ), - "{err:#}" - ); + Refusal::from(err).variant::(|e| matches!(e, LaunchRefusal::NothingToRun)); assert_eq!( built.load(Ordering::SeqCst), 1, @@ -1084,13 +1062,9 @@ mod tests { Ok(_) => panic!("init-failing module must abort launch"), Err(err) => err, }; - assert!( - matches!( - err.downcast_ref::(), - Some(LaunchRefusal::AllDeadOverride { modules: 1 }) - ), - "{err:#}" - ); + Refusal::from(err).variant::(|e| { + matches!(e, LaunchRefusal::AllDeadOverride { modules: 1 }) + }); } #[tokio::test] @@ -1122,14 +1096,10 @@ mod tests { Ok(_) => panic!("an unconfigured chain subscription must abort launch"), Err(err) => err, }; - assert!( - matches!( - err.downcast_ref::(), - Some(BootRefusal::UnconfiguredChain { name, chain_id: 424_242, .. }) - if name == "example" - ), - "the launch error is the boot-time chain refusal: {err:#}", - ); + Refusal::from(err).variant::(|e| { + matches!(e, BootRefusal::UnconfiguredChain { noun: "module", name, chain_id: 424_242, .. } + if name == "example") + }); } /// Add-ons install before the supervisor boots, exactly once. @@ -1178,13 +1148,7 @@ mod tests { Ok(_) => panic!("no modules configured; launch must bail"), Err(err) => err, }; - assert!( - matches!( - err.downcast_ref::(), - Some(LaunchRefusal::NothingToRun) - ), - "{err:#}" - ); + Refusal::from(err).variant::(|e| matches!(e, LaunchRefusal::NothingToRun)); assert_eq!( calls.load(Ordering::SeqCst), 1, @@ -1275,13 +1239,7 @@ mod tests { .wait() .await .expect_err("aborted task surfaces an error"); - assert!( - matches!( - err.downcast_ref::(), - Some(LaunchRefusal::EventLoopGone) - ), - "{err:#}" - ); + Refusal::from(err).variant::(|e| matches!(e, LaunchRefusal::EventLoopGone)); } /// Dropping the handle without `wait` still drains the event loop. diff --git a/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs b/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs index aa2c2a6..9ffdaa2 100644 --- a/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs +++ b/crates/nexum-runtime/src/supervisor/tests/boot_refusals.rs @@ -155,7 +155,6 @@ async fn boot_single_skips_the_provider_kind_service_gate() { .expect("a missing manifest must refuse the boot"); Refusal::from(err) .variant::(|e| matches!(e, BootRefusal::ManifestMissing { .. })) - // Operator wording pin. .lacks("without a host service"); } @@ -262,7 +261,6 @@ async fn boot_refuses_a_blank_manifest_name_for_both_roles() { .expect_refusal() .await .variant::(|e| matches!(e, BootRefusal::Manifest(ParseError::BlankModuleName))) - // Operator wording pin. .lacks("claimed twice") .lacks("read component") .lacks("compile"); @@ -275,7 +273,6 @@ async fn boot_refuses_a_blank_manifest_name_for_both_roles() { .variant::(|e| { matches!(e, BootRefusal::Manifest(ParseError::BlankModuleName)) }) - // Operator wording pin. .lacks("claimed twice") .lacks("read component") .lacks("compile"); @@ -299,9 +296,10 @@ async fn boot_denies_an_undeclared_chain_import_for_balance_tracker() { ) .expect_refusal() .await - .variant::( - |e| matches!(e, CapabilityError::Undeclared(v) if v.capability == "chain"), - ); + .variant::(|e| { + matches!(e, CapabilityError::Undeclared(v) + if v.capability == "chain" && v.wit_import.starts_with("nexum:host/chain")) + }); } /// The example component's only gated import is `logging`, so the refusal @@ -317,7 +315,8 @@ async fn boot_denies_an_undeclared_logging_import_for_a_provider() { .adapter(Entry::new(TestManifest::new("acme").kind("acme-adapter").cap("chain")).wasm(wasm)) .expect_refusal() .await - .variant::( - |e| matches!(e, CapabilityError::Undeclared(v) if v.capability == "logging"), - ); + .variant::(|e| { + matches!(e, CapabilityError::Undeclared(v) + if v.capability == "logging" && v.wit_import.starts_with("nexum:host/logging")) + }); } diff --git a/crates/nexum-runtime/src/supervisor/tests/digest.rs b/crates/nexum-runtime/src/supervisor/tests/digest.rs index 0813131..0586637 100644 --- a/crates/nexum-runtime/src/supervisor/tests/digest.rs +++ b/crates/nexum-runtime/src/supervisor/tests/digest.rs @@ -51,7 +51,6 @@ fn read_verified_component_requires_a_digest_when_the_flag_is_set() { .expect("an unpinned artifact must refuse under the flag"); Refusal::from(err) .variant::(|e| matches!(e, LoadRefusal::DigestUnpinned { .. })) - // Operator wording pin. .lacks("compile"); } @@ -139,8 +138,10 @@ async fn boot_single_refuses_a_mismatched_component_digest() { let (_store, result) = try_boot_single(&wasm, Some(&manifest), false, None).await; Refusal::from(result.err().expect("a stale pin must refuse the boot")) - .variant::(|_| true) - // Operator wording pin. + .variant::(|e| { + e.declared == wrong_digest() + && e.actual == ContentDigest::of_bytes(b"drifted artifact bytes") + }) .lacks("compile"); } @@ -158,7 +159,6 @@ async fn boot_single_requires_a_digest_when_the_engine_flag_is_set() { .expect("an unpinned manifest must refuse under the flag"), ) .variant::(|e| matches!(e, LoadRefusal::DigestUnpinned { .. })) - // Operator wording pin. .lacks("compile"); } @@ -196,8 +196,10 @@ async fn boot_refuses_a_provider_with_a_mismatched_digest() { ) .expect_refusal() .await - .variant::(|_| true) - // Operator wording pin. + .variant::(|e| { + e.declared == wrong_digest() + && e.actual == ContentDigest::of_bytes(b"drifted provider bytes") + }) .lacks("compile"); } @@ -225,6 +227,5 @@ async fn boot_requires_a_module_digest_when_the_engine_flag_is_set() { .expect_refusal() .await .variant::(|e| matches!(e, LoadRefusal::DigestUnpinned { .. })) - // Operator wording pin. .lacks("compile"); } diff --git a/crates/nexum-runtime/src/supervisor/tests/ledger.rs b/crates/nexum-runtime/src/supervisor/tests/ledger.rs index e394341..6c12637 100644 --- a/crates/nexum-runtime/src/supervisor/tests/ledger.rs +++ b/crates/nexum-runtime/src/supervisor/tests/ledger.rs @@ -168,7 +168,6 @@ async fn boot_rejects_duplicate_names_across_and_within_roles() { && held.ends_with("missing-adapter.wasm") && path.ends_with("missing-module.wasm")) }) - // Operator wording pin. .lacks("compile"); let scenario = BootScenario::new(); @@ -182,11 +181,10 @@ async fn boot_rejects_duplicate_names_across_and_within_roles() { .expect_refusal() .await .variant::(|e| { - matches!(e, BootRefusal::NamespaceClaimed { name, held, path, .. } + matches!(e, BootRefusal::NamespaceClaimed { name, held_role: "module", held, role: "module", path } if name == "dup" && held.ends_with("missing-a.wasm") && path.ends_with("missing-b.wasm")) }) - // Operator wording pin. .lacks("compile"); } diff --git a/crates/nexum-runtime/src/test_utils/mod.rs b/crates/nexum-runtime/src/test_utils/mod.rs index d50a9a1..247f970 100644 --- a/crates/nexum-runtime/src/test_utils/mod.rs +++ b/crates/nexum-runtime/src/test_utils/mod.rs @@ -105,6 +105,7 @@ mod tests { use crate::builder::{LaunchRefusal, RuntimeBuilder}; use crate::engine_config::EngineConfig; use crate::host::component::{ChainMethod, ComponentsBuilder, StateHandle, StateStore}; + use crate::test_utils::Refusal; /// A custom component set launches through the public builder on fakes; /// it bails at boot only because the default config declares no modules, @@ -129,13 +130,7 @@ mod tests { Ok(_) => panic!("default config declares no modules; launch must bail"), Err(err) => err, }; - assert!( - matches!( - err.downcast_ref::(), - Some(LaunchRefusal::NothingToRun) - ), - "{err:#}" - ); + Refusal::from(err).variant::(|e| matches!(e, LaunchRefusal::NothingToRun)); // The fake actually serves and records, independent of the launch. let body = pool diff --git a/crates/nexum-runtime/src/test_utils/scenario.rs b/crates/nexum-runtime/src/test_utils/scenario.rs index 40890a5..2c6131a 100644 --- a/crates/nexum-runtime/src/test_utils/scenario.rs +++ b/crates/nexum-runtime/src/test_utils/scenario.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use alloy_chains::Chain; +use derive_more::From; use tempfile::TempDir; use super::manifest::{ManifestSource, TestManifest}; @@ -310,15 +311,9 @@ impl Booted { } } -#[derive(Debug)] +#[derive(Debug, From)] pub struct Refusal(anyhow::Error); -impl From for Refusal { - fn from(err: anyhow::Error) -> Self { - Self(err) - } -} - impl Refusal { /// The typed root under the context wraps, for `matches!` on a variant. pub fn root(&self) -> Option<&E> { @@ -331,14 +326,14 @@ impl Refusal { where E: std::error::Error + Send + Sync + 'static, { - match self.root::() { - None => panic!( + let Some(root) = self.root::() else { + panic!( "refusal carries no {}: {}", std::any::type_name::(), self.chain(), - ), - Some(root) => assert!(pred(root), "refusal variant mismatch: {}", self.chain()), - } + ) + }; + assert!(pred(root), "refusal variant mismatch: {root:?}"); self } @@ -550,7 +545,6 @@ mod tests { matches!(e, BootRefusal::Manifest(ParseError::UnknownCapability { name, .. }) if name == "telepathy") }) - // Operator wording pin. .lacks("compile"); } @@ -563,7 +557,6 @@ mod tests { .variant::( |e| matches!(e, LoadRefusal::UnregisteredKind { kind, .. } if kind == "acme-feed"), ) - // Operator wording pin. .lacks("compile"); } @@ -579,7 +572,6 @@ mod tests { matches!(e, BootRefusal::ManifestMissing { component } if component.ends_with("orphan.wasm")) }) - // Operator wording pin. .lacks("compile"); } @@ -595,7 +587,6 @@ mod tests { matches!(e, BootRefusal::ManifestNotFound { manifest, .. } if manifest.ends_with("modle.toml")) }) - // Operator wording pin. .lacks("compile"); } @@ -609,7 +600,6 @@ mod tests { matches!(e, LoadRefusal::SectionUnclaimed { owner, section } if owner == "acme-user" && section == "acme") }) - // Operator wording pin. .lacks("read component"); BootScenario::new()