- Add FailTask to WorkerService to update build_audit on failure path (fixes bug where audit showed "running" when task actually failed) - Add WorkServiceFailer interface to avoid circular dependency - Add VerifyExecutor with Playwright-based visual verification - Add verify domain types (VerifySpec, VerifyResult, screenshot capture) - Wire VerifyExecutor placeholder into WorkExecutor (impl in Week 2) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
275 lines
7.1 KiB
Go
275 lines
7.1 KiB
Go
package domain
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestVerifySpec_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
spec VerifySpec
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "valid spec with URL only",
|
|
spec: VerifySpec{URL: "https://example.com"},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "valid spec with all fields",
|
|
spec: VerifySpec{
|
|
URL: "https://example.com/page",
|
|
Viewports: []string{"1920x1080", "375x667"},
|
|
WaitFor: "#main",
|
|
WaitTimeout: 5000,
|
|
FullPage: true,
|
|
Video: true,
|
|
CallbackURL: "https://webhook.example.com/notify",
|
|
},
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "empty URL",
|
|
spec: VerifySpec{},
|
|
wantErr: ErrVerifyURLRequired,
|
|
},
|
|
{
|
|
name: "invalid URL scheme",
|
|
spec: VerifySpec{URL: "ftp://example.com"},
|
|
wantErr: nil, // Validate will fail but not return ErrVerifyURLRequired
|
|
},
|
|
{
|
|
name: "invalid callback URL scheme",
|
|
spec: VerifySpec{URL: "https://example.com", CallbackURL: "ftp://webhook.com"},
|
|
wantErr: nil, // Validate will fail but not return ErrVerifyURLRequired
|
|
},
|
|
}
|
|
|
|
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 tt.name == "invalid URL scheme" || tt.name == "invalid callback URL scheme" {
|
|
// These should fail with a different error
|
|
if err == nil {
|
|
t.Errorf("Validate() expected error for %s", tt.name)
|
|
}
|
|
} else if err != nil {
|
|
t.Errorf("Validate() unexpected error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerifySpec_WithDefaults(t *testing.T) {
|
|
t.Run("applies defaults to empty spec", func(t *testing.T) {
|
|
spec := &VerifySpec{URL: "https://example.com"}
|
|
result := spec.WithDefaults()
|
|
|
|
if len(result.Viewports) != 3 {
|
|
t.Errorf("expected 3 default viewports, got %d", len(result.Viewports))
|
|
}
|
|
if result.Viewports[0] != "1920x1080" {
|
|
t.Errorf("expected first viewport '1920x1080', got %q", result.Viewports[0])
|
|
}
|
|
if result.WaitFor != "body" {
|
|
t.Errorf("expected WaitFor 'body', got %q", result.WaitFor)
|
|
}
|
|
if result.WaitTimeout != 10000 {
|
|
t.Errorf("expected WaitTimeout 10000, got %d", result.WaitTimeout)
|
|
}
|
|
})
|
|
|
|
t.Run("preserves existing values", func(t *testing.T) {
|
|
spec := &VerifySpec{
|
|
URL: "https://example.com",
|
|
Viewports: []string{"800x600"},
|
|
WaitFor: "#app",
|
|
WaitTimeout: 5000,
|
|
}
|
|
result := spec.WithDefaults()
|
|
|
|
if len(result.Viewports) != 1 || result.Viewports[0] != "800x600" {
|
|
t.Errorf("expected preserved viewports, got %v", result.Viewports)
|
|
}
|
|
if result.WaitFor != "#app" {
|
|
t.Errorf("expected preserved WaitFor, got %q", result.WaitFor)
|
|
}
|
|
if result.WaitTimeout != 5000 {
|
|
t.Errorf("expected preserved WaitTimeout, got %d", result.WaitTimeout)
|
|
}
|
|
})
|
|
|
|
t.Run("does not modify original", func(t *testing.T) {
|
|
spec := &VerifySpec{URL: "https://example.com"}
|
|
_ = spec.WithDefaults()
|
|
|
|
if len(spec.Viewports) != 0 {
|
|
t.Error("original spec should not be modified")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestVerifyResult_ToWorkResult(t *testing.T) {
|
|
t.Run("success with screenshots", func(t *testing.T) {
|
|
result := &VerifyResult{
|
|
Success: true,
|
|
Screenshots: map[string]string{
|
|
"1920x1080": "/captures/task-1/1920_1080.png",
|
|
"375x667": "/captures/task-1/375_667.png",
|
|
},
|
|
DurationMs: 1500,
|
|
}
|
|
|
|
wr := result.ToWorkResult()
|
|
|
|
if wr.Output != "" {
|
|
t.Errorf("expected empty output on success, got %q", wr.Output)
|
|
}
|
|
if wr.Artifacts["screenshot_1920x1080"] != "/captures/task-1/1920_1080.png" {
|
|
t.Errorf("screenshot_1920x1080 = %q", wr.Artifacts["screenshot_1920x1080"])
|
|
}
|
|
if wr.Artifacts["screenshot_375x667"] != "/captures/task-1/375_667.png" {
|
|
t.Errorf("screenshot_375x667 = %q", wr.Artifacts["screenshot_375x667"])
|
|
}
|
|
if wr.Artifacts["duration_ms"] != "1500" {
|
|
t.Errorf("duration_ms = %q", wr.Artifacts["duration_ms"])
|
|
}
|
|
})
|
|
|
|
t.Run("success with video", func(t *testing.T) {
|
|
result := &VerifyResult{
|
|
Success: true,
|
|
Screenshots: map[string]string{"1920x1080": "/captures/task-1/1920_1080.png"},
|
|
Video: "/captures/task-1/recording.webm",
|
|
DurationMs: 2000,
|
|
}
|
|
|
|
wr := result.ToWorkResult()
|
|
|
|
if wr.Artifacts["video"] != "/captures/task-1/recording.webm" {
|
|
t.Errorf("video = %q", wr.Artifacts["video"])
|
|
}
|
|
})
|
|
|
|
t.Run("failure uses error as output", func(t *testing.T) {
|
|
result := &VerifyResult{
|
|
Success: false,
|
|
Error: "capture failed: timeout",
|
|
}
|
|
|
|
wr := result.ToWorkResult()
|
|
|
|
if wr.Output != "capture failed: timeout" {
|
|
t.Errorf("Output = %q, want error message", wr.Output)
|
|
}
|
|
})
|
|
|
|
t.Run("nil receiver returns empty result", func(t *testing.T) {
|
|
var result *VerifyResult
|
|
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("evaluation results included", func(t *testing.T) {
|
|
result := &VerifyResult{
|
|
Success: true,
|
|
Evaluation: "Page renders correctly with all elements visible",
|
|
Score: 85,
|
|
Passed: true,
|
|
DurationMs: 1000,
|
|
}
|
|
|
|
wr := result.ToWorkResult()
|
|
|
|
if wr.Artifacts["evaluation"] != "Page renders correctly with all elements visible" {
|
|
t.Errorf("evaluation = %q", wr.Artifacts["evaluation"])
|
|
}
|
|
if wr.Artifacts["score"] != "85" {
|
|
t.Errorf("score = %q", wr.Artifacts["score"])
|
|
}
|
|
if wr.Artifacts["passed"] != "true" {
|
|
t.Errorf("passed = %q", wr.Artifacts["passed"])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestVerifyResult_ToBuildResult(t *testing.T) {
|
|
t.Run("success conversion", func(t *testing.T) {
|
|
result := &VerifyResult{
|
|
Success: true,
|
|
Screenshots: map[string]string{
|
|
"1920x1080": "/captures/task-1/1920_1080.png",
|
|
},
|
|
Video: "/captures/task-1/recording.webm",
|
|
DurationMs: 1500,
|
|
}
|
|
|
|
br := result.ToBuildResult()
|
|
|
|
if !br.Success {
|
|
t.Error("expected success = true")
|
|
}
|
|
if br.DurationMs != 1500 {
|
|
t.Errorf("DurationMs = %d, want 1500", br.DurationMs)
|
|
}
|
|
if br.Artifacts["screenshot_1920x1080"] != "/captures/task-1/1920_1080.png" {
|
|
t.Errorf("screenshot artifact missing")
|
|
}
|
|
if br.Artifacts["video"] != "/captures/task-1/recording.webm" {
|
|
t.Errorf("video artifact missing")
|
|
}
|
|
})
|
|
|
|
t.Run("failure conversion", func(t *testing.T) {
|
|
result := &VerifyResult{
|
|
Success: false,
|
|
Error: "capture failed",
|
|
DurationMs: 500,
|
|
}
|
|
|
|
br := result.ToBuildResult()
|
|
|
|
if br.Success {
|
|
t.Error("expected success = false")
|
|
}
|
|
if br.Error != "capture failed" {
|
|
t.Errorf("Error = %q", br.Error)
|
|
}
|
|
})
|
|
|
|
t.Run("nil receiver returns empty", func(t *testing.T) {
|
|
var result *VerifyResult
|
|
br := result.ToBuildResult()
|
|
|
|
if br.Success {
|
|
t.Error("expected success = false for nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDefaultViewports(t *testing.T) {
|
|
viewports := DefaultViewports()
|
|
|
|
if len(viewports) != 3 {
|
|
t.Fatalf("expected 3 viewports, got %d", len(viewports))
|
|
}
|
|
|
|
expected := []string{"1920x1080", "768x1024", "375x667"}
|
|
for i, vp := range expected {
|
|
if viewports[i] != vp {
|
|
t.Errorf("viewport[%d] = %q, want %q", i, viewports[i], vp)
|
|
}
|
|
}
|
|
}
|