Replace internal task tracking (TODO.md, FINDINGS.md) with structured documentation in docs/. Trim CLAUDE.md to agent instructions only. Co-Authored-By: Virgil <virgil@lethean.io>
8.4 KiB
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
../goand../go-store(thego.modreplace directives assume sibling directories) giton$PATH(required at runtime bycontext.goandcompletion.go)claudeon$PATH(required at runtime byservice.go— not needed for tests)ghon$PATHand authenticated (required at runtime bycompletion.goCreatePR — not needed for unit tests)- Redis (optional — required only for
RedisStoreandRedisRegistryintegration 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
# 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:
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:
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:
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:
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 agenticas 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 vialog.E(op, "message", err)from the Core framework. - Sentinel errors (such as
ErrNoEligibleAgent) are package-levelvardeclarations usingerrors.New. - Defensive copies on all
Get/Setoperations 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,doPromptrequire the full Core DI container and aclaudesubprocess. Testing these functions needs either a complete Core bootstrap or interface extraction of the subprocess call.completion.goCreatePR — requiresghCLI installed and authenticated. Not suitable for unit tests.
Adding a New Backend
To add a new AllowanceStore or AgentRegistry backend:
- Create a new file (e.g.,
allowance_memcache.go) implementing all interface methods. - Add a
Close() errormethod if the backend holds resources. - 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). - Add a case to
NewAllowanceStoreFromConfigorNewAgentRegistryFromConfiginconfig.go. - Add a corresponding config field to
AllowanceConfigorRegistryConfig.
Running Integration Tests Locally
# 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.