Skip to main content

Testing Strategy

Version: 1.0 Status: Canonical Development Guidelines Last Updated: 2025-11-17

This document defines the testing strategy for the AGEniX ecosystem.


Table of Contents​

  1. Philosophy
  2. Test Coverage Requirements
  3. Test Types
  4. Test Organization
  5. Testing Workflow
  6. Continuous Integration
  7. Best Practices

1. Philosophy​

Test-Driven Development (TDD)​

AGEniX follows strict TDD principles:

  1. Write tests first - Before implementing functionality
  2. Implement minimal code - Just enough to pass tests
  3. Refactor - Improve code while maintaining tests
  4. Repeat - For every feature and bug fix

Why TDD?​

  • Correctness: Tests define expected behavior before implementation
  • Safety: Regression protection for security-critical code
  • Documentation: Tests serve as executable specifications
  • Confidence: Refactor fearlessly with comprehensive test coverage
  • Design: Writing tests first leads to better API design

2. Test Coverage Requirements​

Minimum Coverage​

ComponentMinimum CoverageTarget Coverage
All code80%90%
Public APIs90%100%
Security-critical code100%100%
Authentication100%100%
Input validation100%100%
Cryptographic operations100%100%

Security-Critical Code​

The following must have 100% test coverage:

  • Authentication and authorization
  • Session key validation
  • Input sanitization and validation
  • RESP protocol parsing
  • Command injection prevention
  • Path traversal prevention
  • Cryptographic operations
  • Rate limiting

Measuring Coverage​

# Install tarpaulin
cargo install cargo-tarpaulin

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

# View report
open coverage/index.html

# CI: Require minimum coverage
cargo tarpaulin --fail-under 80

3. Test Types​

3.1 Unit Tests​

Purpose: Test individual functions and methods in isolation

Location: Co-located with source code in #[cfg(test)] modules

Example:

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

#[test]
fn test_session_key_validation() {
// Test valid key
let valid_key = "a".repeat(32);
assert!(validate_session_key(&valid_key).is_ok());

// Test invalid keys
assert!(validate_session_key("").is_err());
assert!(validate_session_key("short").is_err());
assert!(validate_session_key("../etc/passwd").is_err());
}

#[test]
fn test_worker_id_format() {
assert!(validate_worker_id("worker-001").is_ok());
assert!(validate_worker_id("worker_test_123").is_ok());

// Invalid characters
assert!(validate_worker_id("worker; rm -rf /").is_err());
assert!(validate_worker_id("worker/../etc").is_err());
}
}

Best Practices:

  • One assertion concept per test
  • Descriptive test names
  • Test both success and failure cases
  • Test edge cases and boundaries

3.2 Integration Tests​

Purpose: Test component interactions and full workflows

Location: tests/ directory at repository root

Example:

// tests/integration_test.rs
use agq::server::Server;
use agq::client::RespClient;

#[tokio::test]
async fn test_full_authentication_flow() {
// Start test server
let server = Server::new("127.0.0.1:0").await.unwrap();
let addr = server.local_addr();

tokio::spawn(server.run());

// Connect client
let mut client = RespClient::connect(addr).await.unwrap();

// Test AUTH command
let response = client.auth("test_session_key_32_bytes_long!!").await;
assert_eq!(response.unwrap(), "OK");

// Test authenticated PING
let pong = client.ping().await.unwrap();
assert_eq!(pong, "PONG");
}

#[tokio::test]
async fn test_job_lifecycle() {
// Test: plan submit → job create → worker pull → execute → complete
let server = setup_test_server().await;

// Submit plan
let plan_id = submit_test_plan(&server).await;

// Create job
let job_id = create_job(&server, plan_id).await;

// Worker pulls job
let job = worker_pull_job(&server).await;
assert_eq!(job.job_id, job_id);

// Execute and report completion
execute_job(&server, &job).await;

// Verify job status
let status = get_job_status(&server, job_id).await;
assert_eq!(status, "completed");
}

Best Practices:

  • Set up and tear down test infrastructure
  • Test realistic workflows
  • Use helper functions to reduce boilerplate
  • Clean up resources (connections, temp files)

3.3 Security Tests​

Purpose: Verify protection against common attack vectors

Location: tests/security/ directory

Example:

// tests/security/injection_tests.rs

#[test]
fn test_command_injection_prevention() {
let malicious_inputs = vec![
"worker; rm -rf /",
"worker`cat /etc/passwd`",
"worker$(whoami)",
"worker|ls",
"worker&& cat /etc/shadow",
];

for input in malicious_inputs {
let result = parse_worker_id(input);
assert!(
result.is_err(),
"Should reject command injection: {}",
input
);
}
}

#[test]
fn test_path_traversal_prevention() {
let malicious_paths = vec![
"../../../etc/passwd",
"..\\..\\..\\windows\\system32",
"file:///etc/passwd",
"/etc/passwd",
"~/.ssh/id_rsa",
];

for path in malicious_paths {
let result = validate_file_path(path);
assert!(result.is_err(), "Should reject path traversal: {}", path);
}
}

#[test]
fn test_session_key_constant_time_comparison() {
use std::time::Instant;

let key1 = "a".repeat(32);
let key2 = "b".repeat(32);
let key3 = "a".repeat(32);

// Measure timing for matching keys
let start = Instant::now();
let _ = compare_session_keys(&key1, &key3);
let match_duration = start.elapsed();

// Measure timing for non-matching keys
let start = Instant::now();
let _ = compare_session_keys(&key1, &key2);
let nomatch_duration = start.elapsed();

// Timing should be similar (within 10%)
let ratio = match_duration.as_nanos() as f64 / nomatch_duration.as_nanos() as f64;
assert!(
(0.9..=1.1).contains(&ratio),
"Timing attack vulnerability: ratio = {}",
ratio
);
}

#[test]
fn test_dos_protection_rate_limiting() {
let mut limiter = RateLimiter::new(5, Duration::from_secs(60));

// First 5 requests should succeed
for i in 0..5 {
assert!(limiter.check("192.168.1.1").is_ok(), "Request {} should succeed", i);
}

// 6th request should fail (rate limited)
assert!(limiter.check("192.168.1.1").is_err(), "Should be rate limited");
}

Best Practices:

  • Test all OWASP Top 10 attack vectors
  • Test boundary conditions
  • Test timing attack resistance
  • Test resource exhaustion scenarios

3.4 Fuzzing Tests​

Purpose: Discover edge cases through randomized input

Location: fuzz/fuzz_targets/ directory

Setup:

cargo install cargo-fuzz
cargo fuzz init

Example:

// fuzz/fuzz_targets/resp_parser.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use agq::protocol::parse_resp_message;

fuzz_target!(|data: &[u8]| {
// Parser should never panic, even with malformed input
let _ = parse_resp_message(data);
});

Running Fuzz Tests:

# Run fuzzer for 60 seconds
cargo +nightly fuzz run resp_parser -- -max_total_time=60

# Run with specific corpus
cargo +nightly fuzz run resp_parser fuzz/corpus/resp_parser

# Check coverage
cargo +nightly fuzz coverage resp_parser

Best Practices:

  • Fuzz all parsers and deserializers
  • Run fuzz tests in CI for at least 60 seconds
  • Maintain corpus of interesting inputs
  • Fix all panics and crashes discovered

3.5 Property-Based Tests​

Purpose: Test properties that should hold for all inputs

Dependencies:

[dev-dependencies]
proptest = "1.0"

Example:

use proptest::prelude::*;

proptest! {
#[test]
fn test_job_id_roundtrip(id in "[a-zA-Z0-9]{8,64}") {
// Property: encoding then decoding should return original
let encoded = encode_job_id(&id);
let decoded = decode_job_id(&encoded).unwrap();
assert_eq!(id, decoded);
}

#[test]
fn test_plan_validation_never_panics(
tasks in proptest::collection::vec(any::<Task>(), 0..100)
) {
// Property: validation should never panic
let plan = Plan { tasks };
let _ = validate_plan(&plan); // Should not panic
}

#[test]
fn test_session_key_validation_consistent(
key in "[a-zA-Z0-9]{32,128}"
) {
// Property: validation should be consistent
let result1 = validate_session_key(&key);
let result2 = validate_session_key(&key);
assert_eq!(result1.is_ok(), result2.is_ok());
}
}

Best Practices:

  • Define invariants as properties
  • Test serialization round-trips
  • Test mathematical properties
  • Use reasonable input generators

3.6 Chaos/Failure Tests​

Purpose: Test resilience to failures and unexpected conditions

Example:

#[tokio::test]
async fn test_network_partition_recovery() {
let server = setup_test_server().await;
let mut worker = Worker::connect(&server).await.unwrap();

// Simulate network partition
server.drop_all_connections().await;

// Worker should detect failure and reconnect
tokio::time::sleep(Duration::from_secs(5)).await;

// Worker should have reconnected
assert!(worker.is_connected().await);
}

#[tokio::test]
async fn test_job_timeout_handling() {
let server = setup_test_server().await;

// Create job with very short timeout
let job = Job {
tasks: vec![Task {
command: "sleep 10".into(),
timeout_secs: 1,
..Default::default()
}],
..Default::default()
};

// Execute job
let result = execute_job(&server, &job).await;

// Should fail with timeout error
assert!(matches!(result, Err(JobError::Timeout)));
}

#[test]
fn test_out_of_memory_handling() {
// Attempt to allocate huge plan
let huge_plan = Plan {
tasks: vec![Task::default(); 1_000_000],
..Default::default()
};

// Should fail gracefully, not OOM
let result = validate_plan(&huge_plan);
assert!(result.is_err());
}

4. Test Organization​

Directory Structure​

component/
├── src/
│ ├── lib.rs
│ ├── auth.rs
│ └── auth/
│ └── tests.rs # Unit tests for auth module
├── tests/
│ ├── integration/
│ │ ├── auth_flow.rs
│ │ ├── job_lifecycle.rs
│ │ └── worker_heartbeat.rs
│ └── security/
│ ├── injection_tests.rs
│ ├── dos_tests.rs
│ └── timing_tests.rs
└── fuzz/
└── fuzz_targets/
├── resp_parser.rs
└── plan_deserializer.rs

Test Modules​

Co-located unit tests:

// src/auth.rs
pub fn validate_session_key(key: &str) -> Result<(), Error> {
// Implementation
}

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

#[test]
fn test_validate_session_key() {
// Tests here
}
}

Separate test modules:

// src/auth/tests.rs (for larger test suites)
use super::*;

#[test]
fn test_complex_auth_scenario() {
// Complex test
}

5. Testing Workflow​

Before Committing​

# 1. Format code
cargo fmt

# 2. Check for warnings
cargo clippy -- -D warnings

# 3. Run all tests
cargo test

# 4. Run security audit
cargo audit

# 5. Check test coverage
cargo tarpaulin --fail-under 80

CI Pipeline​

Tests run automatically on:

  • Every push to PR
  • Every commit to main
  • Nightly (including fuzzing)

CI must pass before merge.


6. Continuous Integration​

GitHub Actions Workflow​

name: Test

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable

- name: Format check
run: cargo fmt -- --check

- name: Clippy
run: cargo clippy -- -D warnings

- name: Test
run: cargo test

- name: Security audit
run: cargo audit

- name: Coverage
run: |
cargo install cargo-tarpaulin
cargo tarpaulin --fail-under 80

fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Install Rust nightly
uses: actions-rs/toolchain@v1
with:
toolchain: nightly

- name: Install cargo-fuzz
run: cargo install cargo-fuzz

- name: Fuzz test
run: |
cargo +nightly fuzz run resp_parser -- -max_total_time=60

7. Best Practices​

Test Naming​

Use descriptive names:

Good:

#[test]
fn test_session_key_rejects_empty_string() { }

#[test]
fn test_worker_heartbeat_updates_alive_timestamp() { }

#[test]
fn test_command_injection_with_semicolon_is_blocked() { }

Bad:

#[test]
fn test1() { }

#[test]
fn test_key() { }

#[test]
fn it_works() { }

Test Independence​

Tests must be independent and order-agnostic:

Good:

#[test]
fn test_a() {
let db = create_test_db(); // Fresh DB per test
// Test A
}

#[test]
fn test_b() {
let db = create_test_db(); // Fresh DB per test
// Test B
}

Bad:

static mut SHARED_STATE: i32 = 0;

#[test]
fn test_a() {
unsafe { SHARED_STATE = 1; } // Tests depend on order
}

#[test]
fn test_b() {
unsafe { assert_eq!(SHARED_STATE, 1); } // Breaks if run alone
}

Test Data​

Use test fixtures and builders:

// tests/fixtures.rs
pub fn test_plan() -> Plan {
Plan {
plan_id: "test-plan-001".into(),
tasks: vec![
Task {
task_number: 1,
command: "echo".into(),
args: vec!["hello".into()],
timeout_secs: 10,
}
],
}
}

pub struct PlanBuilder {
plan: Plan,
}

impl PlanBuilder {
pub fn new() -> Self {
Self { plan: Plan::default() }
}

pub fn with_task(mut self, task: Task) -> Self {
self.plan.tasks.push(task);
self
}

pub fn build(self) -> Plan {
self.plan
}
}

Async Test Helpers​

// tests/helpers.rs
use tokio::time::timeout;
use std::time::Duration;

pub async fn wait_for_condition<F>(mut condition: F, max_wait: Duration) -> bool
where
F: FnMut() -> bool,
{
let start = Instant::now();
while start.elapsed() < max_wait {
if condition() {
return true;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
false
}

Component-Specific Guidelines​

AGQ Testing​

  • 100% coverage on authentication
  • 100% coverage on RESP parser
  • Fuzz RESP protocol parsing
  • Test all data structure operations (lists, sorted sets)
  • Test concurrent access scenarios
  • Test TTL expiration

AGW Testing​

  • Test all tool executions
  • Test timeout enforcement
  • Test stdout/stderr capture
  • Test fail-fast behavior
  • Test graceful shutdown
  • Mock external tool calls in tests

AGX Testing​

  • Test plan generation with different prompts
  • Test schema validation
  • Test tool registry integration
  • Mock LLM calls in tests
  • Test plan serialization/deserialization


Maintained by: AGX Core Team Review cycle: Quarterly Questions? See component-specific CLAUDE.md files or open an issue