rdev/internal/domain/verify_test.go
jordan b5fdf35f1b feat: add WorkerService.FailTask for audit updates + visual verification scaffolding
- 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>
2026-02-03 00:09:16 -07:00

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)
}
}
}