- 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>
208 lines
4.9 KiB
Go
208 lines
4.9 KiB
Go
package domain
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestBuildSpec_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
spec BuildSpec
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "valid spec with prompt",
|
|
spec: BuildSpec{Prompt: "Build a landing page"},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "valid spec with all fields",
|
|
spec: BuildSpec{Prompt: "Build it", Template: "nextjs", AutoCommit: true, AutoPush: true},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "empty prompt",
|
|
spec: BuildSpec{},
|
|
wantErr: ErrPromptRequired,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.spec.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 TestBuildStatus_IsValid(t *testing.T) {
|
|
tests := []struct {
|
|
status BuildStatus
|
|
want bool
|
|
}{
|
|
{BuildStatusPending, true},
|
|
{BuildStatusRunning, true},
|
|
{BuildStatusCompleted, true},
|
|
{BuildStatusFailed, true},
|
|
{BuildStatusCancelled, 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 TestBuildResult_ToWorkResult(t *testing.T) {
|
|
t.Run("success with all fields", func(t *testing.T) {
|
|
result := &BuildResult{
|
|
Success: true,
|
|
Output: "Build completed",
|
|
CommitSHA: "abc123",
|
|
FilesChanged: []string{"main.go", "go.mod"},
|
|
DurationMs: 1500,
|
|
Artifacts: map[string]string{"deploy_url": "https://example.com"},
|
|
}
|
|
|
|
wr := result.ToWorkResult()
|
|
|
|
if wr.Output != "Build completed" {
|
|
t.Errorf("Output = %q, want %q", wr.Output, "Build completed")
|
|
}
|
|
if wr.Artifacts["commit_sha"] != "abc123" {
|
|
t.Errorf("commit_sha = %q, want %q", wr.Artifacts["commit_sha"], "abc123")
|
|
}
|
|
if wr.Artifacts["duration_ms"] != "1500" {
|
|
t.Errorf("duration_ms = %q, want %q", wr.Artifacts["duration_ms"], "1500")
|
|
}
|
|
if wr.Artifacts["files_changed_count"] != "2" {
|
|
t.Errorf("files_changed_count = %q, want %q", wr.Artifacts["files_changed_count"], "2")
|
|
}
|
|
if wr.Artifacts["deploy_url"] != "https://example.com" {
|
|
t.Errorf("deploy_url = %q, want %q", wr.Artifacts["deploy_url"], "https://example.com")
|
|
}
|
|
})
|
|
|
|
t.Run("failure uses error as output", func(t *testing.T) {
|
|
result := &BuildResult{
|
|
Success: false,
|
|
Output: "partial output",
|
|
Error: "build failed: missing deps",
|
|
}
|
|
|
|
wr := result.ToWorkResult()
|
|
|
|
if wr.Output != "build failed: missing deps" {
|
|
t.Errorf("Output = %q, want error message", wr.Output)
|
|
}
|
|
})
|
|
|
|
t.Run("nil artifacts map is safe", func(t *testing.T) {
|
|
result := &BuildResult{
|
|
Success: true,
|
|
Output: "done",
|
|
}
|
|
|
|
wr := result.ToWorkResult()
|
|
|
|
if wr.Output != "done" {
|
|
t.Errorf("Output = %q, want %q", wr.Output, "done")
|
|
}
|
|
if len(wr.Artifacts) != 0 {
|
|
t.Errorf("Artifacts = %v, want empty", wr.Artifacts)
|
|
}
|
|
})
|
|
|
|
t.Run("zero duration not included", func(t *testing.T) {
|
|
result := &BuildResult{
|
|
Success: true,
|
|
DurationMs: 0,
|
|
}
|
|
|
|
wr := result.ToWorkResult()
|
|
|
|
if _, ok := wr.Artifacts["duration_ms"]; ok {
|
|
t.Error("duration_ms should not be in artifacts when zero")
|
|
}
|
|
})
|
|
|
|
t.Run("nil receiver returns empty result", func(t *testing.T) {
|
|
var result *BuildResult
|
|
wr := result.ToWorkResult()
|
|
|
|
if wr.Output != "" {
|
|
t.Errorf("Output = %q, want empty", wr.Output)
|
|
}
|
|
if wr.Artifacts != nil {
|
|
t.Errorf("Artifacts = %v, want nil", wr.Artifacts)
|
|
}
|
|
})
|
|
|
|
t.Run("promoted fields overwrite existing artifacts", func(t *testing.T) {
|
|
result := &BuildResult{
|
|
Success: true,
|
|
CommitSHA: "new-sha",
|
|
Artifacts: map[string]string{
|
|
"commit_sha": "old-sha",
|
|
"custom_key": "kept",
|
|
},
|
|
}
|
|
|
|
wr := result.ToWorkResult()
|
|
|
|
if wr.Artifacts["commit_sha"] != "new-sha" {
|
|
t.Errorf("commit_sha = %q, want %q (promoted field should overwrite)", wr.Artifacts["commit_sha"], "new-sha")
|
|
}
|
|
if wr.Artifacts["custom_key"] != "kept" {
|
|
t.Errorf("custom_key = %q, want %q", wr.Artifacts["custom_key"], "kept")
|
|
}
|
|
})
|
|
|
|
t.Run("failed with empty error keeps output", func(t *testing.T) {
|
|
result := &BuildResult{
|
|
Success: false,
|
|
Output: "partial output before crash",
|
|
Error: "",
|
|
}
|
|
|
|
wr := result.ToWorkResult()
|
|
|
|
if wr.Output != "partial output before crash" {
|
|
t.Errorf("Output = %q, want original output when error is empty", wr.Output)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBuildStatus_IsTerminal(t *testing.T) {
|
|
tests := []struct {
|
|
status BuildStatus
|
|
want bool
|
|
}{
|
|
{BuildStatusPending, false},
|
|
{BuildStatusRunning, false},
|
|
{BuildStatusCompleted, true},
|
|
{BuildStatusFailed, true},
|
|
{BuildStatusCancelled, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(string(tt.status), func(t *testing.T) {
|
|
if got := tt.status.IsTerminal(); got != tt.want {
|
|
t.Errorf("IsTerminal() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|