package llamaswap import ( "context" "encoding/base64" "encoding/json" "errors" "net/http" "net/http/httptest" "reflect" "testing" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" "gitea.stevedudenhoeffer.com/steve/majordomo/videogen" ) func TestSubmitChain(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 _ = json.NewDecoder(r.Body).Decode(&gotBody) _, _ = w.Write([]byte(`{"job_id":"job-123"}`)) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) ch, err := p.ChainerModel("videoutils") if err != nil { t.Fatalf("ChainerModel: %v", err) } jobID, err := ch.SubmitChain(context.Background(), videogen.ChainRequest{ Segments: []videogen.ChainSegment{ {Prompt: "a cat walks in", Seconds: 5}, {Prompt: "the cat sits down", Seconds: 5}, }, InitImage: []byte("IMG"), SmoothJoins: true, Size: "1280x704", }) if err != nil { t.Fatalf("SubmitChain: %v", err) } if jobID != "job-123" { t.Errorf("jobID = %q", jobID) } if gotPath != "/upstream/videoutils/v1/video/chain" { t.Errorf("path = %q", gotPath) } segments, _ := gotBody["segments"].([]any) if len(segments) != 2 { t.Fatalf("segments = %v", gotBody["segments"]) } first, _ := segments[0].(map[string]any) if first["prompt"] != "a cat walks in" || first["seconds"] != 5.0 { t.Errorf("segment[0] = %v", first) } if gotBody["init_image_b64"] != base64.StdEncoding.EncodeToString([]byte("IMG")) { t.Errorf("init_image_b64 = %v", gotBody["init_image_b64"]) } if gotBody["smooth_joins"] != true || gotBody["size"] != "1280x704" { t.Errorf("smooth_joins/size = %v/%v", gotBody["smooth_joins"], gotBody["size"]) } } func TestSubmitChainOmitsUnsetFields(t *testing.T) { var gotBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewDecoder(r.Body).Decode(&gotBody) _, _ = w.Write([]byte(`{"data":{"job_id":"job-9"}}`)) // data-wrapped envelope })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) ch, _ := p.ChainerModel("videoutils") jobID, err := ch.SubmitChain(context.Background(), videogen.ChainRequest{ Segments: []videogen.ChainSegment{{Prompt: "a dog"}}, }) if err != nil { t.Fatalf("SubmitChain: %v", err) } if jobID != "job-9" { t.Errorf("jobID = %q, want data-wrapped id", jobID) } for _, k := range []string{"init_image_b64", "smooth_joins", "size"} { if v, ok := gotBody[k]; ok { t.Errorf("unset request sent %q = %v, want omitted", k, v) } } seg, _ := gotBody["segments"].([]any) if first, _ := seg[0].(map[string]any); first == nil { t.Fatalf("segments = %v", gotBody["segments"]) } else if _, ok := first["seconds"]; ok { t.Error("zero seconds sent; want omitted") } } func TestSubmitChainRejectsBadArgs(t *testing.T) { p := New(WithBaseURL("http://unused")) ch, _ := p.ChainerModel("videoutils") if _, err := ch.SubmitChain(context.Background(), videogen.ChainRequest{}); !errors.Is(err, llm.ErrUnsupported) { t.Errorf("no segments: err = %v, want ErrUnsupported", err) } if _, err := ch.SubmitChain(context.Background(), videogen.ChainRequest{ Segments: []videogen.ChainSegment{{Prompt: " "}}, }); !errors.Is(err, llm.ErrUnsupported) { t.Errorf("empty prompt: err = %v, want ErrUnsupported", err) } if _, err := ch.SubmitChain(context.Background(), videogen.ChainRequest{ Segments: []videogen.ChainSegment{{Prompt: "x", Seconds: -1}}, }); !errors.Is(err, llm.ErrUnsupported) { t.Errorf("negative seconds: err = %v, want ErrUnsupported", err) } } func TestSubmitChainRejectsMissingJobID(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"ok":true}`)) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) ch, _ := p.ChainerModel("videoutils") _, err := ch.SubmitChain(context.Background(), videogen.ChainRequest{ Segments: []videogen.ChainSegment{{Prompt: "x"}}, }) var apiErr *llm.APIError if !errors.As(err, &apiErr) { t.Fatalf("err = %v, want APIError for missing job_id", err) } } func TestChainStatus(t *testing.T) { var gotPath string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path _, _ = w.Write([]byte(`{"status":"running","segment":2,"total":3,"segments":["seg-0"]}`)) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) ch, _ := p.ChainerModel("videoutils") job, err := ch.ChainStatus(context.Background(), "job-123") if err != nil { t.Fatalf("ChainStatus: %v", err) } if gotPath != "/upstream/videoutils/v1/jobs/job-123" { t.Errorf("path = %q", gotPath) } if job.Status != "running" || job.Segment != 2 || job.Total != 3 { t.Errorf("job = %+v", job) } if !reflect.DeepEqual(job.SegmentIDs, []string{"seg-0"}) { t.Errorf("SegmentIDs = %v", job.SegmentIDs) } if job.Raw == nil { t.Error("Raw = nil, want raw payload") } } func TestChainStatusToleratesObjectSegments(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"status":"done","segment":2,"total":2,"segments":[{"id":"seg-0"},{"segment_id":"seg-1"}]}`)) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) ch, _ := p.ChainerModel("videoutils") job, err := ch.ChainStatus(context.Background(), "job-123") if err != nil { t.Fatalf("ChainStatus: %v", err) } if !reflect.DeepEqual(job.SegmentIDs, []string{"seg-0", "seg-1"}) { t.Errorf("SegmentIDs = %v", job.SegmentIDs) } } func TestChainStatusRejectsStatuslessPayload(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"detail":"no such job"}`)) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) ch, _ := p.ChainerModel("videoutils") _, err := ch.ChainStatus(context.Background(), "job-123") var apiErr *llm.APIError if !errors.As(err, &apiErr) { t.Fatalf("err = %v, want APIError for statusless payload", err) } } func TestChainJobPathRejectsHostileIDs(t *testing.T) { p := New(WithBaseURL("http://unused")) ch, _ := p.ChainerModel("videoutils") for _, bad := range []string{"", "a/b", "a?b", "a#b", "..", "a..b"} { if _, err := ch.ChainStatus(context.Background(), bad); err == nil { t.Errorf("ChainStatus(%q) succeeded; want error", bad) } if _, err := ch.ChainResult(context.Background(), bad); err == nil { t.Errorf("ChainResult(%q) succeeded; want error", bad) } } } func TestChainResult(t *testing.T) { var gotPath string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path w.Header().Set("Content-Type", "video/mp4") _, _ = w.Write(mp4Fixture()) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) ch, _ := p.ChainerModel("videoutils") res, err := ch.ChainResult(context.Background(), "job-123") if err != nil { t.Fatalf("ChainResult: %v", err) } if gotPath != "/upstream/videoutils/v1/jobs/job-123/result" { t.Errorf("path = %q", gotPath) } if res.Video.MIME != "video/mp4" || len(res.Video.Data) == 0 { t.Fatalf("video = %q/%d bytes", res.Video.MIME, len(res.Video.Data)) } } func TestChainSegmentResult(t *testing.T) { var gotPath string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path w.Header().Set("Content-Type", "video/mp4") _, _ = w.Write(mp4Fixture()) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) ch, _ := p.ChainerModel("videoutils") res, err := ch.ChainSegmentResult(context.Background(), "job-123", 1) if err != nil { t.Fatalf("ChainSegmentResult: %v", err) } if gotPath != "/upstream/videoutils/v1/jobs/job-123/segments/1" { t.Errorf("path = %q", gotPath) } if res.Video.MIME != "video/mp4" { t.Fatalf("video = %q", res.Video.MIME) } if _, err := ch.ChainSegmentResult(context.Background(), "job-123", -1); !errors.Is(err, llm.ErrUnsupported) { t.Errorf("negative segment: err = %v, want ErrUnsupported", err) } } func TestChainResultRejectsNonVideoResponse(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"status":"running"}`)) // a status page, not the clip })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) ch, _ := p.ChainerModel("videoutils") _, err := ch.ChainResult(context.Background(), "job-123") var apiErr *llm.APIError if !errors.As(err, &apiErr) { t.Fatalf("err = %v, want APIError for non-video body", err) } } func TestChainSurfacesAPIError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"error":{"message":"job not found"}}`)) })) defer srv.Close() p := New(WithBaseURL(srv.URL), WithHTTPClient(srv.Client())) ch, _ := p.ChainerModel("videoutils") _, err := ch.ChainStatus(context.Background(), "job-void") var apiErr *llm.APIError if !errors.As(err, &apiErr) { t.Fatalf("err = %T %v, want *llm.APIError", err, err) } if apiErr.Status != http.StatusNotFound || apiErr.Message != "job not found" || apiErr.Model != "videoutils" { t.Errorf("apiErr = %+v", apiErr) } }