- 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>
199 lines
5.2 KiB
Go
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
|
|
}
|