mod common; use std::fs; use std::path::Path; use common::{ask, client, collection, config_for, spawn_relay}; use frxd::config::{Member, save_members}; use frxd::index::LocalIndex; use frxd::message::EXPOSURE_FULL; use frxd::node::{self, Node, NodeHandle, current_period}; use serde_json::Value; fn corpus_dir(root: &Path, name: &str, files: &[(&str, &str)]) -> std::path::PathBuf { let dir = root.join(format!("{name}-docs")); fs::create_dir_all(&dir).unwrap(); for (file, text) in files { fs::write(dir.join(file), text).unwrap(); } dir } async fn start_node( root: &Path, name: &str, relay_url: &str, exposure: &str, files: &[(&str, &str)], ) -> NodeHandle { let docs = corpus_dir(root, name, files); let config = config_for(&root.join(name), name, relay_url); { let index = LocalIndex::open(&config.index_dir()).unwrap(); index .add_collection(&collection("docs", &docs, true, exposure)) .unwrap(); } Node::start(config).await.unwrap() } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn aggregates_count_sent_and_passed_per_member() { let root = tempfile::tempdir().unwrap(); let relay_url = spawn_relay().await; let _bob = start_node( root.path(), "bob", &relay_url, EXPOSURE_FULL, &[("doc.txt", "aggregate rust document")], ) .await; let alice = start_node( root.path(), "alice", &relay_url, EXPOSURE_FULL, &[("mine.txt", "alice rust note")], ) .await; let (_status, _raw, value) = ask(&client(), &alice.addr.to_string(), "rust", 5).await; assert_eq!( value .get("responses") .and_then(Value::as_array) .unwrap() .len(), 1 ); let period = current_period(); let aggregate = node::control_aggregate_request( &format!("http://{}", _bob.addr), &alice.pubkey, &period, Some(700), ) .await .unwrap(); assert_eq!(aggregate.get("sent").and_then(Value::as_u64), Some(1)); assert_eq!(aggregate.get("passed").and_then(Value::as_u64), Some(1)); assert!(aggregate.get("cited").is_none()); assert_eq!( aggregate.get("period").and_then(Value::as_str), Some(period.as_str()) ); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn aggregate_rollup_for_year_sums_months() { let root = tempfile::tempdir().unwrap(); let relay_url = spawn_relay().await; let bob = start_node( root.path(), "bob", &relay_url, EXPOSURE_FULL, &[("doc.txt", "rollup rust document")], ) .await; let alice = start_node( root.path(), "alice", &relay_url, EXPOSURE_FULL, &[("mine.txt", "alice rust note")], ) .await; let (_status, _raw, _value) = ask(&client(), &alice.addr.to_string(), "rust", 5).await; let year = current_period()[0..4].to_string(); let aggregate = node::control_aggregate_request( &format!("http://{}", bob.addr), &alice.pubkey, &year, Some(700), ) .await .unwrap(); assert_eq!(aggregate.get("sent").and_then(Value::as_u64), Some(1)); assert_eq!( aggregate.get("period").and_then(Value::as_str), Some(year.as_str()) ); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn aggregate_floor_rejects_finer_than_month() { let root = tempfile::tempdir().unwrap(); let alice = start_node( root.path(), "alice", &spawn_relay().await, EXPOSURE_FULL, &[("mine.txt", "alice rust note")], ) .await; let http = client(); let response = http .get(format!( "http://{}/v1/local/aggregates?period=2026-09-15", alice.addr )) .send() .await .unwrap(); assert_eq!(response.status(), reqwest::StatusCode::BAD_REQUEST); let local = http .get(format!("http://{}/v1/local/aggregates", alice.addr)) .send() .await .unwrap(); assert!(local.status().is_success()); let config = alice.node.config.clone(); let direct = alice .node .request_aggregate(&config.node.relays[0], "2026-09-15", Some(50)) .await; assert!(direct.is_err()); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn full_exposure_responses_carry_content() { let root = tempfile::tempdir().unwrap(); let relay_url = spawn_relay().await; let bob = start_node( root.path(), "bob", &relay_url, EXPOSURE_FULL, &[("doc.txt", "source test rust content")], ) .await; let alice = start_node( root.path(), "alice", &relay_url, EXPOSURE_FULL, &[("mine.txt", "alice local")], ) .await; save_members( &alice.node.config.members_path(), &[Member { name: "bob".to_string(), pubkey: bob.pubkey.clone(), previous: Vec::new(), }], ) .unwrap(); let (_status, _raw, value) = ask(&client(), &alice.addr.to_string(), "rust", 5).await; let responses = value.get("responses").and_then(Value::as_array).unwrap(); assert_eq!(responses.len(), 1); assert!( responses[0] .get("results") .and_then(Value::as_array) .unwrap()[0] .get("content") .unwrap() .is_string() ); }