|
| 1 | +//! Phase 3 server-port exit criterion (§4): `pump_client` mirrors a record |
| 2 | +//! **both directions** between a local AimDb and a remote AimDb over the shared |
| 3 | +//! session engine. |
| 4 | +//! |
| 5 | +//! Topology: a server `AimDb` (served by `build_aimx_server`) and a client |
| 6 | +//! `AimDb` whose records carry `aimx://` connector links. `run_client` opens the |
| 7 | +//! connection; `pump_client` wires the client's outbound/inbound routes to the |
| 8 | +//! `ClientHandle`: |
| 9 | +//! - **client → server**: producing the client's `cfg` record streams it to the |
| 10 | +//! server via `ClientHandle::write` → the server's `record.set` path. |
| 11 | +//! - **server → client**: updating the server's `tele` record streams it back |
| 12 | +//! through a subscription → the client's inbound producer (arbiter path). |
| 13 | +
|
| 14 | +use std::sync::Arc; |
| 15 | +use std::time::Duration; |
| 16 | + |
| 17 | +use aimdb_core::buffer::BufferCfg; |
| 18 | +use aimdb_core::remote::{AimxConfig, SecurityPolicy}; |
| 19 | +use aimdb_core::session::aimx::{build_aimx_server, AimxClientConnector}; |
| 20 | +use aimdb_core::session::ClientConfig; |
| 21 | +use aimdb_core::{AimDb, AimDbBuilder}; |
| 22 | +use aimdb_tokio_adapter::{TokioAdapter, TokioRecordRegistrarExt}; |
| 23 | +use serde::{Deserialize, Serialize}; |
| 24 | +use serde_json::json; |
| 25 | + |
| 26 | +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| 27 | +struct Msg { |
| 28 | + v: u64, |
| 29 | +} |
| 30 | + |
| 31 | +/// Re-assert `db.<key>` reaches `want`, re-driving `push` each tick so the test |
| 32 | +/// is robust against subscription-registration timing (a fresh subscriber may |
| 33 | +/// only see values produced after it attaches). |
| 34 | +async fn mirror_reaches( |
| 35 | + db: &Arc<AimDb<TokioAdapter>>, |
| 36 | + key: &str, |
| 37 | + want: &serde_json::Value, |
| 38 | + mut push: impl FnMut(), |
| 39 | +) -> bool { |
| 40 | + for _ in 0..100 { |
| 41 | + push(); |
| 42 | + tokio::time::sleep(Duration::from_millis(20)).await; |
| 43 | + if db.try_latest_as_json(key).as_ref() == Some(want) { |
| 44 | + return true; |
| 45 | + } |
| 46 | + } |
| 47 | + false |
| 48 | +} |
| 49 | + |
| 50 | +#[tokio::test] |
| 51 | +async fn pump_client_mirrors_record_both_directions() { |
| 52 | + let dir = tempfile::tempdir().unwrap(); |
| 53 | + let sock = dir.path().join("aimdb.sock"); |
| 54 | + |
| 55 | + // --- server: cfg (writable target) + tele (streamed source) ------------ |
| 56 | + let mut sb = AimDbBuilder::new().runtime(Arc::new(TokioAdapter)); |
| 57 | + sb.configure::<Msg>("cfg", |reg| { |
| 58 | + reg.buffer(BufferCfg::SingleLatest).with_remote_access(); |
| 59 | + }); |
| 60 | + sb.configure::<Msg>("tele", |reg| { |
| 61 | + reg.buffer(BufferCfg::SingleLatest).with_remote_access(); |
| 62 | + }); |
| 63 | + let (server_db, server_runner) = sb.build().await.expect("build server db"); |
| 64 | + let server_db = Arc::new(server_db); |
| 65 | + tokio::spawn(server_runner.run()); |
| 66 | + |
| 67 | + let mut policy = SecurityPolicy::read_write(); |
| 68 | + policy.allow_write_key("cfg"); |
| 69 | + let config = AimxConfig::uds_default() |
| 70 | + .socket_path(&sock) |
| 71 | + .security_policy(policy); |
| 72 | + let server = tokio::spawn(build_aimx_server(server_db.clone(), config).expect("bind server")); |
| 73 | + |
| 74 | + // --- client: cfg links *to* the server, tele links *from* it ----------- |
| 75 | + // The AimxClientConnector registers the `aimx://` scheme (so the links |
| 76 | + // validate) and, on build, dials the server + drives the mirroring pumps. |
| 77 | + let mut cb = AimDbBuilder::new() |
| 78 | + .runtime(Arc::new(TokioAdapter)) |
| 79 | + .with_connector(AimxClientConnector::new(&sock).with_config(ClientConfig { |
| 80 | + reconnect: true, |
| 81 | + reconnect_delay: Duration::from_millis(50), |
| 82 | + sends_hello: false, |
| 83 | + })); |
| 84 | + cb.configure::<Msg>("cfg", |reg| { |
| 85 | + reg.buffer(BufferCfg::SingleLatest) |
| 86 | + .with_remote_access() |
| 87 | + .link_to("aimx://cfg") |
| 88 | + .with_serializer_raw(|m: &Msg| Ok(serde_json::to_vec(m).expect("serialize"))) |
| 89 | + .finish(); |
| 90 | + }); |
| 91 | + cb.configure::<Msg>("tele", |reg| { |
| 92 | + reg.buffer(BufferCfg::SingleLatest) |
| 93 | + .with_remote_access() |
| 94 | + .link_from("aimx://tele") |
| 95 | + .with_deserializer_raw(|d: &[u8]| { |
| 96 | + serde_json::from_slice::<Msg>(d).map_err(|e| e.to_string()) |
| 97 | + }) |
| 98 | + .finish(); |
| 99 | + }); |
| 100 | + // build() collects the connector's engine + pump futures; the runner drives |
| 101 | + // them (spawn-free engine, driven on a task here). |
| 102 | + let (client_db, client_runner) = cb.build().await.expect("build client db"); |
| 103 | + let client_db = Arc::new(client_db); |
| 104 | + tokio::spawn(client_runner.run()); |
| 105 | + |
| 106 | + // client → server: producing client `cfg` mirrors to server `cfg`. |
| 107 | + let want_cfg = json!({ "v": 7 }); |
| 108 | + let mirrored_out = mirror_reaches(&server_db, "cfg", &want_cfg, || { |
| 109 | + client_db |
| 110 | + .set_record_from_json("cfg", json!({ "v": 7 })) |
| 111 | + .expect("set client cfg"); |
| 112 | + }) |
| 113 | + .await; |
| 114 | + assert!( |
| 115 | + mirrored_out, |
| 116 | + "client→server mirror did not reach the server" |
| 117 | + ); |
| 118 | + |
| 119 | + // server → client: updating server `tele` mirrors to client `tele`. |
| 120 | + let want_tele = json!({ "v": 9 }); |
| 121 | + let mirrored_in = mirror_reaches(&client_db, "tele", &want_tele, || { |
| 122 | + server_db |
| 123 | + .set_record_from_json("tele", json!({ "v": 9 })) |
| 124 | + .expect("set server tele"); |
| 125 | + }) |
| 126 | + .await; |
| 127 | + assert!(mirrored_in, "server→client mirror did not reach the client"); |
| 128 | + |
| 129 | + server.abort(); |
| 130 | +} |
0 commit comments