@ -140,6 +140,53 @@ func TestSubmitImageToVideoTaskUsesURLImagesForViduModels(t *testing.T) {
}
}
func TestSubmitImageToVideoTaskWithReferencesUsesGatewayForStartEndFrames ( t * testing . T ) {
var (
gotPath string
gotBody map [ string ] any
)
srv := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
gotPath = r . URL . Path
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_start_end_123","status":"queued"} ` ) )
} ) )
defer srv . Close ( )
client := newNewAPIClient ( Config { NewAPIBaseURL : srv . URL } )
taskID , err := client . submitImageToVideoTaskWithReferences ( context . Background ( ) , "sk-test" , "viduq2-pro" , map [ string ] any {
"prompt" : "transition smoothly from first to last frame" ,
"duration_s" : 5 ,
"aspect_ratio" : "16:9" ,
} , [ ] imageEditReference {
{ URL : "https://example.com/first.png" } ,
{ URL : "https://example.com/last.png" } ,
} )
if err != nil {
t . Fatalf ( "submitImageToVideoTaskWithReferences: %v" , err )
}
if taskID != "task_start_end_123" {
t . Fatalf ( "expected task id task_start_end_123, got %q" , taskID )
}
if gotPath != "/v1/video/generations" {
t . Fatalf ( "expected /v1/video/generations, got %q" , gotPath )
}
images , ok := gotBody [ "images" ] . ( [ ] any )
if ! ok || len ( images ) != 2 || images [ 0 ] != "https://example.com/first.png" || images [ 1 ] != "https://example.com/last.png" {
t . Fatalf ( "unexpected images payload: %#v" , gotBody [ "images" ] )
}
metadata , ok := gotBody [ "metadata" ] . ( map [ string ] any )
if ! ok || metadata [ "action" ] != "firstTailGenerate" || metadata [ "aspect_ratio" ] != "16:9" {
t . Fatalf ( "unexpected metadata: %#v" , gotBody [ "metadata" ] )
}
}
func TestSubmitImageToVideoTaskUsesViduDefaultDurationWhenUnset ( t * testing . T ) {
var gotBody map [ string ] any
@ -273,6 +320,57 @@ func TestResolveVideoReferencesSupportsImagesArray(t *testing.T) {
}
}
func TestResolveVideoReferencesSupportsSourceAndLastFrameArtifacts ( 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-artifact-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" )
}
first := uploadTestMediaArtifact ( t , server , sessionToken , "first.png" )
last := uploadTestMediaArtifact ( t , server , sessionToken , "last.png" )
refs , err := server . resolveVideoReferences ( context . Background ( ) , & job {
UserID : current . User . ID ,
SessionID : current . Token ,
UpstreamKey : current . UpstreamKey ,
} , map [ string ] any {
"source_artifact_id" : first . ID ,
"last_frame_artifact_id" : last . ID ,
} )
if err != nil {
t . Fatalf ( "resolveVideoReferences artifacts: %v" , err )
}
if len ( refs ) != 2 {
t . Fatalf ( "expected 2 refs, got %#v" , refs )
}
if refs [ 0 ] . URL == "" || refs [ 1 ] . URL == "" {
t . Fatalf ( "expected stable URLs for both refs, got %#v" , refs )
}
}
func TestResolveVideoReferencesAcceptsCanonicalImageURL ( t * testing . T ) {
cfg := Config {
SQLitePath : filepath . Join ( t . TempDir ( ) , "popiart.db" ) ,
@ -401,6 +499,61 @@ func TestResolveVideoReferencesAcceptsSignedSameOriginMediaURL(t *testing.T) {
}
}
func TestBuildImageToVideoInputNormalizesStartEndFrameFields ( t * testing . T ) {
input := buildImageToVideoInput ( & job { Input : map [ string ] any {
"image_url" : "https://example.com/first.png" ,
"last_frame_image_url" : "https://example.com/last.png" ,
"prompt" : "smooth transition" ,
"end_frame_artifact_id" : "art_last" ,
} } )
images , ok := input [ "images" ] . ( [ ] string )
if ! ok || len ( images ) != 2 || images [ 0 ] != "https://example.com/first.png" || images [ 1 ] != "https://example.com/last.png" {
t . Fatalf ( "unexpected normalized images: %#v" , input [ "images" ] )
}
if input [ "last_frame_artifact_id" ] != "art_last" || input [ "end_frame_artifact_id" ] != "art_last" {
t . Fatalf ( "expected last-frame artifact aliases to normalize, got %#v" , input )
}
metadata , ok := input [ "metadata" ] . ( map [ string ] any )
if ! ok || metadata [ "action" ] != "firstTailGenerate" {
t . Fatalf ( "expected firstTailGenerate metadata, got %#v" , input [ "metadata" ] )
}
}
func TestLoadSkillsUsesSkillhubSchemaFiles ( t * testing . T ) {
dir := t . TempDir ( )
skillDir := filepath . Join ( dir , "skills" , "popiskill-video-image2video-basic-v1" )
if err := os . MkdirAll ( skillDir , 0 o755 ) ; err != nil {
t . Fatalf ( "mkdir skill dir: %v" , err )
}
if err := os . WriteFile ( filepath . Join ( dir , "index.json" ) , [ ] byte ( ` { "version":1,"skills":[ { "name":"popiskill-video-image2video-basic-v1","path":"skills/popiskill-video-image2video-basic-v1","category":"video","capability":"image2video"}]} ` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write index: %v" , err )
}
if err := os . WriteFile ( filepath . Join ( skillDir , "SKILL.md" ) , [ ] byte ( "---\nname: popiskill-video-image2video-basic-v1\ndescription: Test image to video skill.\n---\n\n# Image To Video\n" ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write skill doc: %v" , err )
}
if err := os . WriteFile ( filepath . Join ( skillDir , "input_schema.json" ) , [ ] byte ( ` { "type":"object","properties": { "last_frame_image_url": { "type":"string"}}} ` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write input schema: %v" , err )
}
if err := os . WriteFile ( filepath . Join ( skillDir , "output_schema.json" ) , [ ] byte ( ` { "type":"object","properties": { "video_url": { "type":"string"}}} ` ) , 0 o644 ) ; err != nil {
t . Fatalf ( "write output schema: %v" , err )
}
skills , err := loadSkills ( dir )
if err != nil {
t . Fatalf ( "loadSkills: %v" , err )
}
if len ( skills ) != 1 {
t . Fatalf ( "expected one skill, got %#v" , skills )
}
if _ , ok := skills [ 0 ] . InputSchema [ "properties" ] . ( map [ string ] any ) [ "last_frame_image_url" ] ; ! ok {
t . Fatalf ( "expected input schema from file, got %#v" , skills [ 0 ] . InputSchema )
}
if _ , ok := skills [ 0 ] . OutputSchema [ "properties" ] . ( map [ string ] any ) [ "video_url" ] ; ! ok {
t . Fatalf ( "expected output schema from file, got %#v" , skills [ 0 ] . OutputSchema )
}
}
func TestFetchVideoTaskFallsBackToGenericTaskEnvelope ( t * testing . T ) {
srv := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
switch r . URL . Path {
@ -614,6 +767,42 @@ func tinyPNG(t *testing.T) []byte {
return data
}
func uploadTestMediaArtifact ( t * testing . T , server * Server , sessionToken , filename string ) artifact {
t . Helper ( )
var body bytes . Buffer
writer := multipart . NewWriter ( & body )
part , err := writer . CreateFormFile ( "file" , filename )
if err != nil {
t . Fatalf ( "create form file: %v" , err )
}
if _ , err := part . Write ( tinyPNG ( t ) ) ; 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/artifacts/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 artifact upload 201, got %d body=%s" , rec . Code , rec . Body . String ( ) )
}
var envelope struct {
OK bool ` json:"ok" `
Data artifact ` json:"data" `
}
if err := json . Unmarshal ( rec . Body . Bytes ( ) , & envelope ) ; err != nil {
t . Fatalf ( "decode artifact upload: %v body=%s" , err , rec . Body . String ( ) )
}
if ! envelope . OK || envelope . Data . ID == "" {
t . Fatalf ( "expected artifact upload data, got %s" , rec . Body . String ( ) )
}
return envelope . Data
}
func makeEmptySkillhub ( t * testing . T ) string {
t . Helper ( )
dir := t . TempDir ( )