Skip to content

Refactor for Testability - Core Architecture #209

@loispostula

Description

@loispostula

Problem

The codebase evolved from bash scripts to Rust CLI, resulting in:

  • Monolithic command functions that are hard to test in isolation
  • Heavy coupling to external services (GitHub API, Docker, S3, etc.)
  • Side effects scattered throughout (file I/O, network calls, command execution)
  • Limited ability to test error conditions and edge cases
  • No clear separation between business logic and I/O

Current State Analysis

Hard-to-test patterns:

  1. Commands directly instantiate external clients (Octocrab, ObjectStore, etc.)
  2. Business logic intertwined with I/O operations
  3. No dependency injection or trait-based abstractions
  4. Tests require real git repositories and cargo workspaces
  5. Parallel execution logic hard to test without real semaphores

Proposed Refactoring Strategy

Phase 1: Extract Core Business Logic

1.1 Create domain models

// src/domain/mod.rs
pub mod package;      // Pure Package representations
pub mod dependency;   // Dependency graph logic
pub mod change_set;   // Changed packages computation
pub mod publish_plan; // What to publish, where, how

1.2 Define service traits

// src/services/traits.rs
pub trait GitService {
    fn diff_files(&self, base: &str, head: &str) -> Result<Vec<PathBuf>>;
    fn resolve_ref(&self, ref_name: &str) -> Result<String>;
}

pub trait GithubService {
    async fn find_release(&self, tag: &str) -> Result<Option<Release>>;
    async fn upload_asset(&self, release_id: u64, name: &str, data: Vec<u8>) -> Result<()>;
}

pub trait CargoRegistry {
    async fn package_exists(&self, name: &str, version: &str) -> Result<bool>;
    async fn publish(&self, package_path: &Path, registry: &str) -> Result<()>;
}

1.3 Implement production and test implementations

Phase 2: Refactor Commands to Use Services

Extract testable functions from monolithic commands. Split into:

  • Pure planning functions (no I/O)
  • Execution functions using service traits (testable with mocks)

Phase 3: Improve CrateGraph Testability

Make CrateGraph independent of git2 by extracting git operations to a trait.

Implementation Plan

Week 1-2: Foundation

  • Create src/domain/ with core business models
  • Define service traits in src/services/traits.rs
  • Create src/testing/ with test utilities and mocks
  • Add builder patterns for test fixtures

Week 3-4: Refactor CrateGraph

  • Extract git operations to GitService trait
  • Make CrateGraph::affected_packages use trait instead of git2::Repository
  • Write comprehensive tests for dependency graph logic
  • Add property-based tests for change detection

Week 5-6: Refactor Publish Command

  • Extract publish planning logic (pure functions)
  • Implement service interfaces for GitHub, Cargo registry
  • Refactor publish/mod.rs to use services
  • Write unit tests for publish planning
  • Write integration tests with mocked services

Week 7-8: Refactor Tests Command

  • Extract test execution planning
  • Abstract database and container management
  • Implement testable parallel execution
  • Add tests for test filtering and selection

Week 9-10: Remaining Commands

  • Apply pattern to check_workspace, fix_lock_files, etc.
  • Update all commands to use CommandContext
  • Ensure 80%+ test coverage for business logic

Success Metrics

  • 80%+ unit test coverage for business logic (not including I/O)
  • All commands have integration tests with mocked external services
  • Test execution time < 30s
  • Can test error conditions without real external services
  • New features can be developed with TDD

Breaking Changes

  • Internal refactoring only; CLI interface remains the same
  • May need to update some internal imports if used by external consumers

Labels

refactoring, testing, priority:high

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions