Testability Boundaries

Trait boundaries around external systems and testing guidance for deterministic orchestration.

Agentty keeps external systems behind trait boundaries so orchestration logic can be tested deterministically.

Testability and Boundaries🔗

External-boundary traits are mocked with mockall, usually via #[cfg_attr(test, mockall::automock)]; shared workspace crates such as ag-agent, ag-forge, and ag-git expose test mocks through crate-root exports gated by test features or test-only exports. The major boundaries:

TraitModuleBoundary
GitClientcrates/ag-git/src/client.rsGit and worktree operations (hook readiness, merge, rebase, diff, push, status, ahead/behind).
FsClientinfra/fs.rsAsync filesystem operations and path probes.
AgentChannelcrates/ag-agent/src/channel.rsProvider-agnostic turn execution.
OneShotClientcrates/ag-agent/src/agent/submission.rsIsolated structured prompts, including transport routing, protocol repair, runtime cleanup, and usage aggregation.
AgentBackendcrates/ag-agent/src/agent/backend.rsPer-provider setup and transport command construction.
AppServerClientcrates/ag-agent/src/app_server/contract.rsProvider app-server RPC execution and session runtime lifecycle.
ReviewRequestClientcrates/ag-forge/src/client.rsReview-request orchestration, comment loading, thread reply/resolution through gh/glab, plus project-scoped assigned GitHub issue list/detail loading through gh.
EventSourceruntime/event.rsTerminal event polling for deterministic event-loop tests.
Clockinfra/clock.rsWall-clock and monotonic time for session orchestration and render throttling.
TmuxClientinfra/tmux.rsTmux subprocess operations for opening worktrees.
ClipboardImageClientinfra/clipboard_image.rsClipboard image capture and temp-file persistence; host clipboard reads are isolated in ag-clipboard.
Repository traitsinfra/db/*.rsNarrow persistence boundaries (SessionRepository, ProjectRepository, ReviewRepository, UsageRepository, ActivityRepository, OperationRepository, SettingRepository).

Beyond these, narrower internal command-runner boundaries (for example ForgeCommandRunner, GitCommandRunner, TmuxCommandRunner, UpdateRunner, and the provider transport traits) keep subprocess sequencing and retry behavior deterministic in unit tests. The runtime also accepts Terminal<B: Backend> via run_with_backend, enabling in-process TUI tests with TestBackend.

The ag-agent crate keeps provider routers, parsers, and concrete transport adapters private. Application workflows that submit isolated utility prompts inject OneShotClient; provider and transport tests use the feature-gated crate-root mocks and helper factories rather than deep module paths. CLI-backed session turns, one-shot prompts, and protocol-repair retries share one crate-private raw subprocess executor for command construction, stdin delivery, PID lifetime, stream collection, and exit classification. Adapter-specific observers translate those raw events into session updates, while one-shot callers consume the collected raw output; response parsing and repair policy stay in the owning adapter.

Typed Errors Across Layers🔗

Each infra boundary exposes a typed error enum (DbError, GitError, AppServerError, AgentError, OneShotError, ClipboardError, and so on) instead of opaque String errors. The private app-server transport error is wrapped by AppServerError::Transport, then by AgentError::AppServer, allowing ?-propagation through the transport, provider, and channel layers without collapsing causal context into formatted strings.

The app layer propagates infra errors through SessionError (app/session/error.rs) and AppError (app/error.rs), both of which wrap infra and OneShotError values via #[from] plus a Workflow(String) variant for contextual app-level failures. At event and display boundaries, errors are converted to String via Display because those types require Clone and Eq.

Testing Guidance🔗

When adding higher-level flows involving multiple external commands, prefer injectable trait boundaries and mockall-based tests over flaky end-to-end shell-heavy tests. Add a narrower internal command-runner boundary when a public orchestration trait still needs deterministic coverage of subprocess sequencing or retry behavior.

Apply the same rule to filesystem discovery and path probes in app/ and runtime/: route directory walking, exists checks, canonicalize, and file copy or persistence helpers through an infra boundary instead of calling std::fs or Path helpers directly from orchestration code. Likewise, route Instant::now() and SystemTime::now() through the shared Clock boundary.

TUI E2E Testing Framework (testty)🔗

The testty workspace crate provides a dual-oracle model for TUI end-to-end testing. The PTY path (portable-pty + vt100) is the semantic oracle for text, style, and location assertions; the VHS path is the visual oracle and review artifact generator.

ModulePurpose
sessionPTY executor: spawns binaries, writes input, captures ANSI output.
frameTerminal frame parser: ANSI bytes to a cell grid.
region / locatorRectangular regions and style-aware text locators.
assertion / recipeStructured matchers and agent-friendly expectation helpers.
scenario / step / journeyScenario DSL compiled to PTY or VHS.
vhs / snapshot / proofVHS tape compilation, paired baselines, proof backends.
featureFeatureDemo builder with hash-cached VHS GIF generation.

testty has no crate-root re-export module: every public item is addressable only through its owning module path (for example, use testty::scenario::Scenario;). The tests/public_api.rs tripwire pins those per-module items as the documented stable surface.