go-agentic/docs/development.md

247 lines
8.4 KiB
Markdown
Raw Normal View History

# go-agentic Development Guide
Module: `forge.lthn.ai/core/go-agentic`
---
## Prerequisites
- Go 1.25 or later (the module declares `go 1.25.5`)
- Go workspace containing `../go` and `../go-store` (the `go.mod` replace directives assume sibling directories)
- `git` on `$PATH` (required at runtime by `context.go` and `completion.go`)
- `claude` on `$PATH` (required at runtime by `service.go` — not needed for tests)
- `gh` on `$PATH` and authenticated (required at runtime by `completion.go` CreatePR — not needed for unit tests)
- Redis (optional — required only for `RedisStore` and `RedisRegistry` integration tests)
### Go Workspace Setup
The repository is part of a Go workspace rooted one level up. If running outside the workspace, the replace directives in `go.mod` must resolve:
```
forge.lthn.ai/core/go → ../go
forge.lthn.ai/core/go-store → ../go-store
```
Either work within the workspace or run `go work sync` after cloning adjacent repositories.
---
## Build and Test Commands
```bash
# Run all tests
go test ./...
# Run a single named test
go test -v -run TestAllowance ./...
# Run with race detector (recommended before committing)
go test -race ./...
# Run benchmarks
go test -bench=. ./...
# Generate coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
```
There is no Taskfile or Makefile in this module. All automation uses plain `go` commands.
---
## Test Patterns
### Naming Conventions
Tests use a `_Good`, `_Bad`, `_Ugly` suffix pattern inherited from the Core Go framework:
- `_Good` — happy path, expected success
- `_Bad` — expected error conditions (invalid input, limit exceeded, not found)
- `_Ugly` — panics, race conditions, or extreme edge cases
### File Organisation
Test files are co-located with their source files. The following test files exist:
| File | Covers |
|---|---|
| `allowance_test.go` | MemoryStore CRUD, UsageRecord, MemoryRegistry |
| `allowance_edge_test.go` | Boundary conditions: exact limit, one-over, warning threshold |
| `allowance_error_test.go` | Error paths in RecordUsage, Check, and ResetAgent via mock store |
| `allowance_sqlite_test.go` | SQLiteStore with `:memory:` database |
| `allowance_redis_test.go` | RedisStore integration (skip if Redis unavailable) |
| `client_test.go` | HTTP client with `httptest.Server` mocks |
| `completion_git_test.go` | AutoCommit, CreateBranch, CommitAndSync, GetDiff using real git repos |
| `completion_test.go` | Completion type parsing |
| `config_test.go` | YAML and .env loading, env var overrides |
| `context_git_test.go` | findRelatedCode with real git repos, keyword search, 10-file limit, truncation |
| `context_test.go` | BuildTaskContext, GatherRelatedFiles |
| `coverage_boost_test.go` | Miscellaneous coverage for edge cases |
| `dispatcher_test.go` | Full dispatch flow, allowance rejection, no-agent path, loop cancellation |
| `embed_test.go` | Prompt() hit/miss and whitespace trimming |
| `events_integration_test.go` | Event emission from Dispatcher and AllowanceService |
| `events_test.go` | ChannelEmitter, MultiEmitter |
| `lifecycle_test.go` | Full claim -> process -> complete integration; fail/cancel flows; concurrent agents |
| `logs_test.go` | StreamLogs polling, context cancellation |
| `registry_redis_test.go` | RedisRegistry integration (skip if Redis unavailable) |
| `registry_sqlite_test.go` | SQLiteRegistry with `:memory:` database |
| `registry_test.go` | MemoryRegistry |
| `router_test.go` | Capability matching, load distribution, critical priority, tie-breaking |
| `service_test.go` | DefaultServiceOptions, TaskPrompt Set/GetTaskID, TaskCommit fields |
| `status_test.go` | GetStatus with nil and mock components, FormatStatus |
| `submit_test.go` | SubmitTask validation and creation |
### Writing Tests
Use `github.com/stretchr/testify/assert` for non-fatal assertions and `require` for fatal ones:
```go
func TestAllowance_Check_Good(t *testing.T) {
store := NewMemoryStore()
svc := NewAllowanceService(store)
err := store.SetAllowance(&AgentAllowance{
AgentID: "agent-1",
DailyTokenLimit: 1000,
})
require.NoError(t, err)
result, err := svc.Check("agent-1", "claude-sonnet-4-5")
require.NoError(t, err)
assert.True(t, result.Allowed)
assert.Equal(t, AllowanceOK, result.Status)
}
```
### Redis Tests
Redis tests use a skip-if-no-Redis pattern:
```go
func TestRedisStore_GetAllowance_Good(t *testing.T) {
rs, err := NewRedisStore("localhost:6379")
if err != nil {
t.Skip("Redis not available:", err)
}
defer rs.Close()
// ...
}
```
Each Redis test uses a unique key prefix to avoid cross-test interference:
```go
rs, _ := NewRedisStore("localhost:6379",
WithRedisPrefix("test-"+t.Name()),
)
defer rs.FlushPrefix(context.Background())
```
### Mock Store Pattern
Use the `errorStore` mock (defined in `allowance_error_test.go`) to test error paths in `AllowanceService` without a real store:
```go
type errorStore struct{ AllowanceStore }
func (e *errorStore) GetAllowance(string) (*AgentAllowance, error) {
return nil, errors.New("store failure")
}
```
---
## Coding Standards
### Language
UK English throughout. Use: colour, organisation, behaviour, licence (noun), centre, serialise, recognise. Never use American spellings.
### Go Style
- All files must have `package agentic` as the first declaration.
- The module does not use `declare(strict_types)` (this is a Go project, not PHP), but all function signatures must have explicit parameter and return types.
- Exported types and functions must have Go doc comments.
- Error strings use the `"op: what failed"` convention via `log.E(op, "message", err)` from the Core framework.
- Sentinel errors (such as `ErrNoEligibleAgent`) are package-level `var` declarations using `errors.New`.
- Defensive copies on all `Get`/`Set` operations in in-memory stores to prevent aliasing.
### Conventional Commits
Commit message format:
```
type(scope): short description
Optional longer body explaining why, not what.
Co-Authored-By: Virgil <virgil@lethean.io>
```
Types: `feat`, `fix`, `test`, `docs`, `refactor`, `chore`
Scopes used in this project: `allowance`, `registry`, `dispatch`, `cli`, `events`, `config`, `test`
Examples from the commit log:
```
feat(events): Phase 8 — event hooks for task lifecycle and quota notifications
feat(dispatch): Phase 7 — priority-ordered dispatch with retry backoff and dead-letter
feat(registry): Phase 5 — persistent agent registry (SQLite + Redis + config factory)
feat(allowance): add Redis backend for AllowanceStore
test: achieve 85.6% coverage with 7 new test files
fix(config): change DefaultBaseURL to localhost, annotate reserved fields
```
### Licence
All source files are licensed under EUPL-1.2. Do not add MIT, Apache, or other licence headers.
### Co-Author
Every commit must include:
```
Co-Authored-By: Virgil <virgil@lethean.io>
```
---
## Coverage Target
The project targets 85%+ line coverage. Current coverage after Phase 8 is above 96%. The following are intentionally excluded from coverage:
- `service.go``NewService`, `OnStartup`, `handleTask`, `doCommit`, `doPrompt` require the full Core DI container and a `claude` subprocess. Testing these functions needs either a complete Core bootstrap or interface extraction of the subprocess call.
- `completion.go` CreatePR — requires `gh` CLI installed and authenticated. Not suitable for unit tests.
---
## Adding a New Backend
To add a new `AllowanceStore` or `AgentRegistry` backend:
1. Create a new file (e.g., `allowance_memcache.go`) implementing all interface methods.
2. Add a `Close() error` method if the backend holds resources.
3. Add a test file using the same pattern as `allowance_sqlite_test.go` (use an in-memory or mock connection; add a skip guard if the backend requires an external service).
4. Add a case to `NewAllowanceStoreFromConfig` or `NewAgentRegistryFromConfig` in `config.go`.
5. Add a corresponding config field to `AllowanceConfig` or `RegistryConfig`.
---
## Running Integration Tests Locally
```bash
# With Redis running on default port
go test -v -run TestRedis ./...
go test -v -run TestRedisRegistry ./...
# With race detector for concurrent-access tests
go test -race -v -run TestConcurrent ./...
# Run only fast unit tests (skips Redis)
go test -short ./...
```
The `-short` flag is respected: Redis tests check `testing.Short()` and skip accordingly if you add that guard, though the current pattern skips on connection failure rather than the short flag.