package application import ( "bytes" "context" "io" "os" "path/filepath" "reflect" "testing" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/billing" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/settings" ) func TestDefaultSettingsServicePersistsRestartRequiredWithoutPartialHotReload(t *testing.T) { path := filepath.Join(t.TempDir(), ".env.local") key := "IMAGE_GENERATE_ENGINE" service := defaultSettingsService(func(name string) string { if name == "ZHINIAN_SETTINGS_FILE" { return path } return "" }) value, err := service.Save(context.Background(), map[string]any{key: "evolink"}) if err != nil { t.Fatal(err) } payload, ok := value.(settings.Payload) if !ok || !payload.RestartRequired { t.Fatalf("payload=%#v", value) } } func TestBillingAccountAndSettingsUseOneRuntimeSource(t *testing.T) { path := filepath.Join(t.TempDir(), ".env.local") if err := os.WriteFile(path, []byte("ZHINIAN_BILLING_ACCOUNT_NAME=Original\nZHINIAN_BILLING_ACCOUNT_BANK=Old Bank\n"), 0o600); err != nil { t.Fatal(err) } service := defaultSettingsService(func(name string) string { if name == "ZHINIAN_SETTINGS_FILE" { return path } return "" }) store := settingsBillingAccountStore{service: service} if err := store.Save(context.Background(), billing.AccountConfig{AccountName: "Updated", BankName: "New Bank", AccountNumber: "123", Contact: "Ops"}); err != nil { t.Fatal(err) } loaded, err := store.Load(context.Background()) if err != nil || loaded.AccountName != "Updated" || loaded.BankName != "New Bank" || loaded.AccountNumber != "123" || loaded.Contact != "Ops" { t.Fatalf("loaded=%#v err=%v", loaded, err) } payload, err := service.Get(context.Background()) if err != nil || fieldValueFromSettings(t, payload, "ZHINIAN_BILLING_ACCOUNT_NAME") != "Updated" { t.Fatalf("settings payload=%#v err=%v", payload, err) } } func fieldValueFromSettings(t *testing.T, value any, key string) string { t.Helper() payload := value.(settings.Payload) for _, group := range payload.Groups { for _, field := range group.Fields { if field.Key == key { return field.Value } } } t.Fatalf("settings field %s not found", key) return "" } func TestRuntimeHealthDetailsMatchTypeScriptDefaultsAndConfiguredModes(t *testing.T) { values := map[string]string{ "VOLCENGINE_ACCESS_KEY_ID": "access", "VOLCENGINE_SECRET_ACCESS_KEY": "secret", "SEEDANCE_API_KEY": "seedance-key", "ZHINIAN_AUTH_REQUIRED": "true", "ZHINIAN_AUTH_SESSION_SECRET": "session-secret", } details := runtimeHealthDetails(func(name string) string { return values[name] }) if details.VisualAPIMode != "volcengine" || details.EvolinkMode != "mock" || details.SeedanceMode != "seedance" || details.BailianMode != "missing" || details.AuthMode != "configured" { t.Fatalf("details = %+v", details) } if len(details.Capabilities) != 2 { t.Fatalf("capabilities = %#v, want image and Seedance", details.Capabilities) } image := details.Capabilities[0].(map[string]any) video := details.Capabilities[1].(map[string]any) if image["id"] != "image.generate" || image["engineLabel"] != "即梦" || video["id"] != "video.generate" || video["engineLabel"] != "Seedance" { t.Fatalf("capabilities = %#v", details.Capabilities) } } func TestRuntimeHealthDetailsHonorExplicitMockFlagsAndImageEngine(t *testing.T) { values := map[string]string{ "IMAGE_GENERATE_ENGINE": "bailian", "BAILIAN_API_KEY": "bailian-key", "BAILIAN_MOCK": "true", "JIMENG_VISUAL_MOCK": "true", "EVOLINK_MOCK": "false", "SEEDANCE_MOCK": "true", } details := runtimeHealthDetails(func(name string) string { return values[name] }) if details.VisualAPIMode != "mock" || details.EvolinkMode != "evolink" || details.SeedanceMode != "mock" || details.BailianMode != "mock" || details.AuthMode != "disabled" { t.Fatalf("details = %+v", details) } image := details.Capabilities[0].(map[string]any) if image["engine"] != "bailian" || image["engineLabel"] != "阿里云百炼" || image["reqKey"] != "wan2.7-image-pro" { t.Fatalf("image capability = %#v", image) } } func TestCapabilitySummaryMatchesConfiguredDefaultVideoEngine(t *testing.T) { for _, test := range []struct { name, engine, wantEngine, wantProvider, wantModel string }{ {name: "Bailian default", engine: "", wantEngine: "bailian", wantProvider: "bailian", wantModel: "wan2.7-i2v-2026-04-25"}, {name: "Seedance configured", engine: "seedance", wantEngine: "seedance", wantProvider: "seedance", wantModel: "doubao-seedance-2-0-260128"}, } { t.Run(test.name, func(t *testing.T) { values := map[string]string{"VIDEO_GENERATE_ENGINE": test.engine} value, err := capabilitySummary(func(name string) string { return values[name] })(context.Background()) if err != nil { t.Fatal(err) } capabilities := value.([]any) video := capabilities[1].(map[string]any) if video["engine"] != test.wantEngine || video["provider"] != test.wantProvider || video["reqKey"] != test.wantModel { t.Fatalf("video capability = %#v", video) } }) } } func TestRemoteAssetMaximumDefaultsToTwentyMiB(t *testing.T) { getenv := func(string) string { return "" } if got := remoteAssetMaxBytes(getenv); got != 20<<20 { t.Fatalf("remote asset maximum = %d, want %d", got, int64(20<<20)) } getenv = func(string) string { return "3145728" } if got := remoteAssetMaxBytes(getenv); got != 3<<20 { t.Fatalf("configured remote asset maximum = %d, want %d", got, int64(3<<20)) } } func TestPrefixedBlobStoreKeepsApplicationStoragePathStable(t *testing.T) { inner := &recordingBlobStore{} store := prefixedBlobStore{prefix: "tenant-prefix", store: inner} stored, err := store.Put(context.Background(), "uploads/day/file.png", bytes.NewReader([]byte("x")), 1, "image/png") if err != nil { t.Fatal(err) } if stored.Key != "uploads/day/file.png" || inner.putKey != "tenant-prefix/uploads/day/file.png" { t.Fatalf("stored=%#v inner=%q", stored, inner.putKey) } _, _ = store.Read(context.Background(), stored.Key) _ = store.Delete(context.Background(), stored.Key) if !reflect.DeepEqual([]string{inner.readKey, inner.deleteKey}, []string{"tenant-prefix/uploads/day/file.png", "tenant-prefix/uploads/day/file.png"}) { t.Fatalf("read/delete=%q/%q", inner.readKey, inner.deleteKey) } } type recordingBlobStore struct{ putKey, readKey, deleteKey string } func (s *recordingBlobStore) Put(_ context.Context, key string, _ io.Reader, _ int64, _ string) (assets.StoredObject, error) { s.putKey = key return assets.StoredObject{Key: key, URL: "https://cdn.example/" + key}, nil } func (s *recordingBlobStore) Read(_ context.Context, key string) (assets.Blob, error) { s.readKey = key return assets.Blob{Body: io.NopCloser(bytes.NewReader(nil))}, nil } func (s *recordingBlobStore) Delete(_ context.Context, key string) error { s.deleteKey = key return nil }