Files
majordomo/provider/llamaswap/video_test.go
T
steveandClaude Opus 5 dbc96898ab
CI / Tidy (pull_request) Successful in 9m25s
CI / Build & Test (pull_request) Successful in 10m28s
fix(videogen): distinct FILENAMES for the two keyframes, not just distinct field names
Caught while writing the receiving end. Distinct multipart field names are not
sufficient: backends stage an uploaded frame under a name derived from the
FILENAME, and our own ComfyUI shim posts to /upload/image with overwrite=true.
Both parts were sending initImageFilename(mime) — literally "frame.png" for
each — so the second upload would have clobbered the first and BOTH keyframe
inputs would have resolved to the same stored image.

The failure mode is the worst kind: a clip pinned at both ends to the same
frame renders cleanly, returns 200, and looks like the feature not working
rather than like a bug. Nothing upstream or downstream would report a fault.

writeImagePart now takes the filename stem (frame / frame_last), and the test
asserts the two arrive under different filenames.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01PLjgrxvHjm1sJgUu9zBPH9
2026-08-08 02:50:43 -04:00

357 lines
12 KiB
Go

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)
}
}
// Both keyframes reach the wire, under DISTINCT field names.
//
// The distinct-name property is the actual contract with the backend shim: the
// two frames could have shared one repeated `input_reference` name, and then
// which is first and which is last would depend on multipart part ORDER — an
// ordering contract invisible in the payload, that nothing would notice
// breaking. Asserting the names is what pins it.
func TestVideoGenerateSendsBothKeyframes(t *testing.T) {
var gotFirst, gotLast []byte
var firstName, lastName string
var sawLastPart bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(32 << 20); err != nil {
t.Errorf("parse form: %v", err)
return
}
if f, hdr, err := r.FormFile("input_reference"); err == nil {
gotFirst, _ = io.ReadAll(f)
firstName = hdr.Filename
f.Close()
}
if f, hdr, err := r.FormFile("input_reference_last"); err == nil {
sawLastPart = true
gotLast, _ = io.ReadAll(f)
lastName = hdr.Filename
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-minimax-h3")
if err != nil {
t.Fatalf("VideoModel: %v", err)
}
first, _ := base64.StdEncoding.DecodeString(onePixelPNG)
last := append(append([]byte{}, first...), 0x00) // distinguishable from first
if _, err := vm.Generate(context.Background(), videogen.Request{
Prompt: "a cat surfing",
InitImage: &videogen.Image{MIME: "image/png", Data: first},
LastImage: &videogen.Image{MIME: "image/png", Data: last},
}); err != nil {
t.Fatalf("Generate: %v", err)
}
if !sawLastPart {
t.Fatal("input_reference_last was not sent — a pinned end frame would be silently dropped")
}
if string(gotFirst) != string(first) {
t.Errorf("input_reference = %d bytes, want %d", len(gotFirst), len(first))
}
if string(gotLast) != string(last) {
t.Errorf("input_reference_last = %d bytes, want %d", len(gotLast), len(last))
}
// The two must not be the same bytes, or a swap/aliasing bug reads as a pass.
if string(gotFirst) == string(gotLast) {
t.Error("both parts carry identical bytes — the frames are being aliased")
}
// DISTINCT FILENAMES, not just distinct field names. Backends stage an
// uploaded frame under a name derived from the filename (our ComfyUI shim
// posts to /upload/image with overwrite=true), so two parts sharing
// "frame.png" would have the second clobber the first and BOTH keyframes
// would resolve to the same stored image — a clip pinned at both ends to
// the same frame, rendering cleanly with nothing reporting a fault.
if firstName == "" || lastName == "" {
t.Fatalf("filenames = %q / %q, want both set", firstName, lastName)
}
if firstName == lastName {
t.Errorf("both parts use filename %q — the second upload would clobber the first", firstName)
}
}
// LastImage alone (no InitImage) is a legitimate request: pin the destination
// and let the model invent the approach. It must not require a first frame.
func TestVideoGenerateLastImageAloneIsAllowed(t *testing.T) {
var sawFirst, sawLast bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(32 << 20); err != nil {
t.Errorf("parse form: %v", err)
return
}
if f, _, err := r.FormFile("input_reference"); err == nil {
sawFirst = true
f.Close()
}
if f, _, err := r.FormFile("input_reference_last"); err == nil {
sawLast = true
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, _ := p.VideoModel("videogen-minimax-h3")
frame, _ := base64.StdEncoding.DecodeString(onePixelPNG)
if _, err := vm.Generate(context.Background(),
videogen.Request{Prompt: "arrive here"},
videogen.WithLastImage(videogen.Image{MIME: "image/png", Data: frame}),
); err != nil {
t.Fatalf("Generate: %v", err)
}
if sawFirst {
t.Error("input_reference sent, want omitted")
}
if !sawLast {
t.Error("input_reference_last omitted, want sent")
}
}
// An empty LastImage is rejected before the request is built, matching
// InitImage's existing contract — a zero-byte frame reaching the backend is a
// confusing upstream error instead of a clear local one.
func TestVideoGenerateRejectsEmptyLastImage(t *testing.T) {
p := New(WithBaseURL("http://unused"))
vm, _ := p.VideoModel("videogen-minimax-h3")
_, err := vm.Generate(context.Background(), videogen.Request{
Prompt: "x",
LastImage: &videogen.Image{MIME: "image/png"},
})
if !errors.Is(err, llm.ErrUnsupported) {
t.Fatalf("err = %v, want llm.ErrUnsupported", err)
}
}