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>
4.9 KiB
go-ws Development Guide
Module: forge.lthn.ai/core/go-ws
Prerequisites
- Go 1.25 or later (benchmarks use
b.Loop(), which requires 1.25+) - A running Redis instance if exercising
RedisBridgeintegration tests (default:10.69.69.87:6379) - No CGO dependencies; builds on Linux, macOS, and Windows without toolchain additions
Build and Test
# Run the full test suite
go test ./...
# Run with race detector (required before every commit)
go test -race ./...
# Run a single test by name
go test -v -run TestHub_Run ./...
# Run benchmarks
go test -bench=. -benchmem ./...
# Run a specific benchmark
go test -bench=BenchmarkBroadcast_100 -benchmem ./...
There is no Taskfile in this module. All automation goes through the standard go toolchain.
Test Organisation
Tests live in the same package (package ws), giving direct access to unexported fields for white-box assertions. Three test files exist:
| File | Coverage |
|---|---|
ws_test.go |
Hub lifecycle, broadcast, channel send, subscribe/unsubscribe, readPump, writePump, integration via httptest, auth integration |
auth_test.go |
APIKeyAuthenticator, AuthenticatorFunc, nil authenticator, OnAuthFailure callback |
redis_test.go |
RedisBridge unit and integration (skipped when Redis unavailable) |
ws_bench_test.go |
9 benchmarks: broadcast, channel send, parallel variants, marshal, WebSocket end-to-end, subscribe/unsubscribe cycle, multi-channel fanout |
Test Naming Convention
Integration tests use httptest.NewServer + gorilla/websocket Dial for end-to-end coverage. Unit tests drive the Hub directly via channels and method calls.
Redis Tests
Redis integration tests call skipIfNoRedis(t) at the top, which pings the configured address and skips if unreachable:
func TestRedisBridge_PublishBroadcast(t *testing.T) {
client := skipIfNoRedis(t)
// ...
}
Each Redis test uses a unique time-based prefix to avoid collisions between parallel runs. Cleanup removes any leftover keys in t.Cleanup.
Benchmark Pattern
Benchmarks use b.Loop() (Go 1.25+) and b.ReportAllocs():
func BenchmarkBroadcast_100(b *testing.B) {
// setup ...
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
_ = hub.Broadcast(msg)
}
b.StopTimer()
// drain ...
}
Coding Standards
Language
UK English throughout: "initialise", "colour", "behaviour", "cancelled", "unauthorised". Never American spellings.
Go Style
declare(strict_types=1)is a PHP concept; in Go, enforce correctness via the type system andgo vet.- All exported symbols must have doc comments.
- Error strings are lowercase and do not end with punctuation (Go convention).
- Use named return values only when they materially clarify intent.
- Prefer
require.NoErroroverassert.NoErrorwhen a test cannot continue after failure.
Licence Header
Every .go source file begins with:
// SPDX-Licence-Identifier: EUPL-1.2
This applies to all files including test files.
Formatting
Standard gofmt. No additional linter configuration is committed. Run go vet ./... before committing; the suite must be clean.
Commit Guidelines
Commits follow Conventional Commits:
type(scope): description
Common types: feat, fix, test, docs, refactor, bench.
Scope is the logical area: ws, auth, redis, reconnect.
Every commit includes the co-author trailer:
Co-Authored-By: Virgil <virgil@lethean.io>
Example:
feat(redis): add envelope pattern for loop prevention
Generated a random sourceID per bridge instance at construction.
Listener drops messages where sourceID matches local bridge.
Co-Authored-By: Virgil <virgil@lethean.io>
Adding a New Message Type
- Add a constant to the
MessageTypeblock inws.go. - Handle the new type in
readPumpif it is client-initiated. - Add a helper method on
Hubif it is server-initiated (following the pattern ofSendProcessOutput). - Add unit tests covering the new type in
ws_test.go. - Update
docs/architecture.mdmessage type table.
Adding a New Authenticator
Implement the Authenticator interface:
type MyJWTAuth struct { /* ... */ }
func (a *MyJWTAuth) Authenticate(r *http.Request) ws.AuthResult {
token := r.Header.Get("Authorization")
claims, err := validateJWT(token)
if err != nil {
return ws.AuthResult{Valid: false, Error: err}
}
return ws.AuthResult{
Valid: true,
UserID: claims.Subject,
Claims: map[string]any{"roles": claims.Roles},
}
}
Pass it to HubConfig.Authenticator. The built-in APIKeyAuthenticator in auth.go serves as a reference implementation. JWT validation libraries are intentionally not imported; consumers bring their own.
Repository
- Forge:
ssh://git@forge.lthn.ai:2223/core/go-ws.git - Push via SSH only; HTTPS authentication is not configured on Forge.
- Licence: EUPL-1.2