CUB-123: integrate gateway, wire PostgreSQL repositories, add SSE streaming
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m23s
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m23s
- Create repository/ package with pgx-backed CRUD for agents, sessions, tasks, projects - Define AgentRepo/SessionRepo/TaskRepo/ProjectRepo interfaces - Update handler to use repository interfaces instead of in-memory stores - Add SSE broker with GET /api/events endpoint (text/event-stream) - Add gateway client that polls OpenClaw for agent states - Add GATEWAY_URL and GATEWAY_POLL_INTERVAL config fields - Seed 5 demo agents (Otto, Rex, Dex, Hex, Pip) on empty DB - Update router to wire SSE broker - All 21 handler tests pass with mock repos
This commit is contained in:
235
go-backend/internal/handler/mock_repos_test.go
Normal file
235
go-backend/internal/handler/mock_repos_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/models"
|
||||
)
|
||||
|
||||
// mockAgentRepo implements repository.AgentRepo in-memory for testing.
|
||||
type mockAgentRepo struct {
|
||||
mu sync.RWMutex
|
||||
m map[string]models.AgentCardData
|
||||
}
|
||||
|
||||
func newMockAgentRepo() *mockAgentRepo {
|
||||
return &mockAgentRepo{m: make(map[string]models.AgentCardData)}
|
||||
}
|
||||
|
||||
func (r *mockAgentRepo) Create(ctx context.Context, a models.AgentCardData) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if _, ok := r.m[a.ID]; ok {
|
||||
return fmt.Errorf("duplicate key: %s", a.ID)
|
||||
}
|
||||
r.m[a.ID] = a
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mockAgentRepo) Get(ctx context.Context, id string) (models.AgentCardData, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
a, ok := r.m[id]
|
||||
if !ok {
|
||||
return a, fmt.Errorf("not found: %s", id)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (r *mockAgentRepo) List(ctx context.Context, statusFilter models.AgentStatus) ([]models.AgentCardData, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
result := make([]models.AgentCardData, 0, len(r.m))
|
||||
for _, a := range r.m {
|
||||
if statusFilter != "" && a.Status != statusFilter {
|
||||
continue
|
||||
}
|
||||
result = append(result, a)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *mockAgentRepo) Update(ctx context.Context, id string, req models.UpdateAgentRequest) (models.AgentCardData, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
a, ok := r.m[id]
|
||||
if !ok {
|
||||
return a, fmt.Errorf("not found: %s", id)
|
||||
}
|
||||
if req.Status != nil {
|
||||
a.Status = *req.Status
|
||||
}
|
||||
if req.CurrentTask != nil {
|
||||
a.CurrentTask = req.CurrentTask
|
||||
}
|
||||
if req.TaskProgress != nil {
|
||||
a.TaskProgress = req.TaskProgress
|
||||
}
|
||||
if req.TaskElapsed != nil {
|
||||
a.TaskElapsed = req.TaskElapsed
|
||||
}
|
||||
if req.Channel != nil {
|
||||
a.Channel = *req.Channel
|
||||
}
|
||||
if req.ErrorMessage != nil {
|
||||
a.ErrorMessage = req.ErrorMessage
|
||||
}
|
||||
a.LastActivity = time.Now().UTC().Format(time.RFC3339)
|
||||
r.m[id] = a
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (r *mockAgentRepo) Delete(ctx context.Context, id string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if _, ok := r.m[id]; !ok {
|
||||
return fmt.Errorf("not found: %s", id)
|
||||
}
|
||||
delete(r.m, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mockAgentRepo) Count(ctx context.Context) (int, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.m), nil
|
||||
}
|
||||
|
||||
// ─── Mock Session Repo ──────────────────────────────────────────────────────────
|
||||
|
||||
type mockSessionRepo struct {
|
||||
mu sync.RWMutex
|
||||
m map[string]models.Session
|
||||
}
|
||||
|
||||
func newMockSessionRepo() *mockSessionRepo {
|
||||
return &mockSessionRepo{m: make(map[string]models.Session)}
|
||||
}
|
||||
|
||||
func (r *mockSessionRepo) Create(ctx context.Context, s models.Session) (models.Session, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if s.ID == "" {
|
||||
s.ID = fmt.Sprintf("sess-%d", len(r.m)+1)
|
||||
}
|
||||
if s.StartedAt.IsZero() {
|
||||
s.StartedAt = time.Now().UTC()
|
||||
}
|
||||
if s.LastActivityAt.IsZero() {
|
||||
s.LastActivityAt = s.StartedAt
|
||||
}
|
||||
r.m[s.ID] = s
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (r *mockSessionRepo) ListActive(ctx context.Context) ([]models.Session, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
result := make([]models.Session, 0)
|
||||
for _, s := range r.m {
|
||||
if s.Status == "running" || s.Status == "streaming" {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *mockSessionRepo) Count(ctx context.Context) (int, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.m), nil
|
||||
}
|
||||
|
||||
// ─── Mock Task Repo ─────────────────────────────────────────────────────────────
|
||||
|
||||
type mockTaskRepo struct {
|
||||
mu sync.RWMutex
|
||||
m map[string]models.Task
|
||||
}
|
||||
|
||||
func newMockTaskRepo() *mockTaskRepo {
|
||||
return &mockTaskRepo{m: make(map[string]models.Task)}
|
||||
}
|
||||
|
||||
func (r *mockTaskRepo) Create(ctx context.Context, t models.Task) (models.Task, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if t.ID == "" {
|
||||
t.ID = fmt.Sprintf("task-%d", len(r.m)+1)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if t.CreatedAt.IsZero() {
|
||||
t.CreatedAt = now
|
||||
}
|
||||
if t.UpdatedAt.IsZero() {
|
||||
t.UpdatedAt = now
|
||||
}
|
||||
r.m[t.ID] = t
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (r *mockTaskRepo) ListRecent(ctx context.Context) ([]models.Task, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
result := make([]models.Task, 0, len(r.m))
|
||||
for _, t := range r.m {
|
||||
result = append(result, t)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *mockTaskRepo) Count(ctx context.Context) (int, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.m), nil
|
||||
}
|
||||
|
||||
// ─── Mock Project Repo ─────────────────────────────────────────────────────────
|
||||
|
||||
type mockProjectRepo struct {
|
||||
mu sync.RWMutex
|
||||
m map[string]models.Project
|
||||
}
|
||||
|
||||
func newMockProjectRepo() *mockProjectRepo {
|
||||
return &mockProjectRepo{m: make(map[string]models.Project)}
|
||||
}
|
||||
|
||||
func (r *mockProjectRepo) Create(ctx context.Context, p models.Project) (models.Project, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if p.ID == "" {
|
||||
p.ID = fmt.Sprintf("proj-%d", len(r.m)+1)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if p.CreatedAt.IsZero() {
|
||||
p.CreatedAt = now
|
||||
}
|
||||
if p.UpdatedAt.IsZero() {
|
||||
p.UpdatedAt = now
|
||||
}
|
||||
if p.AgentIDs == nil {
|
||||
p.AgentIDs = []string{}
|
||||
}
|
||||
r.m[p.ID] = p
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (r *mockProjectRepo) List(ctx context.Context) ([]models.Project, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
result := make([]models.Project, 0, len(r.m))
|
||||
for _, p := range r.m {
|
||||
result = append(result, p)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *mockProjectRepo) Count(ctx context.Context) (int, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.m), nil
|
||||
}
|
||||
Reference in New Issue
Block a user