Add REST API endpoints for submitting visual verification tasks,
tracking progress via SSE, and retrieving screenshot/video artifacts.
Changes:
- Add ScopeVerifyRead/ScopeVerifyWrite auth scopes
- Create VerifyService for task submission and lifecycle management
- Create VerifyHandler with POST/GET/DELETE/SSE endpoints:
- POST /verify - Submit capture task
- GET /verify/{taskId} - Get task status and artifacts
- GET /verify/{taskId}/stream - SSE progress stream
- DELETE /verify/{taskId} - Cancel pending task
- GET /projects/{id}/verify - List verify tasks
- Wire VerifyExecutor in main.go for Playwright pod execution
- Fix work.go validation to include "verify" task type
- Add comprehensive handler tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
144 lines
3.7 KiB
Go
144 lines
3.7 KiB
Go
// Package service provides business logic services.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/port"
|
|
)
|
|
|
|
// VerifyService orchestrates verify task submission and tracking.
|
|
// It coordinates between the work queue (execution) for visual captures.
|
|
type VerifyService struct {
|
|
queue port.WorkQueue
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewVerifyService creates a new verify service.
|
|
func NewVerifyService(queue port.WorkQueue, logger *slog.Logger) *VerifyService {
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
return &VerifyService{
|
|
queue: queue,
|
|
logger: logger.With("service", "verify"),
|
|
}
|
|
}
|
|
|
|
// SubmitCapture validates spec and enqueues a verify task.
|
|
// Returns the task ID for status tracking.
|
|
func (s *VerifyService) SubmitCapture(ctx context.Context, projectID string, spec domain.VerifySpec) (string, error) {
|
|
if err := spec.Validate(); err != nil {
|
|
return "", fmt.Errorf("invalid verify spec: %w", err)
|
|
}
|
|
|
|
if projectID == "" {
|
|
return "", fmt.Errorf("project_id is required")
|
|
}
|
|
|
|
// Apply defaults
|
|
specWithDefaults := spec.WithDefaults()
|
|
|
|
// Build work task spec from verify spec
|
|
taskSpec := map[string]any{
|
|
"url": specWithDefaults.URL,
|
|
"viewports": specWithDefaults.Viewports,
|
|
"wait_for": specWithDefaults.WaitFor,
|
|
"wait_timeout": specWithDefaults.WaitTimeout,
|
|
"full_page": specWithDefaults.FullPage,
|
|
"video": specWithDefaults.Video,
|
|
}
|
|
if specWithDefaults.Evaluate {
|
|
taskSpec["evaluate"] = specWithDefaults.Evaluate
|
|
}
|
|
if specWithDefaults.Prompt != "" {
|
|
taskSpec["prompt"] = specWithDefaults.Prompt
|
|
}
|
|
if specWithDefaults.CallbackURL != "" {
|
|
taskSpec["callback_url"] = specWithDefaults.CallbackURL
|
|
}
|
|
|
|
// Create work task
|
|
task := &domain.WorkTask{
|
|
ProjectID: projectID,
|
|
Type: domain.WorkTaskTypeVerify,
|
|
Spec: taskSpec,
|
|
CallbackURL: specWithDefaults.CallbackURL,
|
|
MaxRetries: 1, // Verify tasks shouldn't retry by default
|
|
}
|
|
|
|
// Enqueue to work queue
|
|
taskID, err := s.queue.Enqueue(ctx, task)
|
|
if err != nil {
|
|
return "", fmt.Errorf("enqueue verify task: %w", err)
|
|
}
|
|
|
|
s.logger.Info("verify task enqueued",
|
|
"task_id", taskID,
|
|
"project_id", projectID,
|
|
"url", specWithDefaults.URL,
|
|
"viewports", specWithDefaults.Viewports,
|
|
)
|
|
|
|
return taskID, nil
|
|
}
|
|
|
|
// GetCapture retrieves a verify task by ID.
|
|
func (s *VerifyService) GetCapture(ctx context.Context, taskID string) (*domain.WorkTask, error) {
|
|
task, err := s.queue.GetTask(ctx, taskID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Verify it's actually a verify task
|
|
if task.Type != domain.WorkTaskTypeVerify {
|
|
return nil, domain.ErrWorkTaskNotFound
|
|
}
|
|
|
|
return task, nil
|
|
}
|
|
|
|
// ListCaptures returns verify tasks for a project.
|
|
func (s *VerifyService) ListCaptures(ctx context.Context, projectID string, opts domain.WorkListOptions) (*domain.WorkListResult, error) {
|
|
opts.Normalize()
|
|
|
|
// Get all tasks for the project
|
|
result, err := s.queue.ListByProject(ctx, projectID, nil, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Filter to only verify tasks
|
|
var verifyTasks []*domain.WorkTask
|
|
for _, task := range result.Tasks {
|
|
if task.Type == domain.WorkTaskTypeVerify {
|
|
verifyTasks = append(verifyTasks, task)
|
|
}
|
|
}
|
|
|
|
return &domain.WorkListResult{
|
|
Tasks: verifyTasks,
|
|
Total: int64(len(verifyTasks)), // Note: this is filtered total, not DB total
|
|
Limit: result.Limit,
|
|
Offset: result.Offset,
|
|
}, nil
|
|
}
|
|
|
|
// CancelCapture cancels a pending verify task.
|
|
func (s *VerifyService) CancelCapture(ctx context.Context, taskID string) error {
|
|
// Verify it's a verify task first
|
|
task, err := s.queue.GetTask(ctx, taskID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if task.Type != domain.WorkTaskTypeVerify {
|
|
return domain.ErrWorkTaskNotFound
|
|
}
|
|
|
|
return s.queue.Cancel(ctx, taskID)
|
|
}
|