//! Live integration against a REAL local Freenet node — the house pattern //! (`#[ignore]`d corpus tests inside the crate, run on demand): //! //! 1. start a node: freenet local local --config-dir --data-dir //! 2. set SOULSTREAM_F0_WASM to a contract wasm (the F0 spike's //! research/freenet-f0/f0-contract build works verbatim) //! 3. cargo test -p freenet-adapter --release -- --ignored //! //! Exercises the full adapter loop: detect → enable → put → get → //! subscribe → update (with the first-write priming path) → notification //! event → kill switch mid-life. use freenet_adapter::{AdapterEvent, AdapterStatus, FreenetAdapter}; use std::time::Duration; fn wasm() -> Option> { let path = std::env::var("SOULSTREAM_F0_WASM").ok()?; std::fs::read(path).ok() } #[tokio::test] #[ignore = "needs a local freenet node on 7509 and SOULSTREAM_F0_WASM"] async fn full_loop_against_live_node() { let Some(code) = wasm() else { panic!("set SOULSTREAM_F0_WASM to a contract wasm path"); }; let a = FreenetAdapter::default(); // Detection first — a wrong environment should fail here, loudly. let detected = a.detect().await; assert_eq!(detected, AdapterStatus::Connected, "node not reachable: {detected:?}"); a.enable(); // Wait for the supervisor to connect. let mut tries = 0; while a.status() != AdapterStatus::Connected { tokio::time::sleep(Duration::from_millis(200)).await; tries += 1; assert!(tries < 50, "supervisor never connected: {:?}", a.status()); } // A fresh instance per run: params carry a nonce. let params = format!("adapter-live-{}", std::process::id()).into_bytes(); let ids = FreenetAdapter::contract_ids(code.clone(), ¶ms); // PUT initial state. let put_ids = a .contract_put(code.clone(), ¶ms, br#"["alpha"]"#.to_vec(), false) .await .expect("put"); assert_eq!(put_ids.instance, ids.instance); // GET it back. let state = a.contract_get(&ids.instance, false).await.expect("get"); assert_eq!(state, br#"["alpha"]"#); // SUBSCRIBE, then watch for the update notification through the // adapter's bounded event bus. let mut events = a.subscribe(); assert!(a.contract_subscribe(&ids.instance).await.expect("subscribe")); // UPDATE via the primed path (exercises get_then_update's shape even // though this node already holds the contract). let summary = a .get_then_update(code.clone(), ¶ms, br#"["beta"]"#.to_vec()) .await .expect("update"); assert!(!summary.is_empty(), "update summary should carry bytes"); // The merged state must be the union. let state = a.contract_get(&ids.instance, false).await.expect("get2"); assert_eq!(state, br#"["alpha","beta"]"#); // The notification should have crossed the event bus (ids only). let deadline = tokio::time::Instant::now() + Duration::from_secs(10); let mut saw_update = false; while tokio::time::Instant::now() < deadline { match tokio::time::timeout(Duration::from_secs(1), events.recv()).await { Ok(Ok(AdapterEvent::ContractUpdate { instance, .. })) if instance == ids.instance => { saw_update = true; break; } Ok(Ok(_)) => continue, Ok(Err(_)) => break, Err(_) => continue, } } assert!(saw_update, "no ContractUpdate event reached the bus"); // Kill switch mid-life: immediate, and ops refuse afterwards. a.disable(); assert_eq!(a.status(), AdapterStatus::Disabled); let err = a.contract_get(&ids.instance, false).await; assert!(matches!(err, Err(freenet_adapter::AdapterError::Disabled))); }