Three coordinated fixes for CI pipeline race conditions:
1. Woodpecker step dependencies: Added depends_on: [deps] to all 6 component
templates (service, worker, cli, app-astro, app-react, app-nextjs) so build
steps wait for go work sync to complete.
2. Idempotent resource provisioning: Modified provisionResources() to check
for existing database/cache before creating, preventing "already exists"
errors on component re-adds.
3. Batch component endpoint: POST /projects/{id}/components/batch enables
atomic multi-component additions in a single git commit. Validates all
components upfront, provisions infra sequentially, commits code components
atomically.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
184 lines
4.6 KiB
Go
184 lines
4.6 KiB
Go
package worker
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
)
|
|
|
|
func TestSDLCTaskExecutor_ParseSpec(t *testing.T) {
|
|
exec := NewSDLCTaskExecutor(SDLCTaskExecutorConfig{
|
|
Namespace: "rdev",
|
|
})
|
|
|
|
t.Run("valid spec", func(t *testing.T) {
|
|
spec, err := exec.parseSpec(map[string]any{
|
|
"command": "feature-create",
|
|
"args": []any{"auth-flow", "--title", "Authentication Flow"},
|
|
"git_clone_url": "https://git.example.com/owner/repo.git",
|
|
"auto_commit": true,
|
|
"auto_push": true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("parseSpec() error = %v", err)
|
|
}
|
|
if spec.Command != "feature-create" {
|
|
t.Errorf("got command %q, want %q", spec.Command, "feature-create")
|
|
}
|
|
if len(spec.Args) != 3 {
|
|
t.Errorf("got %d args, want 3", len(spec.Args))
|
|
}
|
|
if spec.GitCloneURL != "https://git.example.com/owner/repo.git" {
|
|
t.Errorf("got git_clone_url %q", spec.GitCloneURL)
|
|
}
|
|
if !spec.AutoCommit {
|
|
t.Error("expected auto_commit = true")
|
|
}
|
|
if !spec.AutoPush {
|
|
t.Error("expected auto_push = true")
|
|
}
|
|
})
|
|
|
|
t.Run("missing command", func(t *testing.T) {
|
|
_, err := exec.parseSpec(map[string]any{
|
|
"git_clone_url": "https://git.example.com/owner/repo.git",
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for missing command")
|
|
}
|
|
})
|
|
|
|
t.Run("missing git_clone_url", func(t *testing.T) {
|
|
_, err := exec.parseSpec(map[string]any{
|
|
"command": "feature-create",
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for missing git_clone_url")
|
|
}
|
|
})
|
|
|
|
t.Run("args as string slice", func(t *testing.T) {
|
|
spec, err := exec.parseSpec(map[string]any{
|
|
"command": "feature-create",
|
|
"args": []string{"arg1", "arg2"},
|
|
"git_clone_url": "https://git.example.com/owner/repo.git",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("parseSpec() error = %v", err)
|
|
}
|
|
if len(spec.Args) != 2 {
|
|
t.Errorf("got %d args, want 2", len(spec.Args))
|
|
}
|
|
})
|
|
|
|
t.Run("empty args", func(t *testing.T) {
|
|
spec, err := exec.parseSpec(map[string]any{
|
|
"command": "state",
|
|
"git_clone_url": "https://git.example.com/owner/repo.git",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("parseSpec() error = %v", err)
|
|
}
|
|
if len(spec.Args) != 0 {
|
|
t.Errorf("got %d args, want 0", len(spec.Args))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSDLCTaskExecutor_Execute_NoPodGitOps(t *testing.T) {
|
|
exec := NewSDLCTaskExecutor(SDLCTaskExecutorConfig{
|
|
Namespace: "rdev",
|
|
PodGitOps: nil, // No git operations configured
|
|
})
|
|
|
|
task := &domain.WorkTask{
|
|
ID: "task-1",
|
|
ProjectID: "project-1",
|
|
Type: domain.WorkTaskTypeSDLC,
|
|
Spec: map[string]any{
|
|
"command": "feature-create",
|
|
"args": []any{"auth-flow"},
|
|
"git_clone_url": "https://git.example.com/owner/repo.git",
|
|
},
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
result := exec.Execute(context.Background(), task)
|
|
if result.Success {
|
|
t.Error("expected failure when pod git operations not configured")
|
|
}
|
|
if result.Error == "" {
|
|
t.Error("expected error message")
|
|
}
|
|
}
|
|
|
|
func TestSDLCTaskExecutor_Execute_InvalidSpec(t *testing.T) {
|
|
exec := NewSDLCTaskExecutor(SDLCTaskExecutorConfig{
|
|
Namespace: "rdev",
|
|
})
|
|
|
|
task := &domain.WorkTask{
|
|
ID: "task-1",
|
|
ProjectID: "project-1",
|
|
Type: domain.WorkTaskTypeSDLC,
|
|
Spec: map[string]any{
|
|
// Missing required fields
|
|
},
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
result := exec.Execute(context.Background(), task)
|
|
if result.Success {
|
|
t.Error("expected failure for invalid spec")
|
|
}
|
|
if result.Error == "" {
|
|
t.Error("expected error message for invalid spec")
|
|
}
|
|
}
|
|
|
|
func TestSDLCTaskSpec_Valid(t *testing.T) {
|
|
// Verify the domain type is valid
|
|
spec := domain.SDLCTaskSpec{
|
|
Command: "feature-create",
|
|
Args: []string{"auth-flow", "--title", "Auth Flow"},
|
|
GitCloneURL: "https://git.example.com/owner/repo.git",
|
|
AutoCommit: true,
|
|
AutoPush: true,
|
|
}
|
|
|
|
if spec.Command == "" {
|
|
t.Error("command should not be empty")
|
|
}
|
|
if len(spec.Args) != 3 {
|
|
t.Errorf("got %d args, want 3", len(spec.Args))
|
|
}
|
|
}
|
|
|
|
func TestShellQuote(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{"simple", "auth-flow", "auth-flow"},
|
|
{"with space", "Authentication System", "'Authentication System'"},
|
|
{"with single quote", "it's working", "'it'\"'\"'s working'"},
|
|
{"flag", "--title", "--title"},
|
|
{"empty", "", ""},
|
|
{"with dollar", "$HOME", "'$HOME'"},
|
|
{"with backtick", "`cmd`", "'`cmd`'"},
|
|
{"with semicolon", "a;b", "'a;b'"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := shellQuote(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("shellQuote(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|