diff --git a/AGENTS.md b/AGENTS.md index c61d45e..b00c06b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,18 +2,18 @@ ## Overview -CLI tool for managing local backups using `rsync` as the engine. -Built in Go with `cobra` for CLI, `afero` for filesystem abstraction, and YAML for configuration. Local-only — no remote rsync support. +CLI tool for managing local backups using `rsync` as the engine. Built in Go with `cobra` for CLI, `afero` for filesystem abstraction, and YAML for configuration. Local-only — no remote rsync support. + +Design principles: simple human-readable configuration, minimal interdependencies, flexible variable/macro substitution (`${var}`). Each backup job can be listed, simulated (dry-run), or run independently. ## Code Style -- Follow idiomatic Go conventions (see `go.mod` for the required Go version) -- Format with `go fmt`; lint with `golangci-lint` (config in `.golangci.yml`) +- Idiomatic Go (see `go.mod` for version); format with `go fmt`, lint with `golangci-lint` (`.golangci.yml`) - All linters enabled by default — check `.golangci.yml` for disabled ones -- Keep packages focused: `cmd/` for CLI wiring, `internal/` for core logic -- Prefer dependency injection over global state for testability -- Use interfaces at consumption boundaries (see `internal/exec.go`, `internal/job_command.go`) -- All output in commands routed through `cmd.OutOrStdout()` or injected `io.Writer` — never raw `fmt.Printf` +- Packages: `cmd/` for CLI wiring, `internal/` for core logic +- Prefer dependency injection over global state; use interfaces at consumption boundaries +- Route all output through `cmd.OutOrStdout()` or injected `io.Writer` — never raw `fmt.Printf` +- Dual logging: user-facing output via `io.Writer`, file logging via `*log.Logger` under `logs/` - All commands use `RunE` with wrapped errors ## Architecture @@ -28,21 +28,17 @@ backup/ test/ # Unit tests + mockery-generated mocks ``` -### Key Types & Interfaces - -- **`Exec`** (interface): Command execution abstraction (`OsExec` for real, `MockExec` for tests) -- **`JobCommand`** (interface): `Run(job Job) JobStatus` + `GetVersionInfo()` — implemented by `ListCommand`, `SimulateCommand`, `SyncCommand` -- **`SharedCommand`** (struct): Base for all commands — holds `BinPath`, `BaseLogPath`, `Shell Exec`, `Output io.Writer` -- **`CoverageChecker`** (struct): Analyzes path coverage with injected `*log.Logger` and `afero.Fs` -- **`Config`**: YAML-based (`Config`, `Job`, `Path`, variables with `${var}` substitution) +### Key Types, Interfaces & Dependency Injection -### Dependency Injection +| Type | Kind | Description | Injected Dependencies | +| ----------------- | --------- | -------------------------------------------------------------------------------------------------------------- | -------------------------- | +| `Exec` | interface | Command execution abstraction (`OsExec` real, `MockExec` tests) | — | +| `JobCommand` | interface | `Run(job Job) JobStatus` + `GetVersionInfo()` — implemented by `ListCommand`, `SimulateCommand`, `SyncCommand` | `Exec` via constructors | +| `SharedCommand` | struct | Base for all commands — holds `BinPath`, `BaseLogPath`, `Shell Exec`, `Output io.Writer` | `io.Writer`, `Exec` | +| `CoverageChecker` | struct | Analyzes path coverage | `*log.Logger`, `afero.Fs` | +| `Config` | struct | YAML config (`Config`, `Job`, `Path`, `${var}` substitution); custom `UnmarshalYAML` for job defaults | `*log.Logger` in `Apply()` | -- `Exec` injected into all command constructors (`NewListCommand`, `NewSimulateCommand`, `NewSyncCommand`) -- `io.Writer` injected into `SharedCommand` for output capture -- `afero.Fs` injected into `BuildRootCommandWithFs()` → `buildCheckCoverageCommand(fs)` -- `*log.Logger` injected into `CoverageChecker` and `Config.Apply()` -- Commands use `cmd.OutOrStdout()` for testable output +Additional injection points: `afero.Fs` into `BuildRootCommandWithFs()` → `buildCheckCoverageCommand(fs)`; commands use `cmd.OutOrStdout()` for testable output. ## Build and Test @@ -61,36 +57,22 @@ make report-coverage # Generate HTML coverage report ## Testing Conventions -- See `docs/testing-guide.md` for patterns and examples -- Use **dependency injection** — inject interfaces, not concrete types +- See `docs/testing-guide.md` for patterns; `docs/mockery-integration.md` for mock setup - **Mocks**: Generated with [mockery](https://github.com/vektra/mockery) (config: `.mockery.yml`) - - Mock files live in `internal/test/` as `mock__test.go` - - Mock structs named `Mock` (e.g., `MockExec`, `MockJobCommand`) - - See `docs/mockery-integration.md` for setup details -- Use `testify` for assertions (`require` / `assert`) + - Files: `internal/test/mock__test.go`; structs: `Mock` +- Assertions via `testify` (`require` / `assert`); prefer table-driven tests - Test files live in `/test/` subdirectories -- Prefer table-driven tests for multiple input scenarios -- Use `afero.NewMemMapFs()` in tests — never hit the real filesystem -- Use `bytes.Buffer` or `io.Discard` for output capture in tests -- Integration tests use `//go:build integration` tag and run real rsync on temp directories -- CI enforces coverage threshold via `make check-coverage` +- Use `afero.NewMemMapFs()` — never hit the real filesystem +- Use `bytes.Buffer` or `io.Discard` for output capture +- Integration tests: `//go:build integration` tag, real rsync on temp directories ## CI Pipeline -CI runs on every push/PR to `main` (`.github/workflows/go.yml`): +Runs on every push/PR to `main` (`.github/workflows/go.yml`): 1. Sanity check (format + clean + mod tidy) -2. Lint (golangci-lint) +2. Lint (`golangci-lint`) 3. Build 4. Test (with `-race` flag) -5. Integration test (with real rsync, `-tags=integration`) +5. Integration test (real rsync, `-tags=integration`) 6. Coverage threshold enforcement (98%) - -## Conventions - -- No remote rsync — only locally mounted paths -- Job-level granularity: each backup job can be listed, simulated, or run independently -- Dry-run/simulate mode available for all operations -- Logging goes to both an injected `io.Writer` (user output) and `*log.Logger` (file logging) under `logs/` -- Custom YAML unmarshaling handles job defaults (see `internal/job.go`) -- CI runs sanity checks, lint, and build on every push/PR (`.github/workflows/go.yml`)