sp3-verify-1770325830/.sdlc/features/websocket-chat/tasks.md
rdev-worker 42c1444274
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature websocket-chat --requirements 'GET /ws upgrades to...
2026-02-05 21:50:17 +00:00

305 lines
7.8 KiB
Markdown

# Implementation Tasks: WebSocket Chat
## Task Overview
| ID | Task | Status | Blocked By |
|----|------|--------|------------|
| WS-1 | Extend config with Redis URL | pending | - |
| WS-2 | Wire realtime components in main.go | pending | WS-1 |
| WS-3 | Mount WebSocket handler in routes.go | pending | WS-2 |
| WS-4 | Add WebSocket documentation to spec.go | pending | WS-3 |
| WS-5 | Add integration tests for WebSocket chat | pending | WS-3 |
---
## WS-1: Extend config with Redis URL
**Status:** pending
### Description
Add Redis URL configuration to the chat-api service config.
### Files to Modify
- `services/chat-api/internal/config/config.go`
### Implementation Details
Add to the `Config` struct:
```go
// Redis configuration for realtime pub/sub
RedisURL string
```
Add to the `Load()` function:
```go
RedisURL: os.Getenv("REDIS_URL"),
```
### Acceptance Criteria
- [ ] `Config` struct has `RedisURL` field
- [ ] `Load()` reads from `REDIS_URL` environment variable
- [ ] Empty string is valid (disables Redis, uses local-only broadcast)
---
## WS-2: Wire realtime components in main.go
**Status:** pending
**Blocked By:** WS-1
### Description
Initialize the realtime hub and Redis broadcaster in the service entry point.
### Files to Modify
- `services/chat-api/cmd/server/main.go`
### Implementation Details
1. Import required packages:
```go
import (
"context"
"github.com/redis/go-redis/v9"
"git.threesix.ai/jordan/sp3-verify-1770325830/pkg/realtime"
"git.threesix.ai/jordan/sp3-verify-1770325830/services/chat-api/internal/config"
)
```
2. Load config and create context:
```go
cfg := config.Load()
ctx := context.Background()
```
3. Create hub:
```go
hub := realtime.NewHub(logger)
go hub.Run(ctx)
```
4. Create Redis broadcaster (optional):
```go
var broadcaster realtime.Broadcaster
if cfg.RedisURL != "" {
opts, err := redis.ParseURL(cfg.RedisURL)
if err != nil {
logger.Error("invalid redis url", "error", err)
} else {
redisClient := redis.NewClient(opts)
broadcaster = realtime.NewRedisBroadcaster(redisClient, hub, logger)
go broadcaster.Run(ctx)
}
}
```
5. Pass hub and broadcaster to route registration:
```go
api.RegisterRoutes(application, exampleService, hub, broadcaster)
```
### Acceptance Criteria
- [ ] Hub is created and running in a goroutine
- [ ] Redis broadcaster is created when `REDIS_URL` is set
- [ ] Redis broadcaster is nil when `REDIS_URL` is empty
- [ ] Hub and broadcaster are passed to route registration
---
## WS-3: Mount WebSocket handler in routes.go
**Status:** pending
**Blocked By:** WS-2
### Description
Update the route registration to accept realtime components and mount the WebSocket handler.
### Files to Modify
- `services/chat-api/internal/api/routes.go`
### Implementation Details
1. Update function signature:
```go
func RegisterRoutes(
application *app.App,
exampleService *service.ExampleService,
hub realtime.Hub,
broadcaster realtime.Broadcaster,
) {
```
2. Import realtime package:
```go
import "git.threesix.ai/jordan/sp3-verify-1770325830/pkg/realtime"
```
3. Create and configure WebSocket handler:
```go
wsHandler := realtime.NewHandler(hub, logger, realtime.HandlerConfig{
Broadcaster: broadcaster,
OnConnect: func(conn realtime.Connection) {
logger.Info("websocket client connected",
"client_id", conn.ID(),
"user_id", conn.UserID(),
)
},
OnDisconnect: func(conn realtime.Connection) {
logger.Info("websocket client disconnected",
"client_id", conn.ID(),
)
},
OnMessage: func(conn realtime.Connection, msg *realtime.Message) *realtime.Message {
if msg.Type == "" {
msg.Type = realtime.MessageTypeChat
}
return msg
},
AuthRequired: cfg.AuthEnabled,
})
```
4. Mount WebSocket routes:
```go
application.Mount("/api/chat-api/ws", wsHandler.Routes())
```
### Acceptance Criteria
- [ ] Function signature accepts `hub` and `broadcaster` parameters
- [ ] WebSocket handler is created with appropriate callbacks
- [ ] Handler is mounted at `/api/chat-api/ws`
- [ ] Auth requirement respects `AUTH_ENABLED` config
---
## WS-4: Add WebSocket documentation to spec.go
**Status:** pending
**Blocked By:** WS-3
### Description
Document the WebSocket endpoint in the OpenAPI specification.
### Files to Modify
- `services/chat-api/internal/api/spec.go`
### Implementation Details
Add WebSocket tag:
```go
spec.WithTag("WebSocket", "Real-time WebSocket endpoints")
```
Add WebSocket path documentation:
```go
spec.AddPath("/api/chat-api/ws", "get", map[string]any{
"summary": "WebSocket connection",
"description": "Upgrades to WebSocket for real-time chat. Messages are broadcast to all connected clients via Redis pub/sub.",
"tags": []string{"WebSocket"},
"responses": map[string]any{
"101": map[string]any{
"description": "Switching Protocols - WebSocket upgrade successful",
},
"401": openapi.OpResponse("Unauthorized - authentication required", nil),
},
})
spec.AddPath("/api/chat-api/ws/{room}", "get", map[string]any{
"summary": "WebSocket connection to room",
"description": "Upgrades to WebSocket and joins the specified room. Messages are broadcast only to clients in the same room.",
"tags": []string{"WebSocket"},
"parameters": []map[string]any{
{
"name": "room",
"in": "path",
"required": true,
"description": "Room identifier to join",
"schema": map[string]any{"type": "string"},
},
},
"responses": map[string]any{
"101": map[string]any{
"description": "Switching Protocols - WebSocket upgrade successful",
},
"401": openapi.OpResponse("Unauthorized - authentication required", nil),
},
})
```
### Acceptance Criteria
- [ ] WebSocket tag added to spec
- [ ] `/api/chat-api/ws` endpoint documented
- [ ] `/api/chat-api/ws/{room}` endpoint documented with room parameter
- [ ] Response codes documented (101, 401)
---
## WS-5: Add integration tests for WebSocket chat
**Status:** pending
**Blocked By:** WS-3
### Description
Add tests to verify WebSocket functionality.
### Files to Create
- `services/chat-api/internal/api/handlers/websocket_test.go`
### Implementation Details
Test cases:
1. **Connection upgrade** - Verify WebSocket upgrade succeeds
2. **Message broadcast** - Verify messages sent by one client reach others
3. **Room isolation** - Verify room messages don't leak to other rooms
4. **Ping/pong** - Verify heartbeat mechanism
Example test structure:
```go
package handlers_test
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gorilla/websocket"
"git.threesix.ai/jordan/sp3-verify-1770325830/pkg/logging"
"git.threesix.ai/jordan/sp3-verify-1770325830/pkg/realtime"
)
func TestWebSocket_Connection(t *testing.T) {
// Create hub
hub := realtime.NewHub(logging.Nop())
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
// Create handler
handler := realtime.NewHandler(hub, logging.Nop(), realtime.HandlerConfig{})
// Create test server
server := httptest.NewServer(handler.Routes())
defer server.Close()
// Connect WebSocket
wsURL := "ws" + server.URL[4:] + "/"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial failed: %v", err)
}
defer conn.Close()
// Verify connection count
time.Sleep(50 * time.Millisecond)
if hub.ConnectionCount() != 1 {
t.Errorf("expected 1 connection, got %d", hub.ConnectionCount())
}
}
```
### Acceptance Criteria
- [ ] Test WebSocket connection upgrade works
- [ ] Test message broadcast to multiple clients
- [ ] Test room-based message isolation
- [ ] All tests pass