Artifact and media metadata were previously reconstructed from job result refs and JSON sidecar files. This change regularizes metadata into SQLite, keeps filesystem blobs in place, and preserves backward compatibility via lazy fallback and backfill from existing job refs and JSON metadata. Constraint: Blob storage remains on the local filesystem in this phase Rejected: Migrate blobs into SQLite | larger scope and worse operational profile for current media sizes Rejected: Hard cutover without fallback | unsafe for historical data already on the test server Confidence: medium Scope-risk: moderate Directive: Treat SQLite as the metadata source of truth; JSON sidecars are compatibility fallback only Tested: go test ./...; deployed to test server 101.42.99.35; verified /v1/artifacts, /v1/artifacts/:id, signed media URL 200, unsigned content 401 Not-tested: Full historical backfill sweep over all existing artifact rows under production-sized data volume
625 lines
19 KiB
Go
625 lines
19 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestSubmitImageToVideoTaskUsesMultipartInputReference(t *testing.T) {
|
|
var (
|
|
gotModel string
|
|
gotPrompt string
|
|
gotSeconds string
|
|
gotSize string
|
|
gotBytes int
|
|
)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/videos" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if err := r.ParseMultipartForm(4 << 20); err != nil {
|
|
t.Fatalf("parse multipart form: %v", err)
|
|
}
|
|
gotModel = r.FormValue("model")
|
|
gotPrompt = r.FormValue("prompt")
|
|
gotSeconds = r.FormValue("seconds")
|
|
gotSize = r.FormValue("size")
|
|
file, _, err := r.FormFile("input_reference")
|
|
if err != nil {
|
|
t.Fatalf("missing input_reference file: %v", err)
|
|
}
|
|
defer file.Close()
|
|
content, err := io.ReadAll(file)
|
|
if err != nil {
|
|
t.Fatalf("read uploaded file: %v", err)
|
|
}
|
|
gotBytes = len(content)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"id":"task_submit_123","status":"queued"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := newNewAPIClient(Config{NewAPIBaseURL: srv.URL})
|
|
taskID, err := client.submitImageToVideoTask(context.Background(), "sk-test", "sora-2", map[string]any{
|
|
"prompt": "animate the scene gently",
|
|
"duration_s": 5,
|
|
"aspect_ratio": "16:9",
|
|
}, imageEditReference{
|
|
Filename: "reference.png",
|
|
ContentType: "image/png",
|
|
Content: tinyPNG(t),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("submitImageToVideoTask: %v", err)
|
|
}
|
|
|
|
if taskID != "task_submit_123" {
|
|
t.Fatalf("expected task id task_submit_123, got %q", taskID)
|
|
}
|
|
if gotModel != "sora-2" {
|
|
t.Fatalf("expected model sora-2, got %q", gotModel)
|
|
}
|
|
if gotPrompt != "animate the scene gently" {
|
|
t.Fatalf("expected prompt to round-trip, got %q", gotPrompt)
|
|
}
|
|
if gotSeconds != "5" {
|
|
t.Fatalf("expected seconds 5, got %q", gotSeconds)
|
|
}
|
|
if gotSize != "1280x720" {
|
|
t.Fatalf("expected 16:9 to map to 1280x720, got %q", gotSize)
|
|
}
|
|
if gotBytes == 0 {
|
|
t.Fatal("expected uploaded reference bytes")
|
|
}
|
|
}
|
|
|
|
func TestSubmitImageToVideoTaskUsesURLImagesForViduModels(t *testing.T) {
|
|
var gotBody map[string]any
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/videos" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if got := r.Header.Get("Content-Type"); got != "application/json" {
|
|
t.Fatalf("expected application/json content type, got %q", got)
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
|
t.Fatalf("decode request body: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"id":"task_vidu_url_123","status":"queued"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := newNewAPIClient(Config{NewAPIBaseURL: srv.URL})
|
|
taskID, err := client.submitImageToVideoTask(context.Background(), "sk-test", "viduq2-pro-fast", map[string]any{
|
|
"prompt": "animate this still gently",
|
|
"duration_s": 5,
|
|
"aspect_ratio": "9:16",
|
|
}, imageEditReference{
|
|
Filename: "reference.png",
|
|
ContentType: "image/png",
|
|
URL: "https://media.popi.test/m/demo/reference.png",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("submitImageToVideoTask: %v", err)
|
|
}
|
|
|
|
if taskID != "task_vidu_url_123" {
|
|
t.Fatalf("expected task id task_vidu_url_123, got %q", taskID)
|
|
}
|
|
if gotBody["model"] != "viduq2-pro-fast" {
|
|
t.Fatalf("unexpected model: %#v", gotBody["model"])
|
|
}
|
|
images, ok := gotBody["images"].([]any)
|
|
if !ok || len(images) != 1 || images[0] != "https://media.popi.test/m/demo/reference.png" {
|
|
t.Fatalf("expected one image url, got %#v", gotBody["images"])
|
|
}
|
|
if gotBody["duration"] != float64(5) {
|
|
t.Fatalf("unexpected duration: %#v", gotBody["duration"])
|
|
}
|
|
metadata, ok := gotBody["metadata"].(map[string]any)
|
|
if !ok || metadata["aspect_ratio"] != "9:16" {
|
|
t.Fatalf("unexpected metadata: %#v", gotBody["metadata"])
|
|
}
|
|
}
|
|
|
|
func TestSubmitImageToVideoTaskUsesViduDefaultDurationWhenUnset(t *testing.T) {
|
|
var gotBody map[string]any
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
|
t.Fatalf("decode request body: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"id":"task_vidu_default_duration","status":"queued"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := newNewAPIClient(Config{NewAPIBaseURL: srv.URL})
|
|
taskID, err := client.submitImageToVideoTask(context.Background(), "sk-test", "viduq3-turbo", map[string]any{
|
|
"prompt": "animate this still gently",
|
|
}, imageEditReference{
|
|
URL: "https://media.popi.test/m/demo/reference.png",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("submitImageToVideoTask: %v", err)
|
|
}
|
|
if taskID != "task_vidu_default_duration" {
|
|
t.Fatalf("unexpected task id: %q", taskID)
|
|
}
|
|
if gotBody["duration"] != float64(5) {
|
|
t.Fatalf("expected default vidu duration 5, got %#v", gotBody["duration"])
|
|
}
|
|
}
|
|
|
|
func TestSubmitMiniMaxVideoTaskUsesVideoGenerationsEndpoint(t *testing.T) {
|
|
var gotPath string
|
|
var gotBody map[string]any
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
|
t.Fatalf("decode request body: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"task_id":"task_minimax_video_123","status":"submitted"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := newNewAPIClient(Config{NewAPIBaseURL: srv.URL})
|
|
taskID, err := client.submitMiniMaxVideoTask(context.Background(), "sk-test", "I2V-01", map[string]any{
|
|
"prompt": "animate this portrait gently",
|
|
"size": "720x1280",
|
|
}, []imageEditReference{{
|
|
URL: "https://example.com/reference.png",
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("submitMiniMaxVideoTask: %v", err)
|
|
}
|
|
if taskID != "task_minimax_video_123" {
|
|
t.Fatalf("unexpected task id: %q", taskID)
|
|
}
|
|
if gotPath != "/v1/video/generations" {
|
|
t.Fatalf("expected /v1/video/generations, got %q", gotPath)
|
|
}
|
|
if gotBody["model"] != "I2V-01" {
|
|
t.Fatalf("unexpected model: %#v", gotBody["model"])
|
|
}
|
|
images, ok := gotBody["images"].([]any)
|
|
if !ok || len(images) != 1 || images[0] != "https://example.com/reference.png" {
|
|
t.Fatalf("unexpected images payload: %#v", gotBody["images"])
|
|
}
|
|
if gotBody["size"] != "720P" {
|
|
t.Fatalf("expected 720P resolution, got %#v", gotBody["size"])
|
|
}
|
|
}
|
|
|
|
func TestSubmitMiniMaxVideoTaskKeepsS2VImagesArray(t *testing.T) {
|
|
var gotBody map[string]any
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
|
t.Fatalf("decode request body: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"task_id":"task_s2v_123","status":"submitted"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := newNewAPIClient(Config{NewAPIBaseURL: srv.URL})
|
|
taskID, err := client.submitMiniMaxVideoTask(context.Background(), "sk-test", "S2V-01", map[string]any{
|
|
"prompt": "animate the subject gently",
|
|
}, []imageEditReference{{
|
|
URL: "https://example.com/reference.png",
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("submitMiniMaxVideoTask: %v", err)
|
|
}
|
|
if taskID != "task_s2v_123" {
|
|
t.Fatalf("unexpected task id: %q", taskID)
|
|
}
|
|
images, ok := gotBody["images"].([]any)
|
|
if !ok || len(images) != 1 || images[0] != "https://example.com/reference.png" {
|
|
t.Fatalf("unexpected images payload for S2V: %#v", gotBody["images"])
|
|
}
|
|
}
|
|
|
|
func TestResolveVideoReferencesSupportsImagesArray(t *testing.T) {
|
|
cfg := Config{
|
|
SQLitePath: filepath.Join(t.TempDir(), "popiart.db"),
|
|
SkillhubDir: makeEmptySkillhub(t),
|
|
SessionSecret: "test-secret",
|
|
}
|
|
server, err := NewWithConfig(cfg)
|
|
if err != nil {
|
|
t.Fatalf("NewWithConfig: %v", err)
|
|
}
|
|
|
|
imageBytes := tinyPNG(t)
|
|
refSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = w.Write(imageBytes)
|
|
}))
|
|
defer refSrv.Close()
|
|
|
|
refs, err := server.resolveVideoReferences(context.Background(), &job{
|
|
UserID: "user_test",
|
|
UpstreamKey: "sk-upstream",
|
|
}, map[string]any{
|
|
"images": []any{refSrv.URL + "/a.png", refSrv.URL + "/b.png"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("resolveVideoReferences: %v", err)
|
|
}
|
|
if len(refs) != 2 {
|
|
t.Fatalf("expected 2 refs, got %#v", refs)
|
|
}
|
|
}
|
|
|
|
func TestResolveVideoReferencesAcceptsCanonicalImageURL(t *testing.T) {
|
|
cfg := Config{
|
|
SQLitePath: filepath.Join(t.TempDir(), "popiart.db"),
|
|
SkillhubDir: makeEmptySkillhub(t),
|
|
SessionSecret: "test-secret",
|
|
}
|
|
server, err := NewWithConfig(cfg)
|
|
if err != nil {
|
|
t.Fatalf("NewWithConfig: %v", err)
|
|
}
|
|
|
|
imageBytes := tinyPNG(t)
|
|
refSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = w.Write(imageBytes)
|
|
}))
|
|
defer refSrv.Close()
|
|
|
|
refs, err := server.resolveVideoReferences(context.Background(), &job{
|
|
UserID: "user_test",
|
|
UpstreamKey: "sk-upstream",
|
|
}, map[string]any{
|
|
"image_url": refSrv.URL + "/single.png",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("resolveVideoReferences canonical image_url: %v", err)
|
|
}
|
|
if len(refs) != 1 {
|
|
t.Fatalf("expected 1 ref, got %#v", refs)
|
|
}
|
|
if refs[0].URL != refSrv.URL+"/single.png" {
|
|
t.Fatalf("expected URL to round-trip, got %q", refs[0].URL)
|
|
}
|
|
if refs[0].ContentType != "image/png" {
|
|
t.Fatalf("expected image/png, got %q", refs[0].ContentType)
|
|
}
|
|
if !bytes.Equal(refs[0].Content, imageBytes) {
|
|
t.Fatal("expected canonical image_url bytes to match")
|
|
}
|
|
}
|
|
|
|
func TestResolveVideoReferencesAcceptsSignedSameOriginMediaURL(t *testing.T) {
|
|
cfg := Config{
|
|
SQLitePath: filepath.Join(t.TempDir(), "popiart.db"),
|
|
SkillhubDir: makeEmptySkillhub(t),
|
|
SessionSecret: "test-secret",
|
|
}
|
|
server, err := NewWithConfig(cfg)
|
|
if err != nil {
|
|
t.Fatalf("NewWithConfig: %v", err)
|
|
}
|
|
srv := httptest.NewServer(server.Handler())
|
|
defer srv.Close()
|
|
server.cfg.PublicBaseURL = srv.URL
|
|
|
|
sessionToken, _, ok, err := server.store.createSession("sk-video-same-origin-user")
|
|
if err != nil {
|
|
t.Fatalf("createSession: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("expected session creation to succeed")
|
|
}
|
|
current, exists, err := server.store.session(sessionToken)
|
|
if err != nil {
|
|
t.Fatalf("load session: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatal("expected stored session")
|
|
}
|
|
|
|
imageBytes := tinyPNG(t)
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
part, err := writer.CreateFormFile("file", "video-reference.png")
|
|
if err != nil {
|
|
t.Fatalf("create form file: %v", err)
|
|
}
|
|
if _, err := part.Write(imageBytes); err != nil {
|
|
t.Fatalf("write form file: %v", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("close multipart writer: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/media/upload", &body)
|
|
req.Header.Set("Authorization", "Bearer "+sessionToken)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
rec := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("expected media upload 201, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var envelope struct {
|
|
OK bool `json:"ok"`
|
|
Data media `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("decode media upload response: %v body=%s", err, rec.Body.String())
|
|
}
|
|
if !envelope.OK || envelope.Data.URL == "" {
|
|
t.Fatalf("expected signed media url, got %s", rec.Body.String())
|
|
}
|
|
|
|
refs, err := server.resolveVideoReferences(context.Background(), &job{
|
|
UserID: current.User.ID,
|
|
SessionID: current.Token,
|
|
UpstreamKey: current.UpstreamKey,
|
|
}, map[string]any{
|
|
"image_url": envelope.Data.URL,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("resolveVideoReferences same-origin signed media url: %v", err)
|
|
}
|
|
if len(refs) != 1 {
|
|
t.Fatalf("expected 1 ref, got %#v", refs)
|
|
}
|
|
if refs[0].URL != envelope.Data.URL {
|
|
t.Fatalf("expected signed URL to round-trip, got %q", refs[0].URL)
|
|
}
|
|
if refs[0].ContentType != "image/png" {
|
|
t.Fatalf("expected image/png, got %q", refs[0].ContentType)
|
|
}
|
|
if !bytes.Equal(refs[0].Content, imageBytes) {
|
|
t.Fatal("expected same-origin signed media bytes to match")
|
|
}
|
|
}
|
|
|
|
func TestFetchVideoTaskFallsBackToGenericTaskEnvelope(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/v1/videos/task_fallback_123":
|
|
http.NotFound(w, r)
|
|
case "/v1/video/generations/task_fallback_123":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"code":"success","message":"","data":{"task_id":"task_fallback_123","status":"SUCCESS","result_url":"http://example.com/out.mp4","progress":"100%"}}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := newNewAPIClient(Config{NewAPIBaseURL: srv.URL})
|
|
result, err := client.fetchVideoTask(context.Background(), "sk-test", "task_fallback_123")
|
|
if err != nil {
|
|
t.Fatalf("fetchVideoTask: %v", err)
|
|
}
|
|
|
|
if result.TaskID != "task_fallback_123" {
|
|
t.Fatalf("expected task id task_fallback_123, got %q", result.TaskID)
|
|
}
|
|
if result.Status != "completed" {
|
|
t.Fatalf("expected normalized completed status, got %q", result.Status)
|
|
}
|
|
if result.URL != "http://example.com/out.mp4" {
|
|
t.Fatalf("expected result url to round-trip, got %q", result.URL)
|
|
}
|
|
}
|
|
|
|
func TestExecuteImageToVideoJobCompletesAndArtifactCanBeRead(t *testing.T) {
|
|
refBytes := tinyPNG(t)
|
|
videoBytes := []byte("not-a-real-mp4-but-good-enough-for-streaming")
|
|
var (
|
|
fetchCalls int32
|
|
contentAuth string
|
|
)
|
|
|
|
var localProxyURL string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/input.png":
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = w.Write(refBytes)
|
|
case "/v1/videos":
|
|
if err := r.ParseMultipartForm(4 << 20); err != nil {
|
|
t.Fatalf("parse multipart form: %v", err)
|
|
}
|
|
if got := r.FormValue("model"); got != "sora-2" {
|
|
t.Fatalf("expected model sora-2, got %q", got)
|
|
}
|
|
file, _, err := r.FormFile("input_reference")
|
|
if err != nil {
|
|
t.Fatalf("missing input_reference: %v", err)
|
|
}
|
|
defer file.Close()
|
|
if _, err := io.ReadAll(file); err != nil {
|
|
t.Fatalf("read input_reference: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"id":"task_video_123","status":"queued"}`))
|
|
case "/v1/videos/task_video_123":
|
|
call := atomic.AddInt32(&fetchCalls, 1)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if call == 1 {
|
|
_, _ = w.Write([]byte(`{"id":"task_video_123","status":"in_progress","progress":30}`))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(`{"id":"task_video_123","status":"completed","progress":100,"metadata":{"url":"` + localProxyURL + `","format":"mp4"}}`))
|
|
case "/v1/videos/task_video_123/content":
|
|
contentAuth = r.Header.Get("Authorization")
|
|
w.Header().Set("Content-Type", "video/mp4")
|
|
_, _ = w.Write(videoBytes)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
localProxyURL = strings.Replace(srv.URL, "127.0.0.1", "localhost", 1) + "/v1/videos/task_video_123/content"
|
|
|
|
cfg := Config{
|
|
NewAPIBaseURL: srv.URL,
|
|
SQLitePath: filepath.Join(t.TempDir(), "popiart.db"),
|
|
SkillhubDir: makeEmptySkillhub(t),
|
|
SessionSecret: "test-secret",
|
|
}
|
|
server, err := NewWithConfig(cfg)
|
|
if err != nil {
|
|
t.Fatalf("NewWithConfig: %v", err)
|
|
}
|
|
server.cfg.PublicBaseURL = "http://127.0.0.1:8080"
|
|
|
|
token, _, ok, err := server.store.createSession("sk-test-upstream")
|
|
if err != nil {
|
|
t.Fatalf("createSession: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("expected session creation to succeed")
|
|
}
|
|
current, exists, err := server.store.session(token)
|
|
if err != nil {
|
|
t.Fatalf("load session: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatal("expected stored session")
|
|
}
|
|
|
|
record, _, err := server.store.createJob(
|
|
"popiskill-video-image2video-basic-v1",
|
|
"video.image2video",
|
|
"sora-2",
|
|
routeExecMode("video.image2video"),
|
|
map[string]any{
|
|
"image_url": srv.URL + "/input.png",
|
|
"motion_prompt": "the camera slowly pushes in",
|
|
"duration_s": 4,
|
|
"aspect_ratio": "9:16",
|
|
},
|
|
"",
|
|
"normal",
|
|
"",
|
|
current,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("createJob: %v", err)
|
|
}
|
|
|
|
server.executeImageToVideoJob(record)
|
|
|
|
done, exists, err := server.store.getJob(current.User.ID, record.JobID)
|
|
if err != nil {
|
|
t.Fatalf("getJob: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatal("expected completed job")
|
|
}
|
|
if done.Status != "done" {
|
|
t.Fatalf("expected job status done, got %q", done.Status)
|
|
}
|
|
if done.NewAPITaskID != "task_video_123" {
|
|
t.Fatalf("expected persisted task id task_video_123, got %q", done.NewAPITaskID)
|
|
}
|
|
if len(done.ArtifactIDs) != 1 {
|
|
t.Fatalf("expected one artifact, got %d", len(done.ArtifactIDs))
|
|
}
|
|
|
|
item, ref, exists, err := server.store.artifactRef(current.User.ID, done.ArtifactIDs[0])
|
|
if err != nil {
|
|
t.Fatalf("artifactRef: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatal("expected artifact ref")
|
|
}
|
|
if item.ContentType != "video/mp4" {
|
|
t.Fatalf("expected video/mp4 artifact, got %q", item.ContentType)
|
|
}
|
|
if item.MediaID == "" {
|
|
t.Fatalf("expected media id on persisted artifact, got %#v", item)
|
|
}
|
|
if item.URL == "" {
|
|
t.Fatalf("expected stable url on persisted artifact, got %#v", item)
|
|
}
|
|
if item.StorageStatus != "ready" {
|
|
t.Fatalf("expected ready storage status, got %#v", item.StorageStatus)
|
|
}
|
|
if !strings.HasSuffix(item.Filename, ".mp4") {
|
|
t.Fatalf("expected .mp4 filename, got %q", item.Filename)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
contentType, _, reader, err := server.newapi.openResultRef(ctx, current.UpstreamKey, ref)
|
|
if err != nil {
|
|
t.Fatalf("openResultRef: %v", err)
|
|
}
|
|
defer reader.Close()
|
|
content, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatalf("read video content: %v", err)
|
|
}
|
|
if contentType != "video/mp4" {
|
|
t.Fatalf("expected fetched content type video/mp4, got %q", contentType)
|
|
}
|
|
if string(content) != string(videoBytes) {
|
|
t.Fatalf("unexpected streamed content: %q", string(content))
|
|
}
|
|
if contentAuth != "Bearer sk-test-upstream" {
|
|
t.Fatalf("expected auth header to be forwarded to proxy content URL, got %q", contentAuth)
|
|
}
|
|
}
|
|
|
|
func TestResolveVideoSizeUsesViduResolutionLabels(t *testing.T) {
|
|
if got := resolveVideoSize("viduq2", map[string]any{"aspect_ratio": "16:9"}, imageEditReference{}); got != "720p" {
|
|
t.Fatalf("expected vidu 16:9 to map to 720p, got %q", got)
|
|
}
|
|
if got := resolveVideoSize("viduq2", map[string]any{"size": "1080p"}, imageEditReference{}); got != "1080p" {
|
|
t.Fatalf("expected explicit vidu resolution to be preserved, got %q", got)
|
|
}
|
|
if got := resolveVideoSize("sora-2", map[string]any{"aspect_ratio": "16:9"}, imageEditReference{}); got != "1280x720" {
|
|
t.Fatalf("expected sora size to remain pixel dimensions, got %q", got)
|
|
}
|
|
}
|
|
|
|
func tinyPNG(t *testing.T) []byte {
|
|
t.Helper()
|
|
data, err := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH8kAAAAASUVORK5CYII=")
|
|
if err != nil {
|
|
t.Fatalf("decode tiny png: %v", err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func makeEmptySkillhub(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "index.json"), []byte(`{"version":1,"skills":[]}`), 0o644); err != nil {
|
|
t.Fatalf("write skillhub index: %v", err)
|
|
}
|
|
return dir
|
|
}
|