daimon 0.19.0

A Rust-native AI agent framework
---
alwaysApply: true
---

# Test Coverage Requirements

Daimon targets ~90% unit test coverage. Every module, trait implementation, and public API must have thorough tests.

## Coverage Targets

| Metric | Target | Minimum (CI gate) |
|--------|--------|--------------------|
| Line Coverage | 90% | 85% |
| Branch Coverage | 85% | 80% |
| Function Coverage | 95% | 90% |

PRs that drop coverage below the minimum gate MUST NOT be merged.

## Measuring Coverage

### cargo-tarpaulin

```bash
cargo install cargo-tarpaulin

# Quick coverage report
cargo tarpaulin --out Stdout

# HTML report for local review
cargo tarpaulin --out Html --output-dir coverage

# CI-friendly Lcov output
cargo tarpaulin --ignore-tests --out Lcov --output-dir coverage
```

### cargo-llvm-cov (alternative)

```bash
cargo install cargo-llvm-cov

cargo llvm-cov --html
cargo llvm-cov --lcov --output-path coverage/lcov.info
```

## Test Organization

### Unit Tests: Co-located with Source

Every source file with logic MUST have a `#[cfg(test)]` module at the bottom:

```rust
// src/tool/registry.rs

pub struct ToolRegistry { /* ... */ }

impl ToolRegistry {
    pub fn register(&mut self, tool: Arc<dyn Tool>) -> Result<()> { /* ... */ }
    pub fn get(&self, name: &str) -> Option<&Arc<dyn Tool>> { /* ... */ }
    pub fn list(&self) -> Vec<&str> { /* ... */ }
}

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

    fn mock_tool(name: &str) -> Arc<dyn Tool> {
        // test helper
    }

    #[test]
    fn register_adds_tool_to_registry() {
        let mut registry = ToolRegistry::new();
        let tool = mock_tool("calculator");
        registry.register(tool).unwrap();
        assert!(registry.get("calculator").is_some());
    }

    #[test]
    fn register_duplicate_name_returns_error() {
        let mut registry = ToolRegistry::new();
        registry.register(mock_tool("calc")).unwrap();
        let result = registry.register(mock_tool("calc"));
        assert!(result.is_err());
    }

    #[test]
    fn get_nonexistent_returns_none() {
        let registry = ToolRegistry::new();
        assert!(registry.get("nonexistent").is_none());
    }

    #[test]
    fn list_returns_all_registered_names() {
        let mut registry = ToolRegistry::new();
        registry.register(mock_tool("a")).unwrap();
        registry.register(mock_tool("b")).unwrap();
        let names = registry.list();
        assert_eq!(names.len(), 2);
        assert!(names.contains(&"a"));
        assert!(names.contains(&"b"));
    }
}
```

### Integration Tests: `tests/` Directory

For testing cross-module behavior:

```
tests/
├── agent_loop.rs        # Full agent loop integration
├── tool_execution.rs    # Tool registration + execution
├── graph_execution.rs   # Graph orchestration end-to-end
└── streaming.rs         # Stream response integration
```

### Async Tests

Use `#[tokio::test]` for async test functions:

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

    #[tokio::test]
    async fn agent_completes_simple_task() {
        let agent = Agent::builder()
            .model(MockModel::new())
            .build()
            .unwrap();
        let response = agent.run("Hello").await.unwrap();
        assert!(!response.text().is_empty());
    }
}
```

## What to Test

### Must Test (coverage-critical)

- [ ] All public functions and methods
- [ ] All trait implementations
- [ ] Error paths and error variants
- [ ] Edge cases: empty input, max values, None/null
- [ ] Builder validation (missing required fields, invalid config)
- [ ] Async behavior: cancellation, timeout, concurrent access

### Should Test

- [ ] Internal helper functions with non-trivial logic
- [ ] State transitions in agent loops and graph execution
- [ ] Serialization/deserialization roundtrips
- [ ] Feature-flag-gated code paths

### May Skip (exclude from coverage)

- Trivial `Debug`/`Display` implementations
- Simple getters that return a field reference
- Generated code (proc-macro output)
- Platform-specific code untestable in CI

Mark exclusions explicitly:

```rust
#[cfg(not(tarpaulin_include))]
impl std::fmt::Display for DaimonError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // ...
    }
}
```

## Test Naming Convention

```
test_<unit>_<scenario>_<expected_result>
```

Examples:

```rust
#[test]
fn test_registry_register_duplicate_returns_error() { }

#[tokio::test]
async fn test_agent_loop_max_iterations_stops_execution() { }

#[test]
fn test_tool_schema_validates_required_fields() { }

#[tokio::test]
async fn test_stream_cancellation_cleans_up_resources() { }
```

## Test Patterns

### Mock Implementations for Traits

Create mock implementations in a shared test utilities module:

```rust
// src/testing.rs (behind #[cfg(test)] or a `testing` feature flag)

pub struct MockModel {
    responses: Vec<ChatResponse>,
}

impl MockModel {
    pub fn new(responses: Vec<ChatResponse>) -> Self {
        Self { responses }
    }

    pub fn single(response: ChatResponse) -> Self {
        Self { responses: vec![response] }
    }
}

impl Model for MockModel {
    async fn generate(&self, _request: &ChatRequest) -> Result<ChatResponse> {
        // Return responses in sequence
    }
}
```

### Error Path Testing

Every `Result`-returning function needs both success and failure tests:

```rust
#[test]
fn test_parse_valid_input_succeeds() {
    let result = parse_tool_call(r#"{"name": "calc", "args": {}}"#);
    assert!(result.is_ok());
}

#[test]
fn test_parse_invalid_json_returns_error() {
    let result = parse_tool_call("not json");
    assert!(matches!(result, Err(DaimonError::Serialization(_))));
}

#[test]
fn test_parse_missing_name_returns_error() {
    let result = parse_tool_call(r#"{"args": {}}"#);
    assert!(matches!(result, Err(DaimonError::ToolExecution(_))));
}
```

### Property-Based Testing

Use `proptest` for input-space exploration:

```rust
use proptest::prelude::*;

proptest! {
    #[test]
    fn tool_name_roundtrips_through_serialization(name in "[a-z][a-z0-9_]{0,63}") {
        let tool = DynamicTool::new(&name, "desc", schema);
        let serialized = serde_json::to_string(&tool).unwrap();
        let deserialized: DynamicTool = serde_json::from_str(&serialized).unwrap();
        prop_assert_eq!(tool.name(), deserialized.name());
    }
}
```

### Boundary/Stress Testing

```rust
#[tokio::test]
async fn test_agent_loop_respects_max_iterations() {
    let model = MockModel::always_calls_tool();
    let agent = Agent::builder()
        .model(model)
        .tool(noop_tool())
        .max_iterations(3)
        .build()
        .unwrap();

    let result = agent.run("loop forever").await;
    // Agent should stop after max_iterations, not hang
    assert!(result.is_ok());
}

#[test]
fn test_registry_handles_1000_tools() {
    let mut registry = ToolRegistry::new();
    for i in 0..1000 {
        registry.register(mock_tool(&format!("tool_{}", i))).unwrap();
    }
    assert_eq!(registry.list().len(), 1000);
}
```

## CI Integration

### Coverage in CI Pipeline

```yaml
- name: Run tests with coverage
  run: cargo tarpaulin --ignore-tests --out Xml --output-dir coverage

- name: Check coverage threshold
  run: |
    COVERAGE=$(cargo tarpaulin --ignore-tests --out Stdout 2>&1 | rg 'coverage' | rg -o '[0-9]+\.[0-9]+')
    echo "Coverage: ${COVERAGE}%"
    if (( $(echo "$COVERAGE < 85" | bc -l) )); then
      echo "::error::Coverage ${COVERAGE}% is below 85% minimum"
      exit 1
    fi
```

### PR Coverage Diff

PRs should show coverage diff. If a PR decreases coverage by more than 2%, it must add tests before merging.

## When Coverage Drops

1. Run `cargo tarpaulin --out Html` and review the report
2. Identify uncovered lines (red in HTML report)
3. Prioritize:
   - Error handling paths first
   - Public API methods second
   - Edge cases third
4. Add targeted tests for uncovered branches
5. If code is unreachable, consider removing it

## Quick Reference

```bash
# Run all tests
cargo test

# Run tests for a specific module
cargo test tool::registry::tests

# Run a single test
cargo test test_registry_register_duplicate

# Run tests with output visible
cargo test -- --nocapture

# Run only ignored/slow tests
cargo test -- --ignored

# Coverage report
cargo tarpaulin --out Html --output-dir coverage

# Open coverage report
xdg-open coverage/tarpaulin-report.html
```