diff --git a/docs/M6-live-verification.md b/docs/M6-live-verification.md index 9fea8b7..a42f609 100644 --- a/docs/M6-live-verification.md +++ b/docs/M6-live-verification.md @@ -158,3 +158,49 @@ cargo test -p mxaccess-compat --features live-windows-com ` ## Open work None. F49 sweep complete; F50 (residual Frida capture for Suspend/Activate) closed 2026-05-06 per `docs/F50-suspend-activate-evidence.md`. + +## Workspace gate (2026-05-07) + +End-of-session sanity sweep against `master` at commit `9ed4700` plus the F56 unit-test fixture fix that this gate flagged. Run from `rust/` on Windows x64. + +| Gate | Command | Result | +|---|---|---| +| Build | `cargo build --workspace --all-targets` | **Pass** (19.81 s) | +| Tests | `cargo test --workspace --no-fail-fast` | **Pass** — 847 passed, 0 failed, 9 ignored (live-only) | +| Clippy | `cargo clippy --workspace --all-targets -- -D warnings` | **Pass** | +| Bench | `cargo bench -p mxaccess-codec` | **Pass** — R12 < 5 allocs/write target met | + +The `cargo fmt --all -- --check` gate flags pre-existing workspace-wide rustfmt drift across 29 files (~1000 lines, mostly machine-generated `mxaccess-asb-nettcp/src/nbfs.rs`). Drift is unrelated to any individual session's edits and is documented here as a known workspace-hygiene gap; per-file formatting is applied to edited files at edit time. + +### F56 test-fixture bug surfaced + fixed by this gate + +The workspace test sweep flagged 9 failing unit tests in `mxaccess::session` that had been silently failing since F56 landed (commit `5e11b30`). Root cause: F56 added `ensure_publisher_connected` (issuing `INmxService2::Connect` + `AddSubscriberEngine` before each `AdviseSupervisory`) but the in-process fake-NMX-server fixtures' `responses` vec sizes weren't bumped to absorb the two new RPCs. Symptom was `ConnectionAborted (10053)` once the fake server's response budget ran out mid-handshake. + +Fix: bumped each test's `unauthenticated_server` / `recording_server` response count by 2 to cover Connect + AddSubscriberEngine. Tests touched (all in `crates/mxaccess/src/session.rs::tests`): + +- `subscribe_then_unsubscribe_round_trip` (2 → 4 responses) +- `two_subscribes_produce_distinct_correlation_ids` (4 → 6; second subscribe hits the per-engine cache) +- `subscription_stream_yields_data_change_for_matching_correlation` (1 → 3) +- `subscription_stream_filters_out_mismatched_correlation_for_status` (1 → 3) +- `subscription_stream_keeps_data_update_regardless_of_correlation` (1 → 3) +- `subscribe_populates_registry_unsubscribe_clears_it` (2 → 4) +- `read_returns_first_data_change_within_timeout` (2 → 4) +- `read_returns_timeout_when_no_data_arrives` (2 → 4) +- `unsubscribe_skips_un_advise_for_buffered_subscription` (2 → 3 + mid-flow assertion bumped from `len() == 1` to `len() == 3`) + +Bench numbers post-fix (release profile, Windows x64): + +| scenario | allocs/op | +|---|---| +| `write_message::encode` (Int32) | 2.00 | +| `write_message::encode` (Float32) | 2.00 | +| `write_message::encode` (Float64) | 2.00 | +| `write_message::encode` (Boolean) | 1.00 | +| `write_message::encode` (String, 5 chars) | 4.00 | +| `write_message::encode_to_bytes_mut` (Int32, F52.1) | 2.00 | +| `write_message::encode_into_bytes_mut` (Int32, pooled, F52.3) | 1.00 | +| `write_message::encode_into_bytes_mut` (Boolean, pooled, F52.3) | 0.00 | +| `MxReferenceHandle::from_names` (cache, F52.2) | 0.00 | +| `NmxSubscriptionMessage::parse_inner` (DataUpdate, Int32) | 1.00 | + +All numbers match `design/M6-bench-baseline.md` § F52.{1,2,3}. diff --git a/rust/crates/mxaccess/src/session.rs b/rust/crates/mxaccess/src/session.rs index af53aad..bd6fa8c 100644 --- a/rust/crates/mxaccess/src/session.rs +++ b/rust/crates/mxaccess/src/session.rs @@ -2952,8 +2952,16 @@ mod tests { #[tokio::test] async fn subscribe_then_unsubscribe_round_trip() { - // Two RPCs: AdviseSupervisory + UnAdvise. Both return HRESULT 0. - let (addr, handle) = unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new())]).await; + // Four RPCs: Connect + AddSubscriberEngine (F56's + // ensure_publisher_connected) + AdviseSupervisory + UnAdvise. + // All return HRESULT 0. + let (addr, handle) = unauthenticated_server(vec![ + (0, Vec::new()), + (0, Vec::new()), + (0, Vec::new()), + (0, Vec::new()), + ]) + .await; let resolver: Arc = Arc::new(StaticResolver::new(&[( "TestObj.TestInt", sample_metadata(), @@ -3004,12 +3012,15 @@ mod tests { #[tokio::test] async fn two_subscribes_produce_distinct_correlation_ids() { - // Two AdviseSupervisory calls + two UnAdvise calls. + // Six RPCs: Connect + AddSubscriberEngine (once, cached on the + // 2nd subscribe) + 2 AdviseSupervisory + 2 UnAdvise. let (addr, handle) = unauthenticated_server(vec![ (0, Vec::new()), (0, Vec::new()), (0, Vec::new()), (0, Vec::new()), + (0, Vec::new()), + (0, Vec::new()), ]) .await; let resolver: Arc = Arc::new(StaticResolver::new(&[( @@ -3242,7 +3253,9 @@ mod tests { async fn subscription_stream_yields_data_change_for_matching_correlation() { use futures_util::StreamExt; - let (addr, handle) = unauthenticated_server(vec![(0, Vec::new())]).await; + // Three RPCs: Connect + AddSubscriberEngine (F56) + AdviseSupervisory. + let (addr, handle) = + unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new()), (0, Vec::new())]).await; let resolver: Arc = Arc::new(StaticResolver::new(&[( "TestObj.TestInt", sample_metadata(), @@ -3287,7 +3300,9 @@ mod tests { async fn subscription_stream_filters_out_mismatched_correlation_for_status() { use futures_util::StreamExt; - let (addr, handle) = unauthenticated_server(vec![(0, Vec::new())]).await; + // Three RPCs: Connect + AddSubscriberEngine (F56) + AdviseSupervisory. + let (addr, handle) = + unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new()), (0, Vec::new())]).await; let resolver: Arc = Arc::new(StaticResolver::new(&[( "TestObj.TestInt", sample_metadata(), @@ -3322,8 +3337,9 @@ mod tests { use futures_util::StreamExt; // 0x33 DataUpdate has no item_correlation_id; the .NET-style // filter passes them through to all subscriptions. - - let (addr, handle) = unauthenticated_server(vec![(0, Vec::new())]).await; + // Three RPCs: Connect + AddSubscriberEngine (F56) + AdviseSupervisory. + let (addr, handle) = + unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new()), (0, Vec::new())]).await; let resolver: Arc = Arc::new(StaticResolver::new(&[( "TestObj.TestInt", sample_metadata(), @@ -3472,7 +3488,15 @@ mod tests { // F16: every successful subscribe() inserts into the // SubscriptionEntry registry; unsubscribe() removes it. // Recovery walks this registry to replay AdviseSupervisory. - let (addr, handle) = unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new())]).await; + // Four RPCs: Connect + AddSubscriberEngine (F56) + + // AdviseSupervisory + UnAdvise. + let (addr, handle) = unauthenticated_server(vec![ + (0, Vec::new()), + (0, Vec::new()), + (0, Vec::new()), + (0, Vec::new()), + ]) + .await; let resolver: Arc = Arc::new(StaticResolver::new(&[( "TestObj.TestInt", sample_metadata(), @@ -3802,8 +3826,15 @@ mod tests { #[tokio::test] async fn read_returns_first_data_change_within_timeout() { - // Server: AdviseSupervisory ack + UnAdvise ack. - let (addr, handle) = unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new())]).await; + // Server: Connect + AddSubscriberEngine (F56) + + // AdviseSupervisory + UnAdvise. + let (addr, handle) = unauthenticated_server(vec![ + (0, Vec::new()), + (0, Vec::new()), + (0, Vec::new()), + (0, Vec::new()), + ]) + .await; let resolver: Arc = Arc::new(StaticResolver::new(&[( "TestObj.TestInt", sample_metadata(), @@ -3851,9 +3882,16 @@ mod tests { #[tokio::test] async fn read_returns_timeout_when_no_data_arrives() { - // Server only handles the AdviseSupervisory + UnAdvise (no data - // injection). Read must hit the timeout branch. - let (addr, handle) = unauthenticated_server(vec![(0, Vec::new()), (0, Vec::new())]).await; + // Server only handles the Connect + AddSubscriberEngine (F56) + + // AdviseSupervisory + UnAdvise (no data injection). Read must + // hit the timeout branch. + let (addr, handle) = unauthenticated_server(vec![ + (0, Vec::new()), + (0, Vec::new()), + (0, Vec::new()), + (0, Vec::new()), + ]) + .await; let resolver: Arc = Arc::new(StaticResolver::new(&[( "TestObj.TestInt", sample_metadata(), @@ -4407,21 +4445,29 @@ mod tests { /// the negative control; this test pins the buffered branch. #[tokio::test] async fn unsubscribe_skips_un_advise_for_buffered_subscription() { - let (addr, recorded, handle) = - recording_server(vec![(0, Vec::new()), (0, Vec::new())]).await; + // Three RPCs: Connect + AddSubscriberEngine (F56) + + // AdviseSupervisory. The buffered unsubscribe MUST NOT add a + // fourth (F47 skips UnAdvise on buffered drop). + let (addr, recorded, handle) = recording_server(vec![ + (0, Vec::new()), + (0, Vec::new()), + (0, Vec::new()), + ]) + .await; let resolver: Arc = Arc::new(StaticResolver::new(&[( "TestObj.TestInt", sample_metadata(), )])); let session = connect_test_session(addr, resolver).await.unwrap(); - // Issue a plain subscribe — server records AdviseSupervisory. + // Issue a plain subscribe — server records Connect + + // AddSubscriberEngine + AdviseSupervisory. let sub = session.subscribe("TestObj.TestInt").await.unwrap(); let cid = sub.correlation_id; assert_eq!( recorded.lock().unwrap().len(), - 1, - "subscribe should issue 1 RPC" + 3, + "subscribe should issue 3 RPCs (Connect + AddSubscriberEngine + AdviseSupervisory)" ); // Mutate the registry entry's mode to Buffered (synthesise the @@ -4438,13 +4484,14 @@ mod tests { } // Unsubscribe the now-buffered entry. F47 contract: NO - // UnAdvise is emitted on the wire; recorded count stays at 1. + // UnAdvise is emitted on the wire; recorded count stays at 3 + // (the Connect + AddSubscriberEngine + AdviseSupervisory from + // the original subscribe). session.unsubscribe(sub).await.unwrap(); assert_eq!( recorded.lock().unwrap().len(), - 1, - "buffered unsubscribe must not issue UnAdvise; recorded RPC count must stay at 1 \ - (the original AdviseSupervisory)" + 3, + "buffered unsubscribe must not issue UnAdvise; recorded RPC count must stay at 3" ); // Registry is still cleared — F47's skip applies only to the