Summary
Several commands shell out to external programs (git, browser, gpg) and can't be tested with wiremock alone. The GitHub CLI solves this with a command stubber that intercepts all subprocess calls via regex pattern matching.
What gh-cli does
Their implementation lives in `internal/run/stub.go`:
- A `CommandStubber` replaces the real command runner during tests
- Register expected commands as regex patterns with exit codes and output:
```go
cs.Register(`git fetch origin +refs/heads/feature:refs/remotes/origin/feature`, 0, "")
cs.Register(`git checkout -b feature --track origin/feature`, 0, "")
```
- Auto-adds git auth patterns for fetch/pull/push/clone/remote-add/submodule
- Verifies all registered stubs were matched during test cleanup (fails on unused stubs)
- Supports callbacks to inspect raw arguments
- Normalizes `git.exe` to `git` for cross-platform consistency
What this would look like in Rust
A trait-based abstraction over `std::process::Command`:
```rust
#[cfg_attr(test, mockall::automock)]
pub trait CommandRunner {
fn run(&self, program: &str, args: &[&str]) -> eyre::Resultstd::process::Output;
}
```
Or a simpler approach: a global function pointer / env-var-based stub that intercepts command execution in test builds.
Commands this would unlock testing for
- `repo clone` / `wiki clone` -- git clone subprocess
- `pr checkout` -- git fetch + git checkout
- `pr create --agit` -- git push
- `user key upload` / `user gpg upload` / `user gpg verify` -- gpg subprocess
- All browse commands -- `open::that_detached` subprocess
Trade-offs
- Pro: Would enable 100% command coverage without real git repos or browsers
- Pro: Verifies exact git commands being constructed (argument order, flags, etc.)
- Con: Significant refactoring -- every callsite using `std::process::Command` or `tokio::process::Command` needs to go through the abstraction
- Con: Adds complexity to production code paths for testability
Alternative (current approach)
Tier 1/2 testing covers most gaps without refactoring:
- Real temp git repos for clone/checkout tests
- URL extraction for browse commands
- EDITOR env var for editor tests
- HTTP-layer mocking for file upload/download
The command stubber is the long-term ideal but not blocking for the current test suite.
## Summary
Several commands shell out to external programs (git, browser, gpg) and can't be tested with wiremock alone. The GitHub CLI solves this with a command stubber that intercepts all subprocess calls via regex pattern matching.
## What gh-cli does
Their implementation lives in \`internal/run/stub.go\`:
1. A \`CommandStubber\` replaces the real command runner during tests
2. Register expected commands as regex patterns with exit codes and output:
\`\`\`go
cs.Register(\`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature\`, 0, "")
cs.Register(\`git checkout -b feature --track origin/feature\`, 0, "")
\`\`\`
3. Auto-adds git auth patterns for fetch/pull/push/clone/remote-add/submodule
4. Verifies all registered stubs were matched during test cleanup (fails on unused stubs)
5. Supports callbacks to inspect raw arguments
6. Normalizes \`git.exe\` to \`git\` for cross-platform consistency
## What this would look like in Rust
A trait-based abstraction over \`std::process::Command\`:
\`\`\`rust
#[cfg_attr(test, mockall::automock)]
pub trait CommandRunner {
fn run(&self, program: &str, args: &[&str]) -> eyre::Result<std::process::Output>;
}
\`\`\`
Or a simpler approach: a global function pointer / env-var-based stub that intercepts command execution in test builds.
## Commands this would unlock testing for
- \`repo clone\` / \`wiki clone\` -- git clone subprocess
- \`pr checkout\` -- git fetch + git checkout
- \`pr create --agit\` -- git push
- \`user key upload\` / \`user gpg upload\` / \`user gpg verify\` -- gpg subprocess
- All browse commands -- \`open::that_detached\` subprocess
## Trade-offs
- **Pro**: Would enable 100% command coverage without real git repos or browsers
- **Pro**: Verifies exact git commands being constructed (argument order, flags, etc.)
- **Con**: Significant refactoring -- every callsite using \`std::process::Command\` or \`tokio::process::Command\` needs to go through the abstraction
- **Con**: Adds complexity to production code paths for testability
## Alternative (current approach)
Tier 1/2 testing covers most gaps without refactoring:
- Real temp git repos for clone/checkout tests
- URL extraction for browse commands
- EDITOR env var for editor tests
- HTTP-layer mocking for file upload/download
The command stubber is the long-term ideal but not blocking for the current test suite.