rdev/internal/domain/verify.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

199 lines
5.2 KiB
Go

package domain
import (
"errors"
"fmt"
"net/url"
"strconv"
)
// Verify-related errors.
var (
ErrVerifyURLRequired = errors.New("url is required for verify spec")
)
// VerifySpec defines what a verify task should accomplish.
type VerifySpec struct {
// URL is the page to capture (required).
URL string `json:"url"`
// Viewports is a list of viewport sizes to capture.
// Default: ["1920x1080", "768x1024", "375x667"]
Viewports []string `json:"viewports,omitempty"`
// WaitFor is a CSS selector to wait for before capturing.
// Default: "body"
WaitFor string `json:"wait_for,omitempty"`
// WaitTimeout is the maximum time to wait for the selector in milliseconds.
// Default: 10000
WaitTimeout int `json:"wait_timeout,omitempty"`
// FullPage captures the entire scrollable page if true.
FullPage bool `json:"full_page,omitempty"`
// Video records a video of the page load if true.
Video bool `json:"video,omitempty"`
// Evaluate enables AI evaluation of the captures (Week 3).
Evaluate bool `json:"evaluate,omitempty"`
// Prompt provides context for AI evaluation (Week 3).
Prompt string `json:"prompt,omitempty"`
// CallbackURL is the webhook URL for completion notification.
CallbackURL string `json:"callback_url,omitempty"`
}
// DefaultViewports returns the default viewport sizes for captures.
func DefaultViewports() []string {
return []string{"1920x1080", "768x1024", "375x667"}
}
// Validate checks that the VerifySpec has all required fields.
func (s *VerifySpec) Validate() error {
if s.URL == "" {
return ErrVerifyURLRequired
}
// Validate URL format
u, err := url.Parse(s.URL)
if err != nil {
return fmt.Errorf("invalid url: %w", err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("url scheme must be http or https, got %q", u.Scheme)
}
// Validate callback URL if provided
if s.CallbackURL != "" {
cu, err := url.Parse(s.CallbackURL)
if err != nil {
return fmt.Errorf("invalid callback_url: %w", err)
}
if cu.Scheme != "http" && cu.Scheme != "https" {
return fmt.Errorf("callback_url scheme must be http or https, got %q", cu.Scheme)
}
}
return nil
}
// WithDefaults returns a copy of the spec with default values applied.
func (s *VerifySpec) WithDefaults() *VerifySpec {
spec := *s
if len(spec.Viewports) == 0 {
spec.Viewports = DefaultViewports()
}
if spec.WaitFor == "" {
spec.WaitFor = "body"
}
if spec.WaitTimeout == 0 {
spec.WaitTimeout = 10000
}
return &spec
}
// VerifyResult captures the outcome of a verify execution.
type VerifyResult struct {
// Success indicates whether the capture completed successfully.
Success bool `json:"success"`
// Screenshots maps viewport size to screenshot file path.
// Example: {"1920x1080": "/captures/task-id/1920_1080.png"}
Screenshots map[string]string `json:"screenshots,omitempty"`
// Video is the path to the recorded video if requested.
Video string `json:"video,omitempty"`
// Evaluation contains the AI evaluation result (Week 3).
Evaluation string `json:"evaluation,omitempty"`
// Score is the AI-assigned score 0-100 (Week 3).
Score int `json:"score,omitempty"`
// Passed indicates whether the AI evaluation passed (Week 3).
Passed bool `json:"passed,omitempty"`
// DurationMs is how long the capture took in milliseconds.
DurationMs int64 `json:"duration_ms"`
// Error contains the error message if capture failed.
Error string `json:"error,omitempty"`
}
// ToWorkResult converts a VerifyResult to a WorkResult for queue compatibility.
// Screenshots are promoted to artifacts with viewport as key prefix.
func (r *VerifyResult) ToWorkResult() *WorkResult {
if r == nil {
return &WorkResult{}
}
wr := &WorkResult{}
// Use error as output if failed, otherwise empty (captures are in artifacts)
if !r.Success && r.Error != "" {
wr.Output = r.Error
}
// Promote screenshots and metadata to artifacts
if len(r.Screenshots) > 0 || r.Video != "" || r.DurationMs > 0 {
wr.Artifacts = make(map[string]string)
// Add screenshots
for viewport, path := range r.Screenshots {
wr.Artifacts["screenshot_"+viewport] = path
}
// Add video if present
if r.Video != "" {
wr.Artifacts["video"] = r.Video
}
// Add duration
if r.DurationMs > 0 {
wr.Artifacts["duration_ms"] = strconv.FormatInt(r.DurationMs, 10)
}
// Add evaluation results (Week 3)
if r.Evaluation != "" {
wr.Artifacts["evaluation"] = r.Evaluation
}
if r.Score > 0 {
wr.Artifacts["score"] = strconv.Itoa(r.Score)
}
if r.Passed {
wr.Artifacts["passed"] = "true"
}
}
return wr
}
// ToBuildResult converts a VerifyResult to a BuildResult for executor compatibility.
// This allows the verify executor to return results through the work queue.
func (r *VerifyResult) ToBuildResult() *BuildResult {
if r == nil {
return &BuildResult{}
}
br := &BuildResult{
Success: r.Success,
DurationMs: r.DurationMs,
Error: r.Error,
}
// Promote screenshots and video to artifacts
if len(r.Screenshots) > 0 || r.Video != "" {
br.Artifacts = make(map[string]string)
for viewport, path := range r.Screenshots {
br.Artifacts["screenshot_"+viewport] = path
}
if r.Video != "" {
br.Artifacts["video"] = r.Video
}
}
return br
}