Files
spacedrive/core/tests/sync_setup_test.rs
Jamie Pine 966b7fea7a feat(sync): transition device sync to shared resources
- Updated device versioning to use the `updated_at` timestamp for conflict resolution.
- Refactored sync methods to support shared changes with HLC-based conflict resolution.
- Implemented automatic device discovery via shared sync, allowing devices to propagate changes to all members in the library.
- Added migration to transition existing devices to shared sync.
- Enhanced documentation to reflect changes in device ownership and sync methods.
2026-01-22 15:52:42 -08:00

529 lines
14 KiB
Rust

//! Sync setup test using subprocess framework
//!
//! Tests that sync setup works without UNIQUE constraint errors when both devices
//! have the same deterministic default spaces.
use sd_core::testing::CargoTestRunner;
use sd_core::Core;
use std::env;
use std::path::PathBuf;
use std::time::Duration;
use tokio::time::timeout;
/// Alice's sync setup scenario
#[tokio::test]
#[ignore]
async fn alice_sync_setup_scenario() {
if env::var("TEST_ROLE").unwrap_or_default() != "alice" {
return;
}
env::set_var("SPACEDRIVE_TEST_DIR", "/tmp/spacedrive-sync-setup-test");
let data_dir = PathBuf::from("/tmp/spacedrive-sync-setup-test/alice");
println!("Alice: Starting sync setup test");
// Initialize Core
let mut core = timeout(Duration::from_secs(10), Core::new(data_dir))
.await
.unwrap()
.unwrap();
core.device.set_name("Alice Device".to_string()).unwrap();
// Initialize networking
timeout(Duration::from_secs(10), core.init_networking())
.await
.unwrap()
.unwrap();
tokio::time::sleep(Duration::from_secs(2)).await;
println!("Alice: Core initialized");
// Create library
let library = core
.libraries
.create_library("Test Library".to_string(), None, core.context.clone())
.await
.unwrap();
println!("Alice: Library created with ID: {}", library.id());
// Write library ID for Bob
std::fs::write(
"/tmp/spacedrive-sync-setup-test/library_id.txt",
library.id().to_string(),
)
.unwrap();
// Write Alice's device ID for Bob
std::fs::write(
"/tmp/spacedrive-sync-setup-test/alice_device_id.txt",
core.device.device_id().unwrap().to_string(),
)
.unwrap();
// Start pairing
let (pairing_code, _) = if let Some(networking) = core.networking() {
timeout(
Duration::from_secs(15),
networking.start_pairing_as_initiator(false),
)
.await
.unwrap()
.unwrap()
} else {
panic!("Networking not initialized");
};
println!("Alice: Pairing code generated");
std::fs::write(
"/tmp/spacedrive-sync-setup-test/pairing_code.txt",
&pairing_code,
)
.unwrap();
// Wait for pairing
println!("Alice: Waiting for Bob to pair...");
let mut attempts = 0;
while attempts < 45 {
tokio::time::sleep(Duration::from_secs(1)).await;
let connected = core.services.device.get_connected_devices().await.unwrap();
if !connected.is_empty() {
println!("Alice: Pairing successful!");
// Share library with Bob - THIS IS THE CRITICAL TEST
let bob_device_id = connected.first().unwrap().clone();
println!(
"Alice: Sharing library with Bob (device: {})...",
bob_device_id
);
use sd_core::infra::action::CoreAction;
use sd_core::ops::network::sync_setup::{
LibrarySyncAction, LibrarySyncSetupAction, LibrarySyncSetupInput,
};
let input = LibrarySyncSetupInput {
local_device_id: core.device.device_id().unwrap(),
remote_device_id: bob_device_id,
local_library_id: library.id(),
remote_library_id: Some(library.id()),
action: LibrarySyncAction::ShareLocalLibrary {
library_name: "Test Library".to_string(),
},
leader_device_id: core.device.device_id().unwrap(),
};
let action = LibrarySyncSetupAction::from_input(input).unwrap();
let result = action.execute(core.context.clone()).await;
match result {
Ok(_) => {
println!("Alice: ✅ Share library SUCCEEDED!");
std::fs::write(
"/tmp/spacedrive-sync-setup-test/alice_success.txt",
"success",
)
.unwrap();
}
Err(e) => {
println!("Alice: ❌ Share library FAILED: {:?}", e);
std::fs::write(
"/tmp/spacedrive-sync-setup-test/alice_error.txt",
format!("{:?}", e),
)
.unwrap();
panic!("Alice: Share library failed: {:?}", e);
}
}
std::fs::write(
"/tmp/spacedrive-sync-setup-test/alice_paired.txt",
"success",
)
.unwrap();
// Give Bob time to process
tokio::time::sleep(Duration::from_secs(5)).await;
break;
}
attempts += 1;
}
if attempts >= 45 {
panic!("Alice: Pairing timeout");
}
println!("Alice: Test completed");
}
/// Bob's sync setup scenario
#[tokio::test]
#[ignore]
async fn bob_sync_setup_scenario() {
if env::var("TEST_ROLE").unwrap_or_default() != "bob" {
return;
}
env::set_var("SPACEDRIVE_TEST_DIR", "/tmp/spacedrive-sync-setup-test");
let data_dir = PathBuf::from("/tmp/spacedrive-sync-setup-test/bob");
println!("Bob: Starting sync setup test");
// Initialize Core
let mut core = timeout(Duration::from_secs(10), Core::new(data_dir))
.await
.unwrap()
.unwrap();
core.device.set_name("Bob Device".to_string()).unwrap();
// Initialize networking
timeout(Duration::from_secs(10), core.init_networking())
.await
.unwrap()
.unwrap();
tokio::time::sleep(Duration::from_secs(2)).await;
println!("Bob: Core initialized");
// Wait for Alice's library ID
println!("Bob: Waiting for Alice's library ID...");
let library_id = loop {
if let Ok(id) = std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/library_id.txt") {
break id.trim().to_string();
}
tokio::time::sleep(Duration::from_millis(500)).await;
};
println!("Bob: Found library ID: {}", library_id);
// Wait for pairing code
println!("Bob: Waiting for pairing code...");
let pairing_code = loop {
if let Ok(code) =
std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/pairing_code.txt")
{
break code.trim().to_string();
}
tokio::time::sleep(Duration::from_millis(500)).await;
};
// Join pairing
println!("Bob: Joining pairing...");
if let Some(networking) = core.networking() {
timeout(
Duration::from_secs(15),
networking.start_pairing_as_joiner(&pairing_code, false),
)
.await
.unwrap()
.unwrap();
}
// Wait for pairing completion
println!("Bob: Waiting for pairing to complete...");
let mut attempts = 0;
while attempts < 30 {
tokio::time::sleep(Duration::from_secs(1)).await;
let connected = core.services.device.get_connected_devices().await.unwrap();
if !connected.is_empty() {
println!("Bob: Pairing successful!");
// Wait for Alice to share her library (ShareLocalLibrary creates it on Bob's side)
println!("Bob: Waiting for Alice's ShareLocalLibrary to create library...");
let alice_lib_uuid = uuid::Uuid::parse_str(&library_id).unwrap();
let mut lib_wait_attempts = 0;
while lib_wait_attempts < 30 {
tokio::time::sleep(Duration::from_secs(1)).await;
// Check if library was created by Alice's ShareLocalLibrary action
if let Some(lib) = core.libraries.get_library(alice_lib_uuid).await {
println!("Bob: ✅ Library received from Alice! ID: {}", lib.id());
std::fs::write("/tmp/spacedrive-sync-setup-test/bob_success.txt", "success")
.unwrap();
// Verify sync initialized
tokio::time::sleep(Duration::from_secs(2)).await;
break;
}
lib_wait_attempts += 1;
}
if lib_wait_attempts >= 30 {
println!("Bob: ❌ Library was never created - UNIQUE constraint may have failed");
std::fs::write(
"/tmp/spacedrive-sync-setup-test/bob_error.txt",
"Timeout waiting for library from Alice - ShareLocalLibrary may have failed with UNIQUE constraint",
)
.unwrap();
panic!("Bob: Timeout waiting for library");
}
break;
}
attempts += 1;
}
if attempts >= 30 {
panic!("Bob: Pairing timeout");
}
println!("Bob: Test completed");
}
/// Carol's sync setup scenario for three-device test
#[tokio::test]
#[ignore]
async fn carol_three_device_scenario() {
if env::var("TEST_ROLE").unwrap_or_default() != "carol" {
return;
}
env::set_var("SPACEDRIVE_TEST_DIR", "/tmp/spacedrive-three-device-test");
let data_dir = PathBuf::from("/tmp/spacedrive-three-device-test/carol");
println!("Carol: Starting three-device test");
// Initialize Core
let mut core = timeout(Duration::from_secs(10), Core::new(data_dir))
.await
.unwrap()
.unwrap();
core.device.set_name("Carol Device".to_string()).unwrap();
// Initialize networking
timeout(Duration::from_secs(10), core.init_networking())
.await
.unwrap()
.unwrap();
tokio::time::sleep(Duration::from_secs(2)).await;
println!("Carol: Core initialized");
// Wait for Alice's library ID
println!("Carol: Waiting for Alice's library ID...");
let library_id = loop {
if let Ok(id) =
std::fs::read_to_string("/tmp/spacedrive-three-device-test/library_id.txt")
{
break id.trim().to_string();
}
tokio::time::sleep(Duration::from_millis(500)).await;
};
println!("Carol: Found library ID: {}", library_id);
// Wait for pairing code
println!("Carol: Waiting for Alice's second pairing code...");
let pairing_code = loop {
if let Ok(code) =
std::fs::read_to_string("/tmp/spacedrive-three-device-test/pairing_code_carol.txt")
{
break code.trim().to_string();
}
tokio::time::sleep(Duration::from_millis(500)).await;
};
// Join pairing
println!("Carol: Joining pairing...");
if let Some(networking) = core.networking() {
timeout(
Duration::from_secs(15),
networking.start_pairing_as_joiner(&pairing_code, false),
)
.await
.unwrap()
.unwrap();
}
// Wait for pairing completion
println!("Carol: Waiting for pairing to complete...");
let mut attempts = 0;
while attempts < 30 {
tokio::time::sleep(Duration::from_secs(1)).await;
let connected = core.services.device.get_connected_devices().await.unwrap();
if !connected.is_empty() {
println!("Carol: Pairing successful!");
// Wait for Alice's ShareLocalLibrary to create library
println!("Carol: Waiting for library from Alice...");
let alice_lib_uuid = uuid::Uuid::parse_str(&library_id).unwrap();
let mut lib_wait_attempts = 0;
while lib_wait_attempts < 30 {
tokio::time::sleep(Duration::from_secs(1)).await;
if let Some(lib) = core.libraries.get_library(alice_lib_uuid).await {
println!("Carol: ✅ Library received! ID: {}", lib.id());
// Wait a bit for device sync to propagate
tokio::time::sleep(Duration::from_secs(3)).await;
// Check if Bob's device is in the library (via shared sync)
use sd_core::infra::db::entities;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
// Read Bob's device ID
let bob_device_id = if let Ok(id) = std::fs::read_to_string(
"/tmp/spacedrive-three-device-test/bob_device_id.txt",
) {
uuid::Uuid::parse_str(id.trim()).ok()
} else {
None
};
if let Some(bob_id) = bob_device_id {
let bob_device = entities::device::Entity::find()
.filter(entities::device::Column::Uuid.eq(bob_id))
.one(lib.db().conn())
.await
.unwrap();
if bob_device.is_some() {
println!(
"Carol: ✅ Bob's device automatically discovered via shared sync!"
);
std::fs::write(
"/tmp/spacedrive-three-device-test/carol_success.txt",
"success",
)
.unwrap();
} else {
println!("Carol: ❌ Bob's device NOT found in library");
std::fs::write(
"/tmp/spacedrive-three-device-test/carol_error.txt",
"Bob device not found - shared sync failed",
)
.unwrap();
}
} else {
println!("Carol: ⚠️ Could not read Bob's device ID");
}
break;
}
lib_wait_attempts += 1;
}
break;
}
attempts += 1;
}
println!("Carol: Test completed");
}
/// Main test orchestrator
#[tokio::test]
async fn test_sync_setup_no_constraint_error() {
println!("Testing sync setup with deterministic spaces...");
// Clean up
let _ = std::fs::remove_dir_all("/tmp/spacedrive-sync-setup-test");
std::fs::create_dir_all("/tmp/spacedrive-sync-setup-test").unwrap();
let mut runner = CargoTestRunner::for_test_file("sync_setup_test")
.with_timeout(Duration::from_secs(120))
.add_subprocess("alice", "alice_sync_setup_scenario")
.add_subprocess("bob", "bob_sync_setup_scenario");
// Spawn Alice first
println!("Starting Alice...");
runner.spawn_single_process("alice").await.unwrap();
// Wait for Alice to initialize
tokio::time::sleep(Duration::from_secs(8)).await;
// Start Bob
println!("Starting Bob...");
runner.spawn_single_process("bob").await.unwrap();
// Wait for success markers
let result = runner
.wait_for_success(|_| {
let alice_paired =
std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/alice_paired.txt")
.map(|c| c.trim() == "success")
.unwrap_or(false);
let bob_success =
std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/bob_success.txt")
.map(|c| c.trim() == "success")
.unwrap_or(false);
// Check if Bob had an error
if std::path::Path::new("/tmp/spacedrive-sync-setup-test/bob_error.txt").exists() {
let error =
std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/bob_error.txt")
.unwrap();
println!("Bob encountered error: {}", error);
return false;
}
alice_paired && bob_success
})
.await;
match result {
Ok(_) => {
println!("✅ Sync setup test PASSED - no UNIQUE constraint errors!");
}
Err(e) => {
println!("❌ Sync setup test FAILED: {}", e);
// Print error if it exists
if let Ok(error) =
std::fs::read_to_string("/tmp/spacedrive-sync-setup-test/bob_error.txt")
{
println!("Bob's error: {}", error);
}
for (name, output) in runner.get_all_outputs() {
println!("\n{} output:\n{}", name, output);
}
panic!("Sync setup test failed");
}
}
}
/// Three-device discovery test - verify shared resource sync enables automatic device discovery
#[tokio::test]
async fn test_three_device_discovery() {
println!("Testing three-device automatic discovery via shared sync...");
// Clean up
let _ = std::fs::remove_dir_all("/tmp/spacedrive-three-device-test");
std::fs::create_dir_all("/tmp/spacedrive-three-device-test").unwrap();
// This test verifies that:
// 1. Alice pairs with Bob and runs sync setup
// 2. Alice pairs with Carol and runs sync setup
// 3. Bob automatically discovers Carol's device via shared sync
// 4. Carol automatically discovers Bob's device via shared sync
// No direct pairing between Bob and Carol needed!
println!("✅ Three-device discovery test placeholder");
println!("This test requires modifications to alice and bob scenarios");
println!("to add Carol pairing and device ID writing");
// TODO: Implement full three-device test with:
// - Alice pairs with Bob (existing flow)
// - Alice pairs with Carol (new flow)
// - Bob verifies Carol's device in library
// - Carol verifies Bob's device in library
}