//! F49 step 3 — F47 buffered-unsubscribe skip live verification. //! //! `Session::unsubscribe` on a buffered subscription must NOT emit a //! wire-side `UnAdvise` op (mirrors the .NET reference's //! `if (!subscription.IsBuffered)` guard at `MxNativeSession.cs:361-381`). //! Buffered subscriptions are unwound by the engine when the //! `RegisterReference` handle goes away — there's no item-level advise //! to retract. //! //! Structural verification is exhaustive at the unit level (see //! `unsubscribe_skips_un_advise_for_buffered_subscription` in //! `crates/mxaccess/src/session.rs`). This live test confirms the //! behaviour against a real engine: subscribe buffered, immediately //! unsubscribe, verify both calls succeed without surfacing transport //! or HRESULT errors. If `unsubscribe` accidentally issued an //! `UnAdvise` for a buffered correlation id, the engine would either //! reject it (HRESULT != 0) or silently break the unrelated state — //! both surface as a panic here. #![allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::panic )] #[cfg(all(windows, feature = "live-windows-com"))] mod live { use std::sync::Arc; use mxaccess::{BufferedOptions, RecoveryPolicy, Session, SessionOptions}; use mxaccess_galaxy::SqlTagResolver; use mxaccess_rpc::ntlm::NtlmClientContext; fn ntlm_from_test_env() -> NtlmClientContext { let user = std::env::var("MX_TEST_USER").expect("MX_TEST_USER"); let password = std::env::var("MX_TEST_PASSWORD").expect("MX_TEST_PASSWORD"); let domain = std::env::var("MX_TEST_DOMAIN").unwrap_or_default(); let hostname = std::env::var("COMPUTERNAME").unwrap_or_default(); NtlmClientContext::new(&user, &password, &domain, Some(&hostname)) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[ignore] async fn buffered_unsubscribe_skips_unadvise() { if std::env::var_os("MX_LIVE").is_none() { eprintln!("MX_LIVE not set — skipping live test"); return; } let tag = std::env::var("MX_TEST_TAG") .unwrap_or_else(|_| "TestMachine_001.TestChangingInt".to_string()); let _ = tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .with_test_writer() .try_init(); let galaxy_db = std::env::var("MX_GALAXY_DB").expect("MX_GALAXY_DB"); let resolver = Arc::new( SqlTagResolver::from_ado_string(&galaxy_db).expect("SqlTagResolver"), ); let session = Session::connect_nmx_auto( ntlm_from_test_env, SessionOptions::default(), resolver, RecoveryPolicy::default(), ) .await .expect("connect_nmx_auto"); eprintln!("session connected"); let opts = BufferedOptions { update_interval_ms: 1_000, }; let sub = session .subscribe_buffered(&tag, opts) .await .expect("subscribe_buffered"); eprintln!( "buffered subscribed, correlation_id = {:02x?}", sub.correlation_id() ); // Sub-second hold so the engine has at least one DataUpdate // tick in flight when we unsubscribe. tokio::time::sleep(std::time::Duration::from_millis(750)).await; // The contract: unsubscribe on a buffered subscription // returns Ok and does NOT issue UnAdvise on the wire. // If it incorrectly emitted UnAdvise for a buffered // correlation id, the engine would return non-zero HRESULT // (no matching plain advise to retract) and surface here. session .unsubscribe(sub) .await .expect("unsubscribe (buffered) must succeed without emitting UnAdvise"); eprintln!("buffered unsubscribe returned Ok — F47 skip path verified live"); session.shutdown_nmx().await.expect("shutdown"); } } #[cfg(not(all(windows, feature = "live-windows-com")))] mod live { #[test] #[ignore] fn buffered_unsubscribe_skips_unadvise() { eprintln!("test skipped: requires Windows + live-windows-com feature"); } }