Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 26 additions & 44 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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_<interface>_test.go`
- Mock structs named `Mock<Interface>` (e.g., `MockExec`, `MockJobCommand`)
- See `docs/mockery-integration.md` for setup details
- Use `testify` for assertions (`require` / `assert`)
- Files: `internal/test/mock_<interface>_test.go`; structs: `Mock<Interface>`
- Assertions via `testify` (`require` / `assert`); prefer table-driven tests
- Test files live in `<package>/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`)
Loading