@@ -939,3 +939,110 @@ fn test_end_to_end_shared_fresh_policy_mints_independent_instances() {
939939 "fresh factory should have been invoked exactly 2x (one per require_shared consumer)"
940940 ) ;
941941}
942+
943+ // ── Envelope / error-shape regression tests ─────────────────────────────────
944+
945+ /// The shared-side registry envelope must be `Box<Box<dyn C::Shared>>`
946+ /// erased as `Box<dyn Any + Send>`. `require_shared` downcasts the
947+ /// outer `Any` to `Box<dyn C::Shared>` then dereferences. This test
948+ /// pins the shape so anyone refactoring the macro cannot collapse the
949+ /// double-box without the consumer path failing loudly.
950+ #[ test]
951+ fn test_shared_entry_produce_uses_double_box_envelope ( ) {
952+ let factory = shared_instance_factory ( "envelope-val" ) ;
953+ let entry = TestCap :: shared_entry :: < SharedImpl > ( "ext-env" . into ( ) , factory) ;
954+
955+ // Directly invoke the stored produce closure and downcast using
956+ // the exact shape the consumer (`require_shared`) relies on.
957+ let erased: Box < dyn Any + Send > = ( entry. produce ) ( ) ;
958+ let boxed_trait_object: Box < Box < dyn TestCapShared > > = erased
959+ . downcast :: < Box < dyn TestCapShared > > ( )
960+ . expect ( "shared_entry must emit Box<Box<dyn C::Shared>> erased as Box<dyn Any + Send>" ) ;
961+ assert_eq ! ( ( * boxed_trait_object) . value( ) , "envelope-val" ) ;
962+ }
963+
964+ /// `require_local` on an unbound capability must return the dedicated
965+ /// `CapabilityNotBound` variant (not a generic `ConfigError`) so the
966+ /// diagnostic points at "node-code/declaration mismatch", not
967+ /// "user YAML problem".
968+ #[ test]
969+ fn test_require_local_unbound_returns_capability_not_bound ( ) {
970+ let reg = CapabilityRegistry :: new ( ) ;
971+ let mut tracker = ConsumedTracker :: new ( ) ;
972+ let caps = resolve_bindings ( & HashMap :: new ( ) , & reg, & known_exts ( & [ ] ) , & mut tracker) . unwrap ( ) ;
973+
974+ match caps. require_local :: < TestCap > ( ) {
975+ Err ( Error :: CapabilityNotBound {
976+ capability,
977+ execution_model,
978+ } ) => {
979+ assert_eq ! ( capability, "test_cap" ) ;
980+ assert_eq ! ( execution_model, "local" ) ;
981+ }
982+ Err ( other) => panic ! ( "expected CapabilityNotBound, got {other:?}" ) ,
983+ Ok ( _) => panic ! ( "expected CapabilityNotBound, got Ok" ) ,
984+ }
985+ }
986+
987+ #[ test]
988+ fn test_require_shared_unbound_returns_capability_not_bound ( ) {
989+ let reg = CapabilityRegistry :: new ( ) ;
990+ let mut tracker = ConsumedTracker :: new ( ) ;
991+ let caps = resolve_bindings ( & HashMap :: new ( ) , & reg, & known_exts ( & [ ] ) , & mut tracker) . unwrap ( ) ;
992+
993+ match caps. require_shared :: < TestCap > ( ) {
994+ Err ( Error :: CapabilityNotBound {
995+ capability,
996+ execution_model,
997+ } ) => {
998+ assert_eq ! ( capability, "test_cap" ) ;
999+ assert_eq ! ( execution_model, "shared" ) ;
1000+ }
1001+ Err ( other) => panic ! ( "expected CapabilityNotBound, got {other:?}" ) ,
1002+ Ok ( _) => panic ! ( "expected CapabilityNotBound, got Ok" ) ,
1003+ }
1004+ }
1005+
1006+ /// When a node binds a shared-only extension through its *local*-facing
1007+ /// capability (the `SharedAsLocal` fallback), consumption of the
1008+ /// fallback adapter must flip the shared bucket of `ConsumedTracker`
1009+ /// — not a phantom local bucket. Otherwise `unconsumed_shared` would
1010+ /// claim the shared variant is unused and the engine would drop it
1011+ /// while the adapter still depended on it.
1012+ #[ test]
1013+ fn test_fallback_local_consumption_flips_shared_bucket ( ) {
1014+ let mut reg = CapabilityRegistry :: new ( ) ;
1015+ register_shared ( & mut reg, "ext-only-shared" , "val" ) ;
1016+
1017+ let mut tracker = ConsumedTracker :: new ( ) ;
1018+ let caps = resolve_bindings (
1019+ & bindings ( "test_cap" , "ext-only-shared" ) ,
1020+ & reg,
1021+ & known_exts ( & [ "ext-only-shared" ] ) ,
1022+ & mut tracker,
1023+ )
1024+ . unwrap ( ) ;
1025+
1026+ // Before claim: both buckets show the shared extension unconsumed.
1027+ assert ! (
1028+ tracker
1029+ . unconsumed_shared( )
1030+ . iter( )
1031+ . any( |( ext, _) | ext. as_ref( ) == "ext-only-shared" ) ,
1032+ "shared bucket should list ext-only-shared before the fallback claim" ,
1033+ ) ;
1034+ assert ! (
1035+ tracker. unconsumed_local( ) . is_empty( ) ,
1036+ "no native local registration was made, so the local bucket must be empty" ,
1037+ ) ;
1038+
1039+ // Fallback claim (local-facing, but backed by the shared factory).
1040+ let _ = caps. require_local :: < TestCap > ( ) . unwrap ( ) ;
1041+
1042+ // After claim: shared bucket flips (the shared *variant* is what
1043+ // got used).
1044+ assert ! (
1045+ tracker. unconsumed_shared( ) . is_empty( ) ,
1046+ "fallback consumption must flip the shared bucket" ,
1047+ ) ;
1048+ }
0 commit comments