ui优化
This commit is contained in:
1 parent
648b274c55
commit
18cb51670c
14 files changed
+420
-171
No files matched your search
@@ -14,8 +14,9 @@ import (
|
||||
|
||||
type RuntimeUpdater func(context.Context, map[string]string) error
|
||||
|
||||
// RuntimeSettingsRepository persists the subset of settings that can be
|
||||
// applied to provider calls without restarting the Go process.
|
||||
// RuntimeSettingsRepository persists editable settings for shared PostgreSQL
|
||||
// deployments. Runtime application semantics are independent from storage:
|
||||
// some settings apply immediately while startup-only settings require restart.
|
||||
type RuntimeSettingsRepository interface {
|
||||
LoadRuntimeSettings(context.Context, []string) (map[string]string, error)
|
||||
SaveRuntimeSettings(context.Context, map[string]string) error
|
||||
@@ -80,7 +81,7 @@ type Service struct {
|
||||
mu sync.Mutex
|
||||
path string
|
||||
environment map[string]string
|
||||
provider map[string]string
|
||||
database map[string]string
|
||||
repository RuntimeSettingsRepository
|
||||
update RuntimeUpdater
|
||||
billing BillingAccountWriter
|
||||
@@ -100,7 +101,7 @@ func New(path string, environment map[string]string, updater RuntimeUpdater) *Se
|
||||
|
||||
func NewWithRuntimeRepository(path string, environment map[string]string, repository RuntimeSettingsRepository, updater RuntimeUpdater) *Service {
|
||||
return &Service{
|
||||
path: path, environment: cloneStrings(environment), provider: map[string]string{},
|
||||
path: path, environment: cloneStrings(environment), database: map[string]string{},
|
||||
repository: repository, update: updater,
|
||||
}
|
||||
}
|
||||
@@ -133,15 +134,6 @@ func RuntimeSettingKeys() []string {
|
||||
return keys
|
||||
}
|
||||
|
||||
func ProviderSettingKeys() []string {
|
||||
keys := make([]string, 0, len(providerSettingKeys))
|
||||
for key := range providerSettingKeys {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func allowedEnvironmentKey(key string) bool {
|
||||
_, allowed := fieldIndex[key]
|
||||
return allowed
|
||||
@@ -157,38 +149,38 @@ func (s *Service) Get(ctx context.Context) (any, error) {
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if err := s.refreshProviderSettings(ctx); err != nil {
|
||||
if err := s.refreshDatabaseSettings(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.get()
|
||||
}
|
||||
|
||||
// RuntimeValues returns one coherent provider configuration snapshot. Database
|
||||
// RuntimeValues returns one coherent editable configuration snapshot. Database
|
||||
// values override process/file values, including an explicitly saved empty
|
||||
// non-secret value. Callers use a fresh snapshot per request or worker action
|
||||
// so changes made through another replica are also observed.
|
||||
// non-secret value. Dynamic callers use a fresh snapshot per request or worker
|
||||
// action so changes made through another replica are also observed.
|
||||
func (s *Service) RuntimeValues(ctx context.Context) (map[string]string, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if err := s.refreshProviderSettings(ctx); err != nil {
|
||||
if err := s.refreshDatabaseSettings(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.runtimeValues()
|
||||
}
|
||||
|
||||
// CachedRuntimeValues is the non-blocking fallback used by liveness reporting
|
||||
// if PostgreSQL is temporarily unavailable. Mutating/provider request paths use
|
||||
// RuntimeValues and therefore still fail closed on a refresh error.
|
||||
// if PostgreSQL is temporarily unavailable. Mutating and dynamic provider paths
|
||||
// use RuntimeValues and therefore still fail closed on a refresh error.
|
||||
func (s *Service) CachedRuntimeValues() map[string]string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
values, err := s.runtimeValues()
|
||||
if err != nil {
|
||||
values = cloneStrings(s.environment)
|
||||
for key, value := range s.provider {
|
||||
for key, value := range s.database {
|
||||
values[key] = value
|
||||
}
|
||||
}
|
||||
@@ -201,7 +193,7 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if err := s.refreshProviderSettings(ctx); err != nil {
|
||||
if err := s.refreshDatabaseSettings(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updates := map[string]string{}
|
||||
@@ -218,10 +210,10 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
|
||||
updates[key] = next
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
providerUpdates, fileUpdates := map[string]string{}, map[string]string{}
|
||||
databaseUpdates, fileUpdates := map[string]string{}, map[string]string{}
|
||||
for key, value := range updates {
|
||||
if s.repository != nil && isProviderSetting(key) {
|
||||
providerUpdates[key] = value
|
||||
if s.repository != nil {
|
||||
databaseUpdates[key] = value
|
||||
} else {
|
||||
fileUpdates[key] = value
|
||||
}
|
||||
@@ -229,7 +221,7 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
|
||||
// Capture the complete file-backed view before writing so a partial billing
|
||||
// account update cannot blank sibling fields.
|
||||
persisted := map[string]string{}
|
||||
if len(fileUpdates) > 0 || containsBillingAccountUpdate(updates) {
|
||||
if len(fileUpdates) > 0 || (s.billing != nil && containsBillingAccountUpdate(updates)) {
|
||||
var err error
|
||||
persisted, err = readEnv(s.path)
|
||||
if err != nil {
|
||||
@@ -241,9 +233,9 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(providerUpdates) > 0 {
|
||||
if err := s.repository.SaveRuntimeSettings(ctx, cloneStrings(providerUpdates)); err != nil {
|
||||
return nil, fmt.Errorf("persist provider settings: %w", err)
|
||||
if len(databaseUpdates) > 0 {
|
||||
if err := s.repository.SaveRuntimeSettings(ctx, cloneStrings(databaseUpdates)); err != nil {
|
||||
return nil, fmt.Errorf("persist runtime settings: %w", err)
|
||||
}
|
||||
}
|
||||
if s.update != nil {
|
||||
@@ -255,15 +247,15 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
|
||||
s.environment[key] = value
|
||||
persisted[key] = value
|
||||
}
|
||||
for key, value := range providerUpdates {
|
||||
s.provider[key] = value
|
||||
for key, value := range databaseUpdates {
|
||||
s.database[key] = value
|
||||
}
|
||||
if s.billing != nil && containsBillingAccountUpdate(updates) {
|
||||
if err := s.billing.SaveBillingAccount(ctx, BillingAccount{
|
||||
AccountName: billingSetting(s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_NAME"),
|
||||
BankName: billingSetting(s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_BANK"),
|
||||
AccountNumber: billingSetting(s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_NUMBER"),
|
||||
Contact: billingSetting(s.environment, persisted, "ZHINIAN_BILLING_CONTACT"),
|
||||
AccountName: lookup(s.database, s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_NAME", ""),
|
||||
BankName: lookup(s.database, s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_BANK", ""),
|
||||
AccountNumber: lookup(s.database, s.environment, persisted, "ZHINIAN_BILLING_ACCOUNT_NUMBER", ""),
|
||||
Contact: lookup(s.database, s.environment, persisted, "ZHINIAN_BILLING_CONTACT", ""),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("apply billing account settings: %w", err)
|
||||
}
|
||||
@@ -276,13 +268,6 @@ func (s *Service) Save(ctx context.Context, values map[string]any) (any, error)
|
||||
return payload, err
|
||||
}
|
||||
|
||||
func billingSetting(environment, persisted map[string]string, key string) string {
|
||||
if value, ok := environment[key]; ok {
|
||||
return value
|
||||
}
|
||||
return persisted[key]
|
||||
}
|
||||
|
||||
func containsBillingAccountUpdate(updates map[string]string) bool {
|
||||
for key := range updates {
|
||||
switch key {
|
||||
@@ -295,13 +280,7 @@ func containsBillingAccountUpdate(updates map[string]string) bool {
|
||||
|
||||
func requiresRestart(updates map[string]string) bool {
|
||||
for key := range updates {
|
||||
if isProviderSetting(key) {
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "ZHINIAN_BILLING_ACCOUNT_NAME", "ZHINIAN_BILLING_ACCOUNT_BANK", "ZHINIAN_BILLING_ACCOUNT_NUMBER", "ZHINIAN_BILLING_CONTACT":
|
||||
continue
|
||||
default:
|
||||
if _, required := restartRequiredSettingKeys[key]; required {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -313,7 +292,7 @@ func (s *Service) get() (Payload, error) {
|
||||
return Payload{}, err
|
||||
}
|
||||
current := func(field Field) string {
|
||||
if value, ok := s.provider[field.Key]; ok {
|
||||
if value, ok := s.database[field.Key]; ok {
|
||||
return value
|
||||
}
|
||||
if value, ok := s.environment[field.Key]; ok {
|
||||
@@ -337,32 +316,32 @@ func (s *Service) get() (Payload, error) {
|
||||
}
|
||||
image := normalizeImage(current(fieldIndex["IMAGE_GENERATE_ENGINE"]))
|
||||
video := normalizeVideo(current(fieldIndex["VIDEO_GENERATE_ENGINE"]))
|
||||
imageModel := map[string]string{"jimeng": lookup(s.provider, s.environment, file, "JIMENG_IMAGE_GENERATE_46_REQ_KEY", "jimeng_seedream46_cvtob"), "evolink": lookup(s.provider, s.environment, file, "EVOLINK_IMAGE_MODEL", "gpt-image-2"), "bailian": lookup(s.provider, s.environment, file, "BAILIAN_IMAGE_MODEL", "wan2.7-image-pro")}[image]
|
||||
videoModel := lookup(s.provider, s.environment, file, "SEEDANCE_MODEL", "doubao-seedance-2-0-260128")
|
||||
imageModel := map[string]string{"jimeng": lookup(s.database, s.environment, file, "JIMENG_IMAGE_GENERATE_46_REQ_KEY", "jimeng_seedream46_cvtob"), "evolink": lookup(s.database, s.environment, file, "EVOLINK_IMAGE_MODEL", "gpt-image-2"), "bailian": lookup(s.database, s.environment, file, "BAILIAN_IMAGE_MODEL", "wan2.7-image-pro")}[image]
|
||||
videoModel := lookup(s.database, s.environment, file, "SEEDANCE_MODEL", "doubao-seedance-2-0-260128")
|
||||
if video == "bailian" {
|
||||
videoModel = lookup(s.provider, s.environment, file, "BAILIAN_VIDEO_MODEL", "wan2.7-i2v-2026-04-25")
|
||||
videoModel = lookup(s.database, s.environment, file, "BAILIAN_VIDEO_MODEL", "wan2.7-i2v-2026-04-25")
|
||||
}
|
||||
imageConnected := connected(image, s.provider, s.environment, file)
|
||||
videoConnected := connected(video, s.provider, s.environment, file)
|
||||
imageField := project(fieldIndex["IMAGE_GENERATE_ENGINE"], image, s.provider, s.environment, file)
|
||||
videoField := project(fieldIndex["VIDEO_GENERATE_ENGINE"], video, s.provider, s.environment, file)
|
||||
imageConnected := connected(image, s.database, s.environment, file)
|
||||
videoConnected := connected(video, s.database, s.environment, file)
|
||||
imageField := project(fieldIndex["IMAGE_GENERATE_ENGINE"], image, s.database, s.environment, file)
|
||||
videoField := project(fieldIndex["VIDEO_GENERATE_ENGINE"], video, s.database, s.environment, file)
|
||||
assignments := []EngineAssignment{{ID: "image.generate", Label: "图片生成", Engine: image, EngineLabel: label(image), Connected: imageConnected, ConnectionLabel: connection(imageConnected), ReqKey: imageModel, Configurable: true, Field: &imageField}, {ID: "video.generate", Label: "视频生成", Engine: video, EngineLabel: label(video), Connected: videoConnected, ConnectionLabel: connection(videoConnected), ReqKey: videoModel, Configurable: true, Field: &videoField}}
|
||||
services := Services{Visual: connected("jimeng", s.provider, s.environment, file), Evolink: connected("evolink", s.provider, s.environment, file), Seedance: connected("seedance", s.provider, s.environment, file), Bailian: connected("bailian", s.provider, s.environment, file), Auth: lookup(s.provider, s.environment, file, "ZHINIAN_AUTH_SESSION_SECRET", "") != "", Organization: lookup(s.provider, s.environment, file, "DATABASE_URL", "") != "" || lookup(s.provider, s.environment, file, "ZHINIAN_AUTH_SESSION_SECRET", "") != ""}
|
||||
services := Services{Visual: connected("jimeng", s.database, s.environment, file), Evolink: connected("evolink", s.database, s.environment, file), Seedance: connected("seedance", s.database, s.environment, file), Bailian: connected("bailian", s.database, s.environment, file), Auth: lookup(s.database, s.environment, file, "ZHINIAN_AUTH_SESSION_SECRET", "") != "", Organization: lookup(s.database, s.environment, file, "DATABASE_URL", "") != "" || lookup(s.database, s.environment, file, "ZHINIAN_AUTH_SESSION_SECRET", "") != ""}
|
||||
return Payload{Services: services, Capabilities: []Capability{{ID: "image.generate", Label: "图片生成 4.6", ReqKey: imageModel, Engine: image, EngineLabel: label(image), Enabled: true}, {ID: "video.generate", Label: "视频生成", ReqKey: videoModel, Engine: video, EngineLabel: label(video), Enabled: true}}, EngineAssignments: assignments, Groups: groups}, nil
|
||||
}
|
||||
|
||||
func (s *Service) refreshProviderSettings(ctx context.Context) error {
|
||||
func (s *Service) refreshDatabaseSettings(ctx context.Context) error {
|
||||
if s.repository == nil {
|
||||
return nil
|
||||
}
|
||||
values, err := s.repository.LoadRuntimeSettings(ctx, ProviderSettingKeys())
|
||||
values, err := s.repository.LoadRuntimeSettings(ctx, RuntimeSettingKeys())
|
||||
if err != nil {
|
||||
return fmt.Errorf("load provider settings: %w", err)
|
||||
return fmt.Errorf("load runtime settings: %w", err)
|
||||
}
|
||||
s.provider = map[string]string{}
|
||||
s.database = map[string]string{}
|
||||
for key, value := range values {
|
||||
if isProviderSetting(key) {
|
||||
s.provider[key] = value
|
||||
if allowedEnvironmentKey(key) {
|
||||
s.database[key] = value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -382,7 +361,7 @@ func (s *Service) runtimeValues() (map[string]string, error) {
|
||||
for key, value := range s.environment {
|
||||
values[key] = value
|
||||
}
|
||||
for key, value := range s.provider {
|
||||
for key, value := range s.database {
|
||||
values[key] = value
|
||||
}
|
||||
return values, nil
|
||||
@@ -537,8 +516,8 @@ func cloneStrings(input map[string]string) map[string]string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
func lookup(provider, environment, file map[string]string, key, fallback string) string {
|
||||
if value, ok := provider[key]; ok {
|
||||
func lookup(database, environment, file map[string]string, key, fallback string) string {
|
||||
if value, ok := database[key]; ok {
|
||||
return value
|
||||
}
|
||||
if value, ok := environment[key]; ok {
|
||||
@@ -549,9 +528,9 @@ func lookup(provider, environment, file map[string]string, key, fallback string)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
func project(field Field, value string, provider, environment, file map[string]string) Field {
|
||||
func project(field Field, value string, database, environment, file map[string]string) Field {
|
||||
field.Value = value
|
||||
field.Configured = lookup(provider, environment, file, field.Key, "") != ""
|
||||
field.Configured = lookup(database, environment, file, field.Key, "") != ""
|
||||
return field
|
||||
}
|
||||
func normalizeImage(value string) string {
|
||||
@@ -585,16 +564,16 @@ func connection(ok bool) string {
|
||||
}
|
||||
return "待配置"
|
||||
}
|
||||
func connected(engine string, provider, environment, file map[string]string) bool {
|
||||
func connected(engine string, database, environment, file map[string]string) bool {
|
||||
switch engine {
|
||||
case "evolink":
|
||||
return strings.TrimSpace(lookup(provider, environment, file, "EVOLINK_API_KEY", "")) != ""
|
||||
return strings.TrimSpace(lookup(database, environment, file, "EVOLINK_API_KEY", "")) != ""
|
||||
case "seedance":
|
||||
return strings.TrimSpace(lookup(provider, environment, file, "SEEDANCE_API_KEY", "")) != ""
|
||||
return strings.TrimSpace(lookup(database, environment, file, "SEEDANCE_API_KEY", "")) != ""
|
||||
case "bailian":
|
||||
return strings.TrimSpace(lookup(provider, environment, file, "BAILIAN_API_KEY", "")) != "" || strings.TrimSpace(lookup(provider, environment, file, "DASHSCOPE_API_KEY", "")) != ""
|
||||
return strings.TrimSpace(lookup(database, environment, file, "BAILIAN_API_KEY", "")) != "" || strings.TrimSpace(lookup(database, environment, file, "DASHSCOPE_API_KEY", "")) != ""
|
||||
default:
|
||||
return strings.TrimSpace(lookup(provider, environment, file, "VOLCENGINE_ACCESS_KEY_ID", "")) != "" && strings.TrimSpace(lookup(provider, environment, file, "VOLCENGINE_SECRET_ACCESS_KEY", "")) != ""
|
||||
return strings.TrimSpace(lookup(database, environment, file, "VOLCENGINE_ACCESS_KEY_ID", "")) != "" && strings.TrimSpace(lookup(database, environment, file, "VOLCENGINE_SECRET_ACCESS_KEY", "")) != ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,23 +601,13 @@ var fieldIndex = func() map[string]Field {
|
||||
return result
|
||||
}()
|
||||
|
||||
var providerSettingKeys = map[string]struct{}{
|
||||
"VOLCENGINE_ACCESS_KEY_ID": {},
|
||||
"VOLCENGINE_SECRET_ACCESS_KEY": {},
|
||||
"EVOLINK_API_KEY": {},
|
||||
"EVOLINK_BASE_URL": {},
|
||||
"EVOLINK_IMAGE_MODEL": {},
|
||||
"EVOLINK_IMAGE_QUALITY": {},
|
||||
"SEEDANCE_API_KEY": {},
|
||||
"BAILIAN_API_KEY": {},
|
||||
"BAILIAN_BASE_URL": {},
|
||||
"BAILIAN_IMAGE_MODEL": {},
|
||||
"BAILIAN_VIDEO_MODEL": {},
|
||||
"IMAGE_GENERATE_ENGINE": {},
|
||||
"VIDEO_GENERATE_ENGINE": {},
|
||||
}
|
||||
|
||||
func isProviderSetting(key string) bool {
|
||||
_, ok := providerSettingKeys[key]
|
||||
return ok
|
||||
var restartRequiredSettingKeys = map[string]struct{}{
|
||||
"ZHINIAN_AUTH_REQUIRED": {},
|
||||
"ZHINIAN_AUTH_SESSION_SECRET": {},
|
||||
"ZHINIAN_BILLING_REQUIRED": {},
|
||||
"ALI_OSS_ENDPOINT": {},
|
||||
"ALI_OSS_BUCKET": {},
|
||||
"ALI_OSS_ACCESS_KEY_ID": {},
|
||||
"ALI_OSS_ACCESS_KEY_SECRET": {},
|
||||
"ALI_OSS_PUBLIC_BASE_URL": {},
|
||||
}
|
||||
@@ -3,9 +3,11 @@ package settings
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -16,10 +18,11 @@ func TestFixtureIsConsumedByGo(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var fixture struct {
|
||||
Version int `json:"version"`
|
||||
FileName string `json:"fileName"`
|
||||
AllowedKeys []string `json:"allowedKeys"`
|
||||
SecretKeys []string `json:"secretKeys"`
|
||||
Version int `json:"version"`
|
||||
FileName string `json:"fileName"`
|
||||
AllowedKeys []string `json:"allowedKeys"`
|
||||
SecretKeys []string `json:"secretKeys"`
|
||||
DatabaseBackedKeys []string `json:"databaseBackedKeys"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &fixture); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -27,6 +30,12 @@ func TestFixtureIsConsumedByGo(t *testing.T) {
|
||||
if fixture.Version != 1 || fixture.FileName != ".env.local" || len(fixture.AllowedKeys) != 25 || len(fixture.SecretKeys) != 8 {
|
||||
t.Fatalf("fixture = %#v", fixture)
|
||||
}
|
||||
wantDatabaseKeys := RuntimeSettingKeys()
|
||||
gotDatabaseKeys := append([]string(nil), fixture.DatabaseBackedKeys...)
|
||||
sort.Strings(gotDatabaseKeys)
|
||||
if !reflect.DeepEqual(gotDatabaseKeys, wantDatabaseKeys) {
|
||||
t.Fatalf("databaseBackedKeys=%#v want %#v", gotDatabaseKeys, wantDatabaseKeys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSavePreservesSecretsAndUnrelatedEnvLines(t *testing.T) {
|
||||
@@ -73,13 +82,22 @@ func TestServiceSavePreservesSecretsAndUnrelatedEnvLines(t *testing.T) {
|
||||
|
||||
func TestRuntimeRepositoryOverridesEnvironmentAndProjectsSecrets(t *testing.T) {
|
||||
repository := &runtimeSettingsRepositoryStub{values: map[string]string{
|
||||
"EVOLINK_API_KEY": "database-secret",
|
||||
"EVOLINK_BASE_URL": "https://database.example",
|
||||
"IMAGE_GENERATE_ENGINE": "evolink",
|
||||
"EVOLINK_API_KEY": "database-secret",
|
||||
"EVOLINK_BASE_URL": "https://database.example",
|
||||
"IMAGE_GENERATE_ENGINE": "evolink",
|
||||
"ZHINIAN_AUTH_SESSION_SECRET": "database-session-secret",
|
||||
"ZHINIAN_BILLING_CONTACT": "database-billing-contact",
|
||||
"ALI_OSS_BUCKET": "database-bucket",
|
||||
}}
|
||||
service := NewWithRuntimeRepository(
|
||||
filepath.Join(t.TempDir(), ".env.local"),
|
||||
map[string]string{"EVOLINK_API_KEY": "environment-secret", "EVOLINK_BASE_URL": "https://environment.example"},
|
||||
map[string]string{
|
||||
"EVOLINK_API_KEY": "environment-secret",
|
||||
"EVOLINK_BASE_URL": "https://environment.example",
|
||||
"ZHINIAN_AUTH_SESSION_SECRET": "environment-session-secret",
|
||||
"ZHINIAN_BILLING_CONTACT": "environment-billing-contact",
|
||||
"ALI_OSS_BUCKET": "environment-bucket",
|
||||
},
|
||||
repository,
|
||||
nil,
|
||||
)
|
||||
@@ -92,11 +110,37 @@ func TestRuntimeRepositoryOverridesEnvironmentAndProjectsSecrets(t *testing.T) {
|
||||
}
|
||||
assertSecretProjection(t, value, "EVOLINK_API_KEY", true)
|
||||
runtime, err := service.RuntimeValues(context.Background())
|
||||
if err != nil || runtime["EVOLINK_API_KEY"] != "database-secret" {
|
||||
if err != nil || runtime["EVOLINK_API_KEY"] != "database-secret" || runtime["ZHINIAN_AUTH_SESSION_SECRET"] != "database-session-secret" || runtime["ZHINIAN_BILLING_CONTACT"] != "database-billing-contact" || runtime["ALI_OSS_BUCKET"] != "database-bucket" {
|
||||
t.Fatalf("runtime=%#v err=%v", runtime, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeRepositoryPersistsEveryEditableSettingWithoutCreatingLocalFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "nested", ".env.local")
|
||||
repository := &runtimeSettingsRepositoryStub{}
|
||||
service := NewWithRuntimeRepository(path, nil, repository, nil)
|
||||
updates := map[string]any{}
|
||||
want := map[string]string{}
|
||||
for _, key := range RuntimeSettingKeys() {
|
||||
value := "database-" + strings.ToLower(key)
|
||||
updates[key] = value
|
||||
want[key] = value
|
||||
}
|
||||
value, err := service.Save(context.Background(), updates)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(repository.saved, want) {
|
||||
t.Fatalf("saved=%#v want %#v", repository.saved, want)
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("database-backed save unexpectedly created local settings file: %v", err)
|
||||
}
|
||||
if !value.(Payload).RestartRequired {
|
||||
t.Fatal("startup-only database settings should require restart")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeRepositorySaveIsPartialImmediateAndDoesNotCreateSettingsFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "nested", ".env.local")
|
||||
repository := &runtimeSettingsRepositoryStub{values: map[string]string{"EVOLINK_API_KEY": "keep-secret"}}
|
||||
@@ -120,6 +164,19 @@ func TestRuntimeRepositorySaveIsPartialImmediateAndDoesNotCreateSettingsFile(t *
|
||||
assertSecretProjection(t, value, "EVOLINK_API_KEY", true)
|
||||
}
|
||||
|
||||
func TestRuntimeRepositoryFailureDoesNotFallBackToLocalFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "nested", ".env.local")
|
||||
repository := &runtimeSettingsRepositoryStub{saveErr: errors.New("database unavailable")}
|
||||
service := NewWithRuntimeRepository(path, nil, repository, nil)
|
||||
_, err := service.Save(context.Background(), map[string]any{"ALI_OSS_BUCKET": "bucket"})
|
||||
if err == nil || !strings.Contains(err.Error(), "persist runtime settings") {
|
||||
t.Fatalf("Save() error=%v", err)
|
||||
}
|
||||
if _, statErr := os.Stat(path); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("failed database save unexpectedly created local settings file: %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceGetUsesInjectedEnvironmentWithoutMutatingProcess(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), ".env.local")
|
||||
if err := os.WriteFile(path, []byte("EVOLINK_BASE_URL=file-value\nSEEDANCE_API_KEY=file-secret\n"), 0o600); err != nil {
|
||||
@@ -202,6 +259,58 @@ func TestServicePartialBillingUpdatePreservesPersistedSiblingFields(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeRepositoryPartialBillingUpdatePreservesDatabaseSiblings(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "nested", ".env.local")
|
||||
repository := &runtimeSettingsRepositoryStub{values: map[string]string{
|
||||
"ZHINIAN_BILLING_ACCOUNT_NAME": "Acme",
|
||||
"ZHINIAN_BILLING_ACCOUNT_BANK": "Legacy Bank",
|
||||
"ZHINIAN_BILLING_ACCOUNT_NUMBER": "123",
|
||||
"ZHINIAN_BILLING_CONTACT": "Old Ops",
|
||||
}}
|
||||
writer := &billingWriterStub{}
|
||||
service := NewWithRuntimeRepository(path, nil, repository, nil).WithBillingAccountWriter(writer)
|
||||
value, err := service.Save(context.Background(), map[string]any{"ZHINIAN_BILLING_CONTACT": "New Ops"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := BillingAccount{AccountName: "Acme", BankName: "Legacy Bank", AccountNumber: "123", Contact: "New Ops"}
|
||||
if writer.value != want {
|
||||
t.Fatalf("billing account=%#v want %#v", writer.value, want)
|
||||
}
|
||||
if value.(Payload).RestartRequired {
|
||||
t.Fatal("billing account database update should apply immediately")
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("billing database update unexpectedly created local settings file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartRequirementIsIndependentFromDatabasePersistence(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
want bool
|
||||
}{
|
||||
{key: "EVOLINK_API_KEY", want: false},
|
||||
{key: "IMAGE_GENERATE_ENGINE", want: false},
|
||||
{key: "ZHINIAN_BILLING_ACCOUNT_NAME", want: false},
|
||||
{key: "ZHINIAN_AUTH_REQUIRED", want: true},
|
||||
{key: "ZHINIAN_AUTH_SESSION_SECRET", want: true},
|
||||
{key: "ZHINIAN_BILLING_REQUIRED", want: true},
|
||||
{key: "ALI_OSS_ENDPOINT", want: true},
|
||||
{key: "ALI_OSS_BUCKET", want: true},
|
||||
{key: "ALI_OSS_ACCESS_KEY_ID", want: true},
|
||||
{key: "ALI_OSS_ACCESS_KEY_SECRET", want: true},
|
||||
{key: "ALI_OSS_PUBLIC_BASE_URL", want: true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.key, func(t *testing.T) {
|
||||
if got := requiresRestart(map[string]string{test.key: "value"}); got != test.want {
|
||||
t.Fatalf("requiresRestart=%v want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type billingWriterStub struct{ value BillingAccount }
|
||||
|
||||
func (writer *billingWriterStub) SaveBillingAccount(_ context.Context, value BillingAccount) error {
|
||||
@@ -210,8 +319,9 @@ func (writer *billingWriterStub) SaveBillingAccount(_ context.Context, value Bil
|
||||
}
|
||||
|
||||
type runtimeSettingsRepositoryStub struct {
|
||||
values map[string]string
|
||||
saved map[string]string
|
||||
values map[string]string
|
||||
saved map[string]string
|
||||
saveErr error
|
||||
}
|
||||
|
||||
func (repository *runtimeSettingsRepositoryStub) LoadRuntimeSettings(_ context.Context, _ []string) (map[string]string, error) {
|
||||
@@ -219,6 +329,9 @@ func (repository *runtimeSettingsRepositoryStub) LoadRuntimeSettings(_ context.C
|
||||
}
|
||||
|
||||
func (repository *runtimeSettingsRepositoryStub) SaveRuntimeSettings(_ context.Context, values map[string]string) error {
|
||||
if repository.saveErr != nil {
|
||||
return repository.saveErr
|
||||
}
|
||||
repository.saved = clone(values)
|
||||
if repository.values == nil {
|
||||
repository.values = map[string]string{}
|
||||
|
||||
Reference in new issue
Block a user