|
| 1 | +--- |
| 2 | +trigger: always_on |
| 3 | +--- |
| 4 | + |
| 5 | +# Subtree CLI - Architecture & Testing Patterns |
| 6 | + |
| 7 | +Last Updated: 2025-10-26 | Phase: 10 (Complete) | Context: Proven patterns from MVP + CI implementation |
| 8 | + |
| 9 | +## Architecture Pattern: Library + Executable |
| 10 | + |
| 11 | +**Pattern**: Swift community standard for CLI tools |
| 12 | + |
| 13 | +### Structure |
| 14 | + |
| 15 | +``` |
| 16 | +Sources/ |
| 17 | +├── SubtreeLib/ # Library module (all business logic) |
| 18 | +│ ├── Commands/ # Command implementations |
| 19 | +│ └── Utilities/ # Helper utilities |
| 20 | +└── subtree/ # Executable module (thin wrapper) |
| 21 | + └── EntryPoint.swift # Calls SubtreeCommand.main() |
| 22 | +``` |
| 23 | + |
| 24 | +### Responsibilities |
| 25 | + |
| 26 | +**SubtreeLib (Library)**: |
| 27 | +- Contains ALL business logic |
| 28 | +- Fully unit-testable with `@testable import` |
| 29 | +- Can be used programmatically by other Swift code |
| 30 | +- Exports `SubtreeCommand` as public API |
| 31 | + |
| 32 | +**subtree (Executable)**: |
| 33 | +- Thin wrapper (5 lines) |
| 34 | +- Single responsibility: Call `SubtreeCommand.main()` |
| 35 | +- No business logic |
| 36 | +- Enables standalone CLI usage |
| 37 | + |
| 38 | +### Benefits |
| 39 | + |
| 40 | +✅ **Testability**: Unit tests import SubtreeLib directly |
| 41 | +✅ **Reusability**: Library can be embedded in other tools |
| 42 | +✅ **Separation**: CLI concerns separate from business logic |
| 43 | +✅ **Standard**: Matches swift-argument-parser best practices |
| 44 | + |
| 45 | +## Command Pattern: ArgumentParser |
| 46 | + |
| 47 | +**Framework**: swift-argument-parser 1.6.1 |
| 48 | + |
| 49 | +### Key Conventions |
| 50 | + |
| 51 | +- **Public struct**: Conforming to `ParsableCommand` |
| 52 | +- **Static configuration**: Defines command metadata |
| 53 | +- **Subcommands array**: Lists available subcommands |
| 54 | +- **Public init**: Required for library usage |
| 55 | +- **Version string**: Displayed with `--version` |
| 56 | + |
| 57 | +### Automatic Features |
| 58 | + |
| 59 | +ArgumentParser provides for free: |
| 60 | +- `--help` / `-h` flag |
| 61 | +- `--version` flag |
| 62 | +- Subcommand help (`subtree <command> --help`) |
| 63 | +- Error handling with usage display |
| 64 | +- Bash completion support |
| 65 | + |
| 66 | +## Testing Pattern: Two-Layer Approach |
| 67 | + |
| 68 | +### Unit Tests (SubtreeLibTests/) |
| 69 | + |
| 70 | +**Purpose**: Test business logic directly |
| 71 | + |
| 72 | +**Characteristics**: |
| 73 | +- Uses `@testable import` for internal access |
| 74 | +- Tests configuration, logic, utilities |
| 75 | +- Fast (no process execution) |
| 76 | +- Built-in Swift Testing framework (Swift 6.1) |
| 77 | + |
| 78 | +### Integration Tests (IntegrationTests/) |
| 79 | + |
| 80 | +**Purpose**: Test CLI end-to-end |
| 81 | + |
| 82 | +**Characteristics**: |
| 83 | +- Uses TestHarness to execute actual binary |
| 84 | +- Captures stdout/stderr/exit code |
| 85 | +- Tests real CLI behavior |
| 86 | +- Async execution with swift-subprocess |
| 87 | + |
| 88 | +## TestHarness Pattern |
| 89 | + |
| 90 | +**Location**: `Tests/IntegrationTests/TestHarness.swift` |
| 91 | + |
| 92 | +### Purpose |
| 93 | + |
| 94 | +Execute CLI commands in tests and capture results. |
| 95 | + |
| 96 | +### Core API |
| 97 | + |
| 98 | +```swift |
| 99 | +struct TestHarness { |
| 100 | + func run(arguments: [String]) async throws -> CommandResult |
| 101 | +} |
| 102 | + |
| 103 | +struct CommandResult { |
| 104 | + let stdout: String |
| 105 | + let stderr: String |
| 106 | + let exitCode: Int |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +### Git Support |
| 111 | + |
| 112 | +**GitRepositoryFixture**: Creates temporary git repositories for testing |
| 113 | +- UUID-based unique paths (parallel-safe) |
| 114 | +- Async initialization with initial commit |
| 115 | +- Helper methods: `getCurrentCommit()`, `getCommitCount()`, `fileExists()` |
| 116 | +- Complete cleanup with `tearDown()` |
| 117 | + |
| 118 | +**TestHarness Git Helpers**: `runGit()`, `verifyCommitExists()`, `getGitConfig()`, `isGitRepository()` |
| 119 | + |
| 120 | +## Swift Testing Framework |
| 121 | + |
| 122 | +**Version**: Built into Swift 6.1 toolchain (no package dependency) |
| 123 | + |
| 124 | +### Key Features |
| 125 | + |
| 126 | +- **@Suite**: Groups related tests |
| 127 | +- **@Test**: Marks individual test functions |
| 128 | +- **#expect**: Assertion macro (replaces XCTest assertions) |
| 129 | +- **async/await**: First-class async test support |
| 130 | +- **Parallel execution**: Tests run concurrently by default |
| 131 | + |
| 132 | +## Conventions |
| 133 | + |
| 134 | +### Swift Naming |
| 135 | + |
| 136 | +- **Files**: PascalCase (SubtreeCommand.swift, ExitCode.swift) |
| 137 | +- **Types**: PascalCase (SubtreeCommand, TestHarness) |
| 138 | +- **Functions**: camelCase (run, verifyCommitExists) |
| 139 | +- **Constants**: camelCase (executablePath, exitCode) |
| 140 | + |
| 141 | +### Testing Discipline |
| 142 | + |
| 143 | +- **TDD**: Write tests first, verify failure, implement, verify pass |
| 144 | +- **Test naming**: Descriptive strings in @Test("description") |
| 145 | +- **Async tests**: Use async/await, not callbacks |
| 146 | +- **Test isolation**: Each test is independent, use fixtures for setup |
| 147 | +- **Assertions**: Use #expect(), not traditional assert() |
| 148 | + |
| 149 | +### Code Organization |
| 150 | + |
| 151 | +- **Library first**: All logic in SubtreeLib |
| 152 | +- **Public API**: Only expose what's needed externally |
| 153 | +- **Documentation**: Use Swift doc comments (///) |
| 154 | +- **Error handling**: Use Swift errors, not exit() in library code |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +## MVP Validation Checkpoint |
| 159 | + |
| 160 | +Verify architecture matches implementation: |
| 161 | + |
| 162 | +```bash |
| 163 | +# 1. Library + Executable Pattern |
| 164 | +ls -la Sources/SubtreeLib/Commands/ |
| 165 | +wc -l Sources/subtree/main.swift # Should be ~5 lines |
| 166 | + |
| 167 | +# 2. Test Structure |
| 168 | +grep "@testable import SubtreeLib" Tests/SubtreeLibTests/*.swift |
| 169 | +grep "let harness: TestHarness" Tests/IntegrationTests/*.swift |
| 170 | + |
| 171 | +# 3. Swift Testing (no package dependency) |
| 172 | +! grep "swift-testing" Package.swift || echo "ERROR" |
| 173 | + |
| 174 | +# 4. All tests pass |
| 175 | +swift test # Should show 25/25 tests passed |
| 176 | +``` |
| 177 | + |
| 178 | +**Expected**: All checks pass, architecture is consistent |
| 179 | + |
| 180 | +--- |
| 181 | + |
| 182 | +## Update Triggers |
| 183 | + |
| 184 | +Update this file when: |
| 185 | + |
| 186 | +1. **Architecture patterns change** (new layers, different structure) |
| 187 | +2. **Testing patterns evolve** (new test utilities, different approaches) |
| 188 | +3. **Framework updates** (ArgumentParser API changes, Testing updates) |
| 189 | +4. **New command patterns** (subcommands, shared utilities) |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +**Lines**: ~195 (under 200-line limit) |
0 commit comments