- Add ListPipelines/GetPipeline to CIProvider port with Woodpecker adapter
- Add DNS alias endpoints: GET/POST/DELETE /projects/{id}/domains
- Implement worker executor daemon, build executor, and git operations
- Add build service, worker service, and build audit tracking
- Add worker registry with PostgreSQL adapter and migration
- Add multi-provider code agent interface (Claude Code + OpenCode)
- Add create-and-build combo endpoint
- Update landing-page cookbook to reflect all gaps closed
- Fix tech debt: unified validation, auth scopes, error wrapping, slog patterns
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
147 lines
3.0 KiB
Go
147 lines
3.0 KiB
Go
package domain
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestWorker_IsAvailable(t *testing.T) {
|
|
tests := []struct {
|
|
status WorkerStatus
|
|
want bool
|
|
}{
|
|
{WorkerStatusIdle, true},
|
|
{WorkerStatusBusy, false},
|
|
{WorkerStatusDraining, false},
|
|
{WorkerStatusOffline, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(string(tt.status), func(t *testing.T) {
|
|
w := &Worker{Status: tt.status}
|
|
if got := w.IsAvailable(); got != tt.want {
|
|
t.Errorf("IsAvailable() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorker_IsHealthy(t *testing.T) {
|
|
threshold := 90 * time.Second
|
|
|
|
tests := []struct {
|
|
name string
|
|
lastHeartbeat time.Time
|
|
want bool
|
|
}{
|
|
{
|
|
name: "recent heartbeat",
|
|
lastHeartbeat: time.Now().Add(-30 * time.Second),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "stale heartbeat",
|
|
lastHeartbeat: time.Now().Add(-2 * time.Minute),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "exactly at threshold",
|
|
lastHeartbeat: time.Now().Add(-90 * time.Second),
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := &Worker{LastHeartbeat: tt.lastHeartbeat}
|
|
if got := w.IsHealthy(threshold); got != tt.want {
|
|
t.Errorf("IsHealthy() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkerStatus_IsValid(t *testing.T) {
|
|
tests := []struct {
|
|
status WorkerStatus
|
|
want bool
|
|
}{
|
|
{WorkerStatusIdle, true},
|
|
{WorkerStatusBusy, true},
|
|
{WorkerStatusDraining, true},
|
|
{WorkerStatusOffline, true},
|
|
{"unknown", false},
|
|
{"", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(string(tt.status), func(t *testing.T) {
|
|
if got := tt.status.IsValid(); got != tt.want {
|
|
t.Errorf("IsValid() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorker_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
worker Worker
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "valid worker",
|
|
worker: Worker{ID: "worker-1", Hostname: "host-1"},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "missing ID",
|
|
worker: Worker{Hostname: "host-1"},
|
|
wantErr: ErrWorkerIDRequired,
|
|
},
|
|
{
|
|
name: "missing hostname",
|
|
worker: Worker{ID: "worker-1"},
|
|
wantErr: ErrWorkerHostnameRequired,
|
|
},
|
|
{
|
|
name: "both missing",
|
|
worker: Worker{},
|
|
wantErr: ErrWorkerIDRequired,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.worker.Validate()
|
|
if tt.wantErr != nil {
|
|
if !errors.Is(err, tt.wantErr) {
|
|
t.Errorf("Validate() error = %v, want %v", err, tt.wantErr)
|
|
}
|
|
} else if err != nil {
|
|
t.Errorf("Validate() unexpected error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidWorkerStatuses(t *testing.T) {
|
|
statuses := ValidWorkerStatuses()
|
|
if len(statuses) != 4 {
|
|
t.Errorf("expected 4 statuses, got %d", len(statuses))
|
|
}
|
|
|
|
found := make(map[WorkerStatus]bool)
|
|
for _, s := range statuses {
|
|
found[s] = true
|
|
}
|
|
|
|
expected := []WorkerStatus{WorkerStatusIdle, WorkerStatusBusy, WorkerStatusDraining, WorkerStatusOffline}
|
|
for _, s := range expected {
|
|
if !found[s] {
|
|
t.Errorf("expected %s in valid statuses", s)
|
|
}
|
|
}
|
|
}
|