package llamaswap import ( "context" "encoding/base64" "errors" "io" "net/http" "net/http/httptest" "strings" "testing" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" "gitea.stevedudenhoeffer.com/steve/majordomo/videogen" ) func TestVideoGenerate(t *testing.T) { var gotPath, gotContentType string var gotForm map[string]string var gotFrame []byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotContentType = r.Header.Get("Content-Type") if err := r.ParseMultipartForm(32 << 20); err != nil { t.Errorf("parse form: %v", err) return } gotForm = map[string]string{} for k, v := range r.MultipartForm.Value { gotForm[k] = v[0] } if f, _, err := r.FormFile("input_reference"); err == nil { gotFrame, _ = io.ReadAll(f) f.Close() } w.Header().Set("Content-Type", "video/mp4") _, _ = w.Write([]byte("fake-mp4-bytes")) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) vm, err := p.VideoModel("videogen-wan") if err != nil { t.Fatalf("VideoModel: %v", err) } frame, _ := base64.StdEncoding.DecodeString(onePixelPNG) res, err := vm.Generate(context.Background(), videogen.Request{Prompt: "a cat surfing", InitImage: &videogen.Image{MIME: "image/png", Data: frame}}, videogen.WithSize("1280x704"), videogen.WithNumFrames(81), videogen.WithFPS(16), videogen.WithSteps(4), videogen.WithGuidanceScale(1.0), videogen.WithNegativePrompt("blurry"), videogen.WithSeed(42), ) if err != nil { t.Fatalf("Generate: %v", err) } if string(res.Video.Data) != "fake-mp4-bytes" || res.Video.MIME != "video/mp4" { t.Fatalf("video = %d bytes, MIME %q", len(res.Video.Data), res.Video.MIME) } if gotPath != "/v1/videos/sync" { t.Errorf("path = %q", gotPath) } if !strings.HasPrefix(gotContentType, "multipart/form-data") { t.Errorf("content-type = %q", gotContentType) } want := map[string]string{ "model": "videogen-wan", "prompt": "a cat surfing", "negative_prompt": "blurry", "width": "1280", "height": "704", "num_frames": "81", "fps": "16", "num_inference_steps": "4", "guidance_scale": "1", "seed": "42", } for k, v := range want { if gotForm[k] != v { t.Errorf("form[%q] = %q, want %q", k, gotForm[k], v) } } if string(gotFrame) != string(frame) { t.Errorf("input_reference = %d bytes, want %d", len(gotFrame), len(frame)) } } func TestVideoGenerateOmitsUnsetOverrides(t *testing.T) { var gotForm map[string][]string var hadFrame bool srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = r.ParseMultipartForm(32 << 20) gotForm = r.MultipartForm.Value _, _, err := r.FormFile("input_reference") hadFrame = err == nil w.Header().Set("Content-Type", "video/mp4") _, _ = w.Write([]byte("v")) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) vm, _ := p.VideoModel("videogen-wan") if _, err := vm.Generate(context.Background(), videogen.Request{Prompt: "x"}); err != nil { t.Fatalf("Generate: %v", err) } for _, k := range []string{"negative_prompt", "width", "height", "num_frames", "fps", "num_inference_steps", "guidance_scale", "seed"} { if _, ok := gotForm[k]; ok { t.Errorf("form field %q sent, want omitted", k) } } if hadFrame { t.Error("input_reference sent, want omitted") } } func TestVideoGenerateValidation(t *testing.T) { p := New(WithBaseURL("http://unused.invalid")) vm, _ := p.VideoModel("videogen-wan") cases := []struct { name string req videogen.Request }{ {"empty prompt", videogen.Request{}}, {"negative frames", videogen.Request{Prompt: "x", NumFrames: -1}}, {"negative fps", videogen.Request{Prompt: "x", FPS: -1}}, {"empty init image", videogen.Request{Prompt: "x", InitImage: &videogen.Image{}}}, {"bad size", videogen.Request{Prompt: "x", Size: "banana"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := vm.Generate(context.Background(), tc.req) if !errors.Is(err, llm.ErrUnsupported) { t.Fatalf("err = %v, want ErrUnsupported", err) } }) } } func TestVideoGenerateUpstreamError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, `{"error":{"message":"boom"}}`, http.StatusInternalServerError) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) vm, _ := p.VideoModel("videogen-wan") _, err := vm.Generate(context.Background(), videogen.Request{Prompt: "x"}) var apiErr *llm.APIError if !errors.As(err, &apiErr) { t.Fatalf("err = %v, want *llm.APIError", err) } } func TestVideoGenerateEmptyResponse(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "video/mp4") })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) vm, _ := p.VideoModel("videogen-wan") _, err := vm.Generate(context.Background(), videogen.Request{Prompt: "x"}) var apiErr *llm.APIError if !errors.As(err, &apiErr) { t.Fatalf("err = %v, want *llm.APIError for empty body", err) } } func TestVideoModelRequiresBaseURL(t *testing.T) { p := New() if _, err := p.VideoModel("videogen-wan"); err == nil { t.Fatal("VideoModel with no base URL should error") } } func TestVideoMIME(t *testing.T) { // A minimal ISO-BMFF prefix http.DetectContentType sniffs as video/mp4 // (the "mp4" brand prefix must appear inside the declared ftyp box). mp4Magic := append([]byte{0, 0, 0, 20}, []byte("ftypmp42\x00\x00\x00\x00mp42")...) cases := []struct { contentType string data []byte want string }{ {"video/webm", []byte("x"), "video/webm"}, {"video/mp4; charset=binary", []byte("x"), "video/mp4"}, {"application/octet-stream", mp4Magic, "video/mp4"}, // Neither declared nor sniffable as video → "" (Generate errors). {"application/octet-stream", []byte(`{"id":"job-1"}`), ""}, {"", []byte("x"), ""}, } for _, tc := range cases { if got := videoMIME(tc.contentType, tc.data); got != tc.want { t.Errorf("videoMIME(%q, %q) = %q, want %q", tc.contentType, tc.data, got, tc.want) } } } func TestVideoGenerateNonVideoBodyErrors(t *testing.T) { // A stock async /v1/videos handler mounted at the sync path (or an HTML // error page behind a proxy) answers 200 with a non-video body — that // must be an error, never a "successful" garbage clip. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"job-1","status":"queued"}`)) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) vm, _ := p.VideoModel("videogen-wan") _, err := vm.Generate(context.Background(), videogen.Request{Prompt: "x"}) var apiErr *llm.APIError if !errors.As(err, &apiErr) { t.Fatalf("err = %v, want *llm.APIError for non-video 2xx body", err) } if !strings.Contains(apiErr.Message, "not a video") { t.Errorf("message = %q, want mention of non-video body", apiErr.Message) } }