package app import ( "context" "encoding/json" "testing" "time" "infinite-canvas/backend/internal/kernel" "infinite-canvas/backend/internal/model" "infinite-canvas/backend/internal/repository" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func TestVideoTaskTimeoutHasFiveMinuteSafetyFloor(t *testing.T) { policy := defaultRuntimePolicy().Task policy.VideoTimeoutMinutes = 1 policy.ImageTimeoutMinutes = 1 if got := taskExecutionTimeoutWithPolicy("canvas_video", policy); got != 5*time.Minute { t.Fatalf("video timeout = %s, want 5m", got) } if got := taskExecutionTimeoutWithPolicy("canvas_image", policy); got != time.Minute { t.Fatalf("image timeout = %s, want 1m", got) } } func TestDefaultVideoTaskTimeoutIsOneHour(t *testing.T) { policy := defaultRuntimePolicy().Task if policy.VideoTimeoutMinutes != 60 { t.Fatalf("default video timeout = %d minutes, want 60", policy.VideoTimeoutMinutes) } if got := taskExecutionTimeoutWithPolicy("canvas_video", policy); got != time.Hour { t.Fatalf("video execution timeout = %s, want 1h", got) } } func TestProviderPollingDeadlineDefaultsToOneHour(t *testing.T) { startedAt := time.Now() deadline := providerPollingDeadline(context.Background()) remaining := deadline.Sub(startedAt) if remaining < time.Hour || remaining > time.Hour+time.Second { t.Fatalf("provider polling deadline = %s, want approximately 1h", remaining) } } func TestOnlyResumableNewAPIChannel2VideoDeadlinesStayRunning(t *testing.T) { svc := &Service{} input, err := json.Marshal(canvasGenerationInput{Mode: "video", Config: providerConfig{BaseURL: "https://example.com", InterfaceType: string(model.ChannelInterfaceNewAPIChannel2)}}) if err != nil { t.Fatal(err) } startedAt := time.Now() base := model.Task{ID: "task-1", Type: "canvas_video", ProviderRequestID: "provider-task-1", StartedAt: &startedAt} if !svc.shouldDeferVideoProviderTask(base, string(input), context.DeadlineExceeded) { t.Fatal("resumable NewAPI Channel 2 deadline should remain running") } pendingErr := providerStatePendingError{TaskID: base.ProviderRequestID, Cause: providerHTTPError{StatusCode: 400, Body: `{"code":"task_not_exist"}`}} if !svc.shouldDeferVideoProviderTask(base, string(input), pendingErr) { t.Fatal("resumable NewAPI Channel 2 provider sync delay should remain running") } expiredStartedAt := startedAt.Add(-newAPIChannel2TaskSyncMaxAge - time.Second) expiredTask := model.Task{ID: base.ID, Type: base.Type, ProviderRequestID: base.ProviderRequestID, StartedAt: &expiredStartedAt} if svc.shouldDeferVideoProviderTask(expiredTask, string(input), pendingErr) { t.Fatal("provider sync delay must stop after the bounded synchronization window") } if !svc.shouldDeferVideoProviderTask(expiredTask, string(input), context.DeadlineExceeded) { t.Fatal("a normal long-running provider task must remain resumable after the synchronization window") } if svc.shouldDeferVideoProviderTask(model.Task{ID: base.ID, Type: base.Type, ProviderRequestID: base.ProviderRequestID}, string(input), pendingErr) { t.Fatal("provider sync delay without a persisted start time must fail closed") } if svc.shouldDeferVideoProviderTask(model.Task{ID: "task-2", Type: "canvas_video"}, string(input), context.DeadlineExceeded) { t.Fatal("missing provider task id must not be deferred") } if svc.shouldDeferVideoProviderTask(model.Task{ID: "task-2", Type: "canvas_video"}, string(input), pendingErr) { t.Fatal("provider sync delay without persisted provider task id must not be deferred") } if svc.shouldDeferVideoProviderTask(base, string(input), context.Canceled) { t.Fatal("explicit cancellation must not be deferred") } if svc.shouldDeferVideoProviderTask(base, string(input), providerHTTPError{StatusCode: 400, Body: `{"code":"task_not_exist"}`}) { t.Fatal("untyped provider error must not be deferred") } other, err := json.Marshal(canvasGenerationInput{Mode: "video", Config: providerConfig{BaseURL: "https://example.com", InterfaceType: string(model.ChannelInterfaceNewAPIVideo)}}) if err != nil { t.Fatal(err) } if svc.shouldDeferVideoProviderTask(base, string(other), context.DeadlineExceeded) { t.Fatal("providers without a verified query-only resume contract must fail closed") } } func TestResumableVideoDeadlineUsesResolvedSystemChannelProtocol(t *testing.T) { db, err := gorm.Open(sqlite.Open("file:"+kernel.NewID()+"?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err := db.AutoMigrate(&model.ModelChannel{}, &model.ChannelModel{}); err != nil { t.Fatal(err) } channel := model.ModelChannel{ ID: "channel-video", UserID: "admin", Scope: model.ChannelScopeSystem, Enabled: true, Name: "Video", BaseURL: "https://example.com", APIKey: "test-key", APIFormat: "openai", ModelsJSON: `["video-model"]`, } channelModel := model.ChannelModel{ ID: "model-video", ChannelID: channel.ID, ModelKey: "video-model", Capability: "video", Protocol: model.ChannelInterfaceNewAPIChannel2, Enabled: true, PriceConfigured: true, } if err := db.Create(&channel).Error; err != nil { t.Fatal(err) } if err := db.Create(&channelModel).Error; err != nil { t.Fatal(err) } input, err := json.Marshal(canvasGenerationInput{Mode: "video", Config: providerConfig{ChannelID: channel.ID, Model: "video-model"}}) if err != nil { t.Fatal(err) } svc := New(repository.New(db), t.TempDir()) task := model.Task{ID: "task-system", Type: "canvas_video", ProviderRequestID: "provider-task-1"} if !svc.shouldDeferVideoProviderTask(task, string(input), context.DeadlineExceeded) { t.Fatal("system-channel task must use the resolved model protocol when deciding query-only recovery") } } func TestRuntimePolicyDefaultsAndSelfUseModeValidate(t *testing.T) { if err := validateRuntimePolicy(defaultRuntimePolicy()); err != nil { t.Fatalf("default runtime policy error = %v", err) } selfUse := selfUseRuntimePolicy() if err := validateRuntimePolicy(selfUse); err != nil { t.Fatalf("self-use runtime policy error = %v", err) } if selfUse.Task.WorkerConcurrency != 999 || selfUse.Resource.ResourceUploadMB != 999 { t.Fatalf("self-use maxima = worker %d, upload %d", selfUse.Task.WorkerConcurrency, selfUse.Resource.ResourceUploadMB) } } func TestRuntimePolicyRejectsSingleFileAboveAccountCapacity(t *testing.T) { policy := defaultRuntimePolicy() policy.Resource.StoredFileGB = 1 policy.Resource.ResourceUploadMB = 999 if err := validateRuntimePolicy(policy); err != nil { t.Fatalf("999MB should fit in 1GB: %v", err) } policy.Resource.StoredFileGB = 0 if err := validateRuntimePolicy(policy); err == nil { t.Fatal("zero account capacity should be rejected") } } func TestRuntimePolicySaveAndResetTakeEffectImmediately(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err := db.AutoMigrate(&model.SystemSetting{}, &model.AdminAuditEvent{}); err != nil { t.Fatal(err) } svc := New(repository.New(db), t.TempDir()) actor := &model.User{ID: "admin", Role: model.UserRoleAdmin} policy := defaultRuntimePolicy() policy.Task.ActiveTaskLimit = 17 if _, err := svc.UpdateRuntimePolicySetting(actor, policy); err != nil { t.Fatal(err) } effective, err := svc.RuntimePolicy() if err != nil || effective.Task.ActiveTaskLimit != 17 { t.Fatalf("effective active task limit = %d, error = %v", effective.Task.ActiveTaskLimit, err) } if _, err := svc.ResetRuntimePolicySetting(actor); err != nil { t.Fatal(err) } effective, err = svc.RuntimePolicy() if err != nil || effective.Task.ActiveTaskLimit != 5 { t.Fatalf("reset active task limit = %d, error = %v", effective.Task.ActiveTaskLimit, err) } } func TestRuntimePolicyBackfillsRecycleBinRetentionForLegacyJSON(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err := db.AutoMigrate(&model.SystemSetting{}); err != nil { t.Fatal(err) } legacy := defaultRuntimePolicy() encoded, err := json.Marshal(legacy) if err != nil { t.Fatal(err) } var value map[string]any if err := json.Unmarshal(encoded, &value); err != nil { t.Fatal(err) } resource := value["resource"].(map[string]any) delete(resource, "recycleBinRetentionDays") encoded, err = json.Marshal(value) if err != nil { t.Fatal(err) } if err := db.Create(&model.SystemSetting{Key: runtimePolicySettingKey, ValueJSON: string(encoded), UpdatedBy: "admin"}).Error; err != nil { t.Fatal(err) } policy, err := New(repository.New(db), t.TempDir()).RuntimePolicy() if err != nil { t.Fatal(err) } if policy.Resource.RecycleBinRetentionDays != 30 { t.Fatalf("legacy recycle bin retention = %d, want 30", policy.Resource.RecycleBinRetentionDays) } }