go-agentic/docs/development.md
Snider 6e8ae2a1ec docs: graduate TODO/FINDINGS into production documentation
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>
2026-02-20 15:02:20 +00:00

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 ../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

# 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 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.goNewService, 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

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