# 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 ``` 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 ``` --- ## 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.