refactor(*): 平台架构 - 拆分后端领域模块并同步插件协议与模型能力
This commit is contained in:
133
backend/internal/app/task_render_test.go
Normal file
133
backend/internal/app/task_render_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
// renderTestProject 构造一个可渲染的 v2 快照:一条可见视频轨 + 一个
|
||||
// 经 directMedia.storageKey(resource:<id>)引用后端资源的片段。
|
||||
func renderTestProject(storageKey string) renderProject {
|
||||
visible := true
|
||||
return renderProject{
|
||||
Version: 2,
|
||||
DurationMs: 2000,
|
||||
Tracks: []renderTrack{{ID: "track-v1", Kind: "video", Visible: &visible}},
|
||||
Clips: []renderClip{{
|
||||
ID: "clip-v1",
|
||||
Kind: "video",
|
||||
TrackID: "track-v1",
|
||||
StartMs: 0,
|
||||
DurationMs: 2000,
|
||||
SourceStartMs: 0,
|
||||
Volume: 1,
|
||||
DirectMedia: &struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
StorageKey string `json:"storageKey"`
|
||||
}{ID: "asset-v1", Kind: "video", StorageKey: storageKey},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func renderInputJSON(t *testing.T, project renderProject) string {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(timelineRenderInput{ProjectID: "prj-render", Timeline: project})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal render input: %v", err)
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func seedRunningRenderTask(t *testing.T, db *gorm.DB, inputJSON string) *model.Task {
|
||||
t.Helper()
|
||||
task := &model.Task{
|
||||
ID: fmt.Sprintf("tsk-render-%d", time.Now().UnixNano()),
|
||||
UserID: "usr-render-test",
|
||||
Type: model.TaskTypeTimelineRender,
|
||||
Status: model.TaskStatusRunning,
|
||||
Stage: "已领取",
|
||||
InputJSON: inputJSON,
|
||||
}
|
||||
if err := db.Create(task).Error; err != nil {
|
||||
t.Fatalf("seed task: %v", err)
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
func TestCreateTimelineRenderTaskQueues(t *testing.T) {
|
||||
svc, db := newTimelineTaskTestService(t)
|
||||
|
||||
task, err := svc.CreateTimelineRenderTask("usr-render-test", TimelineRenderCreateRequest{
|
||||
ProjectID: "prj-render",
|
||||
Timeline: renderTestProject("resource:res-1"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
if task.Type != model.TaskTypeTimelineRender || task.Status != model.TaskStatusQueued {
|
||||
t.Fatalf("task = %s/%s, want %s/queued", task.Type, task.Status, model.TaskTypeTimelineRender)
|
||||
}
|
||||
if task.Provider != "local" || task.Model != "ffmpeg" {
|
||||
t.Fatalf("provider/model = %s/%s, want local/ffmpeg", task.Provider, task.Model)
|
||||
}
|
||||
|
||||
var stored model.Task
|
||||
if err := db.First(&stored, "id = ?", task.ID).Error; err != nil {
|
||||
t.Fatalf("load task: %v", err)
|
||||
}
|
||||
if stored.ProjectID != "prj-render" || stored.UserID != "usr-render-test" {
|
||||
t.Fatalf("stored owner/project mismatch: %s/%s", stored.UserID, stored.ProjectID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTimelineRenderTaskRejectsNoMedia(t *testing.T) {
|
||||
svc, _ := newTimelineTaskTestService(t)
|
||||
|
||||
visible := true
|
||||
textOnly := renderProject{
|
||||
Version: 2,
|
||||
DurationMs: 1000,
|
||||
Tracks: []renderTrack{{ID: "track-t1", Kind: "text", Visible: &visible}},
|
||||
Clips: []renderClip{{
|
||||
ID: "clip-t1", Kind: "text", TrackID: "track-t1", StartMs: 0,
|
||||
DurationMs: 1000, Text: "字幕",
|
||||
}},
|
||||
}
|
||||
_, err := svc.CreateTimelineRenderTask("usr-render-test", TimelineRenderCreateRequest{Timeline: textOnly})
|
||||
if err == nil || !strings.Contains(err.Error(), "可渲染") {
|
||||
t.Fatalf("err = %v, want mention of 没有可渲染的媒体片段", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimelineRenderFailsFastWhenSourceUnreadable(t *testing.T) {
|
||||
svc, db := newTimelineTaskTestService(t)
|
||||
// 指向不存在的资源;ffmpeg 路径设为无效值不影响断言——本测试在
|
||||
// 素材落盘(materialize)阶段即失败,且该失败早于任何外部命令执行。
|
||||
task := seedRunningRenderTask(t, db, renderInputJSON(t, renderTestProject("resource:res-missing")))
|
||||
|
||||
t.Setenv(renderFfmpegEnv, "/no/such/ffmpeg")
|
||||
w := newTaskWorkerCoordinator(svc)
|
||||
if err := w.processTimelineRender(task, context.Background()); err != nil {
|
||||
t.Fatalf("process: want nil (task terminal handled internally), got %v", err)
|
||||
}
|
||||
|
||||
var stored model.Task
|
||||
if err := db.First(&stored, "id = ?", task.ID).Error; err != nil {
|
||||
t.Fatalf("load task: %v", err)
|
||||
}
|
||||
if stored.Status != model.TaskStatusFailed {
|
||||
t.Fatalf("status = %q, want failed", stored.Status)
|
||||
}
|
||||
if !strings.Contains(stored.Error, "时间线引用的媒体") {
|
||||
t.Fatalf("error = %q, want mention of 无法读取时间线引用的媒体", stored.Error)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user