docs/core/testing.mdx
Testing in Spacedrive Core ensures reliability across single-device operations and multi-device networking scenarios. This guide covers the available frameworks, patterns, and best practices.
Spacedrive Core provides two primary testing approaches:
Tests live in two locations:
core/tests/ - Integration tests that verify complete workflowscore/src/testing/ - Test framework utilities and helpersFor single-device tests, use Tokio's async test framework:
#[tokio::test]
async fn test_library_creation() {
let setup = IntegrationTestSetup::new("library_test").await.unwrap();
let core = setup.create_core().await.unwrap();
let library = core.libraries
.create_library("Test Library", None)
.await
.unwrap();
assert!(!library.id.is_empty());
}
The IntegrationTestSetup utility provides isolated test environments:
// Basic setup
let setup = IntegrationTestSetup::new("test_name").await?;
// Custom configuration
let setup = IntegrationTestSetup::with_config("test_name", |builder| {
builder
.log_level("debug")
.networking_enabled(true)
.volume_monitoring_enabled(false)
}).await?;
Key features:
test_data/{test_name}/library/logs/Spacedrive provides two approaches for testing multi-device scenarios:
Use CargoTestRunner subprocess framework when:
Examples: Device pairing, network discovery, connection management
// Uses real networking, separate processes
let mut runner = CargoTestRunner::new()
.add_subprocess("alice", "alice_pairing_scenario")
.add_subprocess("bob", "bob_pairing_scenario");
Use custom harness with mock transport when:
Examples: Real-time sync, backfill, content identity linking, conflict resolution
// Uses mock transport, single process, fast and deterministic
let harness = TwoDeviceHarnessBuilder::new("sync_test")
.collect_events(true)
.build()
.await?;
| Aspect | Subprocess Framework | Custom Harness |
|---|---|---|
| Speed | Slower (real networking) | Fast (in-memory) |
| Networking | Real (discovery, NAT) | Mock transport |
| Isolation | True process isolation | Shared process |
| Debugging | Harder (multiple processes) | Easier (single process) |
| Determinism | Network timing varies | Fully deterministic |
| Use Case | Network features | Sync/data logic |
The subprocess framework spawns separate cargo test processes for each device role:
let mut runner = CargoTestRunner::new()
.with_timeout(Duration::from_secs(90))
.add_subprocess("alice", "alice_scenario")
.add_subprocess("bob", "bob_scenario");
runner.run_until_success(|outputs| {
outputs.values().all(|output| output.contains("SUCCESS"))
}).await?;
Create separate test functions for each device role:
#[tokio::test]
async fn test_device_pairing() {
let mut runner = CargoTestRunner::new()
.add_subprocess("alice", "alice_pairing")
.add_subprocess("bob", "bob_pairing");
runner.run_until_success(|outputs| {
outputs.values().all(|o| o.contains("PAIRING_SUCCESS"))
}).await.unwrap();
}
#[tokio::test]
#[ignore]
async fn alice_pairing() {
if env::var("TEST_ROLE").unwrap_or_default() != "alice" {
return;
}
let data_dir = PathBuf::from(env::var("TEST_DATA_DIR").unwrap());
let core = create_test_core(data_dir).await.unwrap();
// Alice initiates pairing
let (code, _) = core.start_pairing_as_initiator().await.unwrap();
fs::write("/tmp/pairing_code.txt", &code).unwrap();
// Wait for connection
wait_for_connection(&core).await;
println!("PAIRING_SUCCESS");
}
Processes coordinate through:
TEST_ROLE and TEST_DATA_DIRWhen testing filesystem watcher functionality, several critical setup steps are required:
The default TestConfigBuilder disables the filesystem watcher (for performance in sync tests). Tests that verify watcher events must explicitly enable it:
let mut config = TestConfigBuilder::new(test_root.clone())
.build()?;
// CRITICAL: Enable watcher for change detection tests
config.services.fs_watcher_enabled = true;
config.save()?;
let core = Core::new(config.data_dir.clone()).await?;
macOS temp directories (/var/folders/...) don't reliably deliver filesystem events. Use home directory paths instead:
// ❌ Don't use TempDir for watcher tests
let temp_dir = TempDir::new()?;
// ✅ Use home directory
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
let test_root = PathBuf::from(home).join(".spacedrive_test_my_test");
// Clean up before
let _ = tokio::fs::remove_dir_all(&test_root).await;
tokio::fs::create_dir_all(&test_root).await?;
// ... run test ...
// Clean up after
tokio::fs::remove_dir_all(&test_root).await?;
Ephemeral paths must be indexed before watching:
// 1. Index the directory (ephemeral mode)
let config = IndexerJobConfig::ephemeral_browse(
SdPath::local(dest_dir.clone()),
IndexScope::Current
);
let job = IndexerJob::new(config);
library.jobs().dispatch(job).await?.wait().await?;
// 2. Mark indexing complete (indexer job does this automatically)
context.ephemeral_cache().mark_indexing_complete(&dest_dir);
// 3. Register for watching (indexer job does this automatically)
watcher.watch_ephemeral(dest_dir.clone()).await?;
// Now filesystem events will be detected
For persistent locations, the watcher auto-loads locations at startup. New locations created during tests must be manually registered:
// After creating and indexing a location
let location_meta = LocationMeta {
id: location_uuid,
library_id: library.id(),
root_path: location_path.clone(),
rule_toggles: RuleToggles::default(),
};
watcher.watch_location(location_meta).await?;
The IndexingHarness handles this automatically.
Start collecting events after initialization to avoid library statistics noise:
// Complete all setup first
let harness = IndexingHarnessBuilder::new("test").build().await?;
let location = harness.add_and_index_location(...).await?;
// Wait for setup to settle
tokio::time::sleep(Duration::from_millis(500)).await;
// Start collecting BEFORE the operation you're testing
let mut collector = EventCollector::new(&harness.core.events);
let handle = tokio::spawn(async move {
collector.collect_events(Duration::from_secs(5)).await;
collector
});
// Perform operation
perform_copy_operation().await?;
// Collect and verify
let collector = handle.await.unwrap();
let stats = collector.analyze().await;
assert!(stats.resource_changed.get("file").copied().unwrap_or(0) >= 2);
The EventCollector automatically filters out:
LibraryStatisticsUpdated)Different handlers emit different event types:
ResourceChanged events per file (CREATE + MODIFY)ResourceChangedBatch events// Ephemeral assertion
let file_events = stats.resource_changed.get("file").copied().unwrap_or(0);
assert!(file_events >= 2, "Expected file ResourceChanged events");
// Persistent assertion
let batch_count = stats.resource_changed_batch.get("file").copied().unwrap_or(0);
assert!(batch_count >= 2, "Expected file ResourceChangedBatch events");
Wait for specific Core events with timeouts:
let mut events = core.events.subscribe();
let event = wait_for_event(
&mut events,
|e| matches!(e, Event::JobCompleted { .. }),
Duration::from_secs(30)
).await?;
For tests that need to verify event emission patterns (e.g., ResourceChanged events during operations), use the shared EventCollector helper:
use helpers::EventCollector;
// Create collector with full event capture for debugging
let mut collector = EventCollector::with_capture(&harness.core.events);
// Spawn collection task
let collection_handle = tokio::spawn(async move {
collector.collect_events(Duration::from_secs(10)).await;
collector
});
// Perform operations that emit events
perform_copy_operation().await?;
location.reindex().await?;
// Retrieve collector and analyze
let collector = collection_handle.await.unwrap();
// Print statistics summary
let stats = collector.analyze().await;
stats.print();
// Print full event details for debugging (when using with_capture)
collector.print_events().await;
// Write events to JSON file for later inspection
collector.write_to_file(&snapshot_dir.join("events.json")).await?;
// Filter specific events
let file_events = collector.get_resource_batch_events("file").await;
let indexing_events = collector.get_events_by_type("IndexingCompleted").await;
The EventCollector tracks:
Statistics Output:
Event Statistics:
==================
ResourceChangedBatch events:
file → 45 resources
Indexing events:
Started: 1
Completed: 1
Entry events:
Created: 3
Modified: 0
Job events:
Started:
indexer → 1
Completed:
indexer → 1
Detailed Event Output (with with_capture()):
=== Collected Events (8) ===
[1] IndexingStarted
Location: 550e8400-e29b-41d4-a716-446655440000
[2] JobStarted
Job: indexer (job_123)
[3] ResourceChangedBatch
Type: file
Resources: 45 items
Paths: 1 affected
[4] IndexingCompleted
Location: 550e8400-e29b-41d4-a716-446655440000
Files: 42, Dirs: 3
[5] JobCompleted
Job: indexer (job_123)
Output: Success
Use Cases:
Query the database directly to verify state:
use sd_core::entities;
let entries = entities::entry::Entity::find()
.filter(entities::entry::Column::Name.contains("test"))
.all(db.conn())
.await?;
assert_eq!(entries.len(), expected_count);
Test job execution and resumption:
// Start a job
let job_id = core.jobs.dispatch(IndexingJob::new(...)).await?;
// Monitor progress
wait_for_event(&mut events, |e| matches!(
e,
Event::JobProgress { id, .. } if *id == job_id
), timeout).await?;
// Verify completion
let job = core.jobs.get_job(job_id).await?;
assert_eq!(job.status, JobStatus::Completed);
Test synchronization without real networking:
let transport = Arc::new(Mutex::new(Vec::new()));
let mut core_a = create_test_core().await?;
let mut core_b = create_test_core().await?;
// Connect cores with mock transport
connect_with_mock_transport(&mut core_a, &mut core_b, transport).await?;
// Verify sync
perform_operation_on_a(&core_a).await?;
wait_for_sync(&core_b).await?;
All test data MUST be created in the system temp directory. Never persist data outside temp unless using the snapshot flag.
Naming convention: spacedrive-test-{test_name}
// ✅ CORRECT: Platform-aware temp directory
let test_data = TestDataDir::new("file_operations")?;
// Creates: /tmp/spacedrive-test-file_operations/ (Unix)
// or: %TEMP%\spacedrive-test-file_operations\ (Windows)
// ❌ INCORRECT: Hardcoded paths outside temp
let test_dir = PathBuf::from("~/Library/Application Support/spacedrive/tests");
let test_dir = PathBuf::from("core/data/test");
Standard structure:
/tmp/spacedrive-test-{test_name}/
├── core_data/ # Core database and state
├── locations/ # Test file locations
└── logs/ # Test execution logs
Cleanup: Temp directories are automatically cleaned up after test completion using RAII pattern.
Snapshots preserve test state for post-mortem debugging. They are optional and controlled by an environment variable.
Enable snapshots:
# Single test
SD_TEST_SNAPSHOTS=1 cargo test file_move_test --nocapture
# Entire suite
SD_TEST_SNAPSHOTS=1 cargo xtask test-core
Snapshot location (when enabled):
~/Library/Application Support/spacedrive/test_snapshots/ (macOS)
~/.local/share/spacedrive/test_snapshots/ (Linux)
%APPDATA%\spacedrive\test_snapshots\ (Windows)
Structure:
test_snapshots/
└── {test_name}/
└── {timestamp}/
├── summary.md # Test metadata and statistics
├── core_data/ # Database copies
│ ├── database.db
│ └── sync.db
├── events.json # Event bus events (JSON lines)
└── logs/ # Test execution logs
When to use snapshots:
Not needed for:
Some tests generate fixtures used by other test suites (e.g., TypeScript tests consuming Rust-generated event data). These fixtures follow the same conventions as snapshots: always write to temp, only copy to source when explicitly requested.
Generate fixtures:
# Single fixture test
SD_REGENERATE_FIXTURES=1 cargo test normalized_cache_fixtures_test --nocapture
Fixture location (when enabled):
packages/ts-client/src/__fixtures/backend_events.json (TypeScript test fixtures)
Default behavior:
When SD_REGENERATE_FIXTURES=1 is set:
Example fixture test:
#[tokio::test]
async fn generate_typescript_fixtures() -> Result<()> {
let temp_dir = TempDir::new()?;
// Generate fixture data
let fixture_data = generate_real_backend_events().await?;
// Always write to temp
let temp_fixture_path = temp_dir.path().join("backend_events.json");
std::fs::write(&temp_fixture_path, serde_json::to_string_pretty(&fixture_data)?)?;
// Only copy to source if explicitly requested
if std::env::var("SD_REGENERATE_FIXTURES").is_ok() {
let source_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent().unwrap()
.join("packages/ts-client/src/__fixtures__/backend_events.json");
std::fs::copy(&temp_fixture_path, &source_path)?;
println!("Fixtures copied to source: {}", source_path.display());
}
Ok(())
}
When to regenerate fixtures:
TestDataDir - Manages test data directories with automatic cleanup and snapshot support:
#[tokio::test]
async fn test_file_operations() -> Result<()> {
let test_data = TestDataDir::new("file_operations")?;
let core = Core::new(test_data.core_data_path()).await?;
// Perform test operations...
// Optional: capture snapshot at specific point
if let Some(manager) = test_data.snapshot_manager() {
manager.capture("after_indexing").await?;
}
// Automatic cleanup and final snapshot (if enabled) on drop
Ok(())
}
SnapshotManager - Captures test snapshots (accessed via TestDataDir):
// Multi-phase snapshot capture
if let Some(manager) = test_data.snapshot_manager() {
manager.capture("after_setup").await?;
manager.capture("after_sync").await?;
manager.capture("final_state").await?;
}
Integration with existing harnesses:
// IndexingHarness uses TestDataDir internally
let harness = IndexingHarnessBuilder::new("my_test").build().await?;
// Access snapshot manager through harness
if let Some(manager) = harness.snapshot_manager() {
manager.capture("after_indexing").await?;
}
// TwoDeviceHarness has built-in snapshot method
harness.capture_snapshot("after_sync").await?;
The framework provides comprehensive test helpers in core/tests/helpers/:
Event Collection:
EventCollector - Collect and analyze all events from the event busEventStats - Statistics about collected events with formatted outputIndexing Tests:
IndexingHarnessBuilder - Create isolated test environments with indexing supportTestLocation - Builder for test locations with filesLocationHandle - Handle to indexed locations with verification methodsSync Tests:
TwoDeviceHarnessBuilder - Pre-configured two-device sync test environmentsMockTransport - Mock network transport for deterministic sync testingwait_for_sync() - Sophisticated sync completion detectionTestConfigBuilder - Custom test configurationsDatabase & Jobs:
wait_for_event() - Wait for specific events with timeoutwait_for_indexing() - Wait for indexing job completionregister_device() - Register a device in a libraryFor volume-related tests, use the test volume utilities:
use helpers::test_volumes;
let volume = test_volumes::create_test_volume().await?;
// Test volume operations
test_volumes::cleanup_test_volume(volume).await?;
Spacedrive maintains a curated suite of core integration tests that run in CI and during local development. These tests are defined in a single source of truth using the xtask pattern.
The cargo xtask test-core command runs all core integration tests with progress tracking:
# Run all core tests (minimal output)
cargo xtask test-core
# Run with full test output
cargo xtask test-core --verbose
Example output:
════════════════════════════════════════════════════════════════
Spacedrive Core Tests Runner
Running 13 test suite(s)
════════════════════════════════════════════════════════════════
[1/13] Running: Library tests
────────────────────────────────────────────────────────────────
✓ PASSED (2s)
[2/13] Running: Indexing test
────────────────────────────────────────────────────────────────
✓ PASSED (15s)
...
════════════════════════════════════════════════════════════════
Test Results Summary
════════════════════════════════════════════════════════════════
Total time: 7m 24s
✓ Passed (11/13):
✓ Library tests
✓ Indexing test
...
✗ Failed (2/13):
✗ Sync realtime test
✗ File sync test
All core integration tests are defined in xtask/src/test_core.rs in the CORE_TESTS constant:
pub const CORE_TESTS: &[TestSuite] = &[
TestSuite {
name: "Library tests",
args: &["test", "-p", "sd-core", "--lib", "--", "--test-threads=1"],
},
TestSuite {
name: "Indexing test",
args: &["test", "-p", "sd-core", "--test", "indexing_test", "--", "--test-threads=1"],
},
// ... more tests
];
Benefits:
The GitHub Actions workflow runs the core test suite on all platforms:
# .github/workflows/core_tests.yml
- name: Run all tests
run: cargo xtask test-core --verbose
Tests run in parallel on:
With fail-fast: false, all platforms complete even if one fails.
Core integration tests use the Spacedrive source code itself as test data instead of user directories. This ensures:
// Tests index the Spacedrive project root
let test_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_path_buf();
let location = harness
.add_and_index_location(test_path.to_str().unwrap(), "spacedrive")
.await?;
Tests that need multiple locations use different subdirectories:
let project_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_path_buf();
let core_path = project_root.join("core");
let apps_path = project_root.join("apps");
To add a new test to the core suite:
core/tests/your_test.rsCORE_TESTS in xtask/src/test_core.rs:pub const CORE_TESTS: &[TestSuite] = &[
// ... existing tests
TestSuite {
name: "Your new test",
args: &[
"test",
"-p",
"sd-core",
"--test",
"your_test",
"--",
"--test-threads=1",
"--nocapture",
],
},
];
The test will automatically:
cargo xtask test-core outputcargo test --workspace
# Run curated core test suite
cargo xtask test-core
# With full output
cargo xtask test-core --verbose
cargo test test_device_pairing --nocapture
# Run individual scenario
TEST_ROLE=alice TEST_DATA_DIR=/tmp/test cargo test alice_scenario -- --ignored --nocapture
RUST_LOG=debug cargo test test_name --nocapture
test_cross_device_file_transfer over test_transferTestDataDir or TempDir (see Test Data & Snapshot Conventions)env!("CARGO_MANIFEST_DIR") to locate the Spacedrive repo for test indexing$HOME/Desktop or $HOME/Downloadscore/, apps/, etc. when testing multi-location scenarios// ✅ Good: Platform-aware temp directory for test data
let test_data = TestDataDir::new("my_test")?;
// ✅ Good: Uses project source code for deterministic indexing
let test_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_path_buf();
// ❌ Bad: Data outside temp directory
let test_dir = PathBuf::from("core/data/test");
// ❌ Bad: Uses user directory (non-deterministic)
let desktop_path = std::env::var("HOME").unwrap() + "/Desktop";
#[ignore] on scenario functionsCommon debugging approaches:
--nocapture to see all outputtest_data/{test_name}/library/job_logs/RUST_LOG=trace for maximum verbositycargo test default parallelism#[tokio::test]TestDataDir or harness for test data (never hardcode paths outside temp)CargoTestRunner#[ignore]When adding a test to the core suite (cargo xtask test-core):
--test-threads=1 if accessing shared resourcesxtask/src/test_core.rsSpacedrive provides a bridge infrastructure for running TypeScript tests against a real Rust daemon. This enables true end-to-end testing across the Rust backend and TypeScript frontend, verifying that cache updates, WebSocket events, and React hooks work correctly with real data.
The TypeScript bridge test pattern works as follows:
IndexingHarnessBuilderbun test with specific TypeScript test fileSpacedriveClient.fromTcpSocket()This pattern tests the entire stack: Rust daemon → RPC transport → TypeScript client → React hooks → cache updates.
Create a test in core/tests/ that spawns the daemon and TypeScript test:
#[tokio::test]
async fn test_typescript_cache_updates() -> anyhow::Result<()> {
// Create daemon with RPC server enabled
let harness = IndexingHarnessBuilder::new("typescript_bridge_test")
.enable_daemon() // Start RPC server for TypeScript client
.build()
.await?;
// Create test location with files
let test_location = harness.create_test_location("test_files").await?;
test_location.create_dir("folder_a").await?;
test_location.write_file("folder_a/file1.txt", "Content").await?;
// Index the location
let location = test_location
.index("Test Location", IndexMode::Shallow)
.await?;
// Get daemon socket address
let socket_addr = harness
.daemon_socket_addr()
.expect("Daemon should be enabled")
.to_string();
// Prepare bridge config for TypeScript
let bridge_config = TestBridgeConfig {
socket_addr,
library_id: harness.library.id().to_string(),
location_db_id: location.db_id,
location_path: test_location.path().to_path_buf(),
test_data_path: harness.temp_path().to_path_buf(),
};
// Write config to temp file
let config_path = harness.temp_path().join("bridge_config.json");
tokio::fs::write(&config_path, serde_json::to_string_pretty(&bridge_config)?).await?;
// Spawn TypeScript test
let ts_test_file = "packages/ts-client/tests/integration/mytest.test.ts";
let workspace_root = std::env::current_dir()?.parent().unwrap().to_path_buf();
let output = tokio::process::Command::new("bun")
.arg("test")
.arg(workspace_root.join(ts_test_file))
.env("BRIDGE_CONFIG_PATH", config_path.to_str().unwrap())
.current_dir(&workspace_root)
.output()
.await?;
// Verify TypeScript test passed
if !output.status.success() {
anyhow::bail!("TypeScript test failed: {:?}", output.status.code());
}
harness.shutdown().await?;
Ok(())
}
Create a test in packages/ts-client/tests/integration/:
import { describe, test, expect, beforeAll } from "bun:test";
import { readFile } from "fs/promises";
import { SpacedriveClient } from "../../src/client";
import { renderHook, waitFor } from "@testing-library/react";
import { SpacedriveProvider } from "../../src/hooks/useClient";
import { useNormalizedQuery } from "../../src/hooks/useNormalizedQuery";
interface BridgeConfig {
socket_addr: string;
library_id: string;
location_db_id: number;
location_path: string;
test_data_path: string;
}
let bridgeConfig: BridgeConfig;
let client: SpacedriveClient;
beforeAll(async () => {
// Read bridge config from Rust test
const configPath = process.env.BRIDGE_CONFIG_PATH;
const configJson = await readFile(configPath, "utf-8");
bridgeConfig = JSON.parse(configJson);
// Connect to daemon via TCP socket
client = SpacedriveClient.fromTcpSocket(bridgeConfig.socket_addr);
client.setCurrentLibrary(bridgeConfig.library_id);
});
describe("Cache Update Tests", () => {
test("should update cache when files move", async () => {
const wrapper = ({ children }) =>
React.createElement(SpacedriveProvider, { client }, children);
// Query directory listing with useNormalizedQuery
const { result } = renderHook(
() =>
useNormalizedQuery({
query: "files.directory_listing",
input: { path: { Physical: { path: folderPath } } },
resourceType: "file",
pathScope: { Physical: { path: folderPath } },
debug: true, // Enable debug logging
}),
{ wrapper },
);
// Wait for initial data
await waitFor(() => {
expect(result.current.data).toBeDefined();
});
// Perform file operation
await rename(oldPath, newPath);
// Wait for watcher to detect change (500ms buffer + processing)
await new Promise((resolve) => setTimeout(resolve, 2000));
// Verify cache updated
expect(result.current.data.files).toContainEqual(
expect.objectContaining({ name: "newfile" }),
);
});
});
TypeScript tests connect to the daemon via TCP socket using TcpSocketTransport. This transport is designed for Bun/Node.js environments and enables testing outside the browser.
// Automatic with factory method
const client = SpacedriveClient.fromTcpSocket("127.0.0.1:6969");
// Manual construction
import { TcpSocketTransport } from "@sd/ts-client/transports";
const transport = new TcpSocketTransport("127.0.0.1:6969");
const client = new SpacedriveClient(transport);
The TCP transport:
The primary use case for bridge tests is verifying that useNormalizedQuery cache updates work correctly when the daemon emits ResourceChanged or ResourceChangedBatch events.
Key patterns:
debug: true in useNormalizedQuery optionswaitFor and assertions// Enable debug logging
const { result } = renderHook(
() =>
useNormalizedQuery({
query: "files.directory_listing",
input: {
/* ... */
},
resourceType: "file",
pathScope: {
/* ... */
},
debug: true, // Logs event processing
}),
{ wrapper },
);
// Collect all events for debugging
const allEvents: any[] = [];
const originalCreateSubscription = (client as any).subscriptionManager
.createSubscription;
(client as any).subscriptionManager.createSubscription = function (
filter: any,
callback: any,
) {
const wrappedCallback = (event: any) => {
allEvents.push({ timestamp: new Date().toISOString(), event });
console.log(`Event received:`, JSON.stringify(event, null, 2));
callback(event);
};
return originalCreateSubscription.call(this, filter, wrappedCallback);
};
# Run all TypeScript bridge tests
cargo test --package sd-core --test typescript_bridge_test -- --nocapture
# Run specific bridge test
cargo test test_typescript_use_normalized_query_with_file_moves -- --nocapture
# Run only the TypeScript side (requires manual daemon setup)
cd packages/ts-client
BRIDGE_CONFIG_PATH=/path/to/config.json bun test tests/integration/mytest.test.ts
File moves between folders:
Folder renames:
Bulk operations:
Content-addressed files:
IndexMode::Content to enable content identificationalternate_paths update correctlyCheck Rust logs:
RUST_LOG=debug cargo test typescript_bridge -- --nocapture
Check TypeScript output: The Rust test prints all TypeScript stdout/stderr. Look for:
[TS] prefixed log messages🔔 emojiVerify daemon is running:
# In Rust test output, look for:
Socket address: 127.0.0.1:XXXXX
Library ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Check bridge config:
# The config file is written to test_data directory
cat /tmp/test_data/typescript_bridge_test/bridge_config.json
Common issues:
debug: true to see if events are received.enable_daemon()client.setCurrentLibrary() uses correct ID from configFor complete examples, refer to:
Core Test Infrastructure:
xtask/src/test_core.rs - Single source of truth for all core integration tests.github/workflows/core_tests.yml - CI workflow using xtask test runnerSingle Device Tests:
tests/copy_action_test.rs - Event collection during file operations (persistent + ephemeral)tests/job_resumption_integration_test.rs - Job interruption handlingSubprocess Framework (Real Networking):
tests/device_pairing_test.rs - Device pairing with real network discoveryCustom Harness (Mock Transport):
tests/sync_realtime_test.rs - Real-time sync testing with deterministic transport using Spacedrive source codetests/sync_backfill_test.rs - Backfill sync with deterministic test datatests/sync_backfill_race_test.rs - Race condition testing with concurrent operationstests/file_transfer_test.rs - Cross-device file operationsTypeScript Bridge Tests:
tests/typescript_bridge_test.rs - Rust harness that spawns TypeScript testspackages/ts-client/tests/integration/useNormalizedQuery.test.ts - File move cache updatespackages/ts-client/tests/integration/useNormalizedQuery.folder-rename.test.ts - Folder rename propagationpackages/ts-client/tests/integration/useNormalizedQuery.bulk-moves.test.ts - Bulk operations with content-addressed files