Skip to main content

Testing Guide

ClawDesk maintains 610 passing tests across the workspace. This guide covers the test pyramid, conventions, and how to contribute well-tested code.

Test Pyramid

         ╱╲
╱ E2E ╲ ~30 tests — Full gateway integration
╱────────╲
╱Integration╲ ~180 tests — Cross-crate interactions
╱──────────────╲
╱ Unit Tests ╲ ~400 tests — Single module isolation
╱────────────────────╲
LevelLocationScopeSpeed
Unitsrc/*.rs (inline #[cfg(test)])Single function/module<1ms
Integrationtests/*.rsCross-module, crate-level<100ms
E2Etests/e2e/ in gatewayFull HTTP request cycle<5s

Running Tests

# All tests (610 total)
cargo test --workspace

# Single crate
cargo test -p clawdesk-domain

# Single test by name
cargo test -p clawdesk-agents test_pipeline_context_assembly

# With output
cargo test --workspace -- --nocapture

# Only unit tests (exclude integration)
cargo test --workspace --lib

# Only integration tests
cargo test --workspace --test '*'

Filtering

# Run all tests matching a pattern
cargo test --workspace fallback

# Run tests in a specific module
cargo test -p clawdesk-domain routing::tests

Writing Unit Tests

Place unit tests in the same file as the code under test:

pub fn calculate_token_budget(max_tokens: usize, reserved: usize) -> usize {
max_tokens.saturating_sub(reserved)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn token_budget_normal() {
assert_eq!(calculate_token_budget(4096, 512), 3584);
}

#[test]
fn token_budget_overflow_protection() {
assert_eq!(calculate_token_budget(100, 200), 0);
}
}

Async Tests

Use #[tokio::test] for async tests:

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn session_store_roundtrip() {
let store = InMemorySessionStore::new();
let session = Session::new("test-channel");

store.save(&session).await.unwrap();
let loaded = store.get(&session.id).await.unwrap();

assert_eq!(loaded.unwrap().id, session.id);
}
}

Writing Integration Tests

Place cross-module tests in tests/ at the crate root:

// tests/pipeline_integration.rs

use clawdesk_agents::{Pipeline, PipelineConfig};
use clawdesk_types::{Message, Session};

#[tokio::test]
async fn full_pipeline_processes_message() {
let config = PipelineConfig::default();
let pipeline = Pipeline::new(config);
let session = Session::new("test");
let message = Message::user("Hello, world!");

let result = pipeline.process(&session, &message).await;

assert!(result.is_ok());
assert!(!result.unwrap().content.is_empty());
}

Mocking

Use trait-based mocking for dependencies. Create mock implementations in test modules:

#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;

struct MockSessionStore {
sessions: std::sync::Mutex<HashMap<SessionId, Session>>,
}

#[async_trait]
impl SessionStore for MockSessionStore {
async fn get(&self, id: &SessionId) -> Result<Option<Session>, StorageError> {
Ok(self.sessions.lock().unwrap().get(id).cloned())
}

async fn save(&self, session: &Session) -> Result<(), StorageError> {
self.sessions.lock().unwrap().insert(session.id.clone(), session.clone());
Ok(())
}
}

#[tokio::test]
async fn agent_uses_session_store() {
let store = Arc::new(MockSessionStore {
sessions: std::sync::Mutex::new(HashMap::new()),
});
// ... test with mock
}
}
tip

Prefer creating simple mock structs over using mocking frameworks. ClawDesk's trait-based architecture makes manual mocking straightforward.

Test Fixtures

For reusable test data, create helper functions:

#[cfg(test)]
pub(crate) mod fixtures {
use clawdesk_types::*;

pub fn sample_message() -> Message {
Message {
id: MessageId::new(),
role: Role::User,
content: "Test message".into(),
timestamp: chrono::Utc::now(),
..Default::default()
}
}

pub fn sample_session() -> Session {
Session {
id: SessionId::new(),
channel_id: ChannelId::from("test-channel"),
..Default::default()
}
}
}

E2E Tests

End-to-end tests spin up a real gateway and make HTTP requests:

// crates/clawdesk-gateway/tests/e2e/health.rs

use reqwest::StatusCode;

#[tokio::test]
async fn health_endpoint_returns_ok() {
let addr = spawn_test_server().await;

let resp = reqwest::get(format!("http://{addr}/api/v1/health"))
.await
.unwrap();

assert_eq!(resp.status(), StatusCode::OK);
}

Coverage

Generate coverage reports with cargo-llvm-cov:

# Install
cargo install cargo-llvm-cov

# Generate HTML report
cargo llvm-cov --workspace --html

# Open the report
open target/llvm-cov/html/index.html
info

There is no hard coverage threshold, but aim for >80% on domain and agent crates. Infrastructure adapters may have lower coverage when they depend on external services.

Test Conventions

  1. Test names describe the scenario: test_fallback_triggers_on_rate_limit
  2. One assertion per logical concept — multiple assert! calls are fine if testing one behavior
  3. No sleeping — use tokio::time::pause() for time-dependent tests
  4. Deterministic — tests must not depend on ordering or external state
  5. Fast — individual tests should complete in under 100ms

CI Integration

All tests run in CI on every pull request:

- cargo fmt --check
- cargo clippy --workspace -- -D warnings
- cargo test --workspace

A PR cannot be merged unless all 610+ tests pass.