164 lines
4.5 KiB
Go
164 lines
4.5 KiB
Go
package platformsettings
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestValidAIRole(t *testing.T) {
|
||
|
|
for _, role := range AIRoles {
|
||
|
|
if !ValidAIRole(role) {
|
||
|
|
t.Fatalf("%q should be valid", role)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ValidAIRole("embeddings") {
|
||
|
|
t.Fatal("embeddings alias is not a stored role key")
|
||
|
|
}
|
||
|
|
if ValidAIRole("") {
|
||
|
|
t.Fatal("empty role should be invalid")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPublicAIConfigs_masksSecret(t *testing.T) {
|
||
|
|
key := DeriveKey("test-ai-config-secret-material", "fallback")
|
||
|
|
plain := "sk-live-super-secret-key"
|
||
|
|
enc, err := EncryptSecret(key, plain)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
svc := &Service{Key: key}
|
||
|
|
doc := storedDoc{
|
||
|
|
AIConfigs: map[string]aiConfigStored{
|
||
|
|
AIRoleSupport: {
|
||
|
|
Provider: "openai",
|
||
|
|
BaseURL: "https://api.openai.com/v1",
|
||
|
|
Model: "gpt-4o-mini",
|
||
|
|
APIKeyEnc: enc,
|
||
|
|
APIKeyLast4: last4(plain),
|
||
|
|
Enabled: true,
|
||
|
|
Extras: map[string]string{"temperature": "0.2"},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
view := svc.publicAIConfigs(doc)
|
||
|
|
if len(view) != len(AIRoles) {
|
||
|
|
t.Fatalf("expected %d roles, got %d", len(AIRoles), len(view))
|
||
|
|
}
|
||
|
|
got := view[AIRoleSupport]
|
||
|
|
if got.APIKeyMasked == "" || strings.Contains(got.APIKeyMasked, "super-secret") {
|
||
|
|
t.Fatalf("api key not masked: %+v", got)
|
||
|
|
}
|
||
|
|
if got.HasAPIKey != true || got.APIKeyLast4 != last4(plain) {
|
||
|
|
t.Fatalf("unexpected mask meta: %+v", got)
|
||
|
|
}
|
||
|
|
if got.Extras["temperature"] != "0.2" {
|
||
|
|
t.Fatalf("extras: %+v", got.Extras)
|
||
|
|
}
|
||
|
|
if view[AIRoleDocsAPI].Role != AIRoleDocsAPI {
|
||
|
|
t.Fatalf("missing empty role stub: %+v", view[AIRoleDocsAPI])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPublicAIConfigs_processingFallsBackToOpenAI(t *testing.T) {
|
||
|
|
key := DeriveKey("test-ai-config-secret-material", "fallback")
|
||
|
|
plain := "sk-legacy-abcdef12"
|
||
|
|
enc, err := EncryptSecret(key, plain)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
svc := &Service{Key: key}
|
||
|
|
doc := storedDoc{
|
||
|
|
OpenAI: openaiStored{
|
||
|
|
BaseURL: "https://example.test/v1",
|
||
|
|
Model: "gpt-test",
|
||
|
|
APIKeyEnc: enc,
|
||
|
|
APIKeyLast4: last4(plain),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
view := svc.publicAIConfigs(doc)
|
||
|
|
got := view[AIRoleProcessing]
|
||
|
|
if !got.HasAPIKey || got.Source != SourceDB || got.Model != "gpt-test" {
|
||
|
|
t.Fatalf("processing fallback: %+v", got)
|
||
|
|
}
|
||
|
|
if strings.Contains(got.APIKeyMasked, "legacy") {
|
||
|
|
t.Fatalf("leaked key: %q", got.APIKeyMasked)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestResolveAIConfig_envFallback(t *testing.T) {
|
||
|
|
svc := NewService(nil, EnvConfig{
|
||
|
|
OpenAIAPIKey: "env-key-1234",
|
||
|
|
OpenAIBaseURL: "https://api.openai.com/v1",
|
||
|
|
OpenAIModel: "gpt-4o",
|
||
|
|
})
|
||
|
|
got, err := svc.ResolveAIConfig(context.Background(), AIRoleProcessing)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if got.APIKey != "env-key-1234" || got.Source != SourceEnv || !got.Enabled {
|
||
|
|
t.Fatalf("got %+v", got)
|
||
|
|
}
|
||
|
|
support, err := svc.ResolveAIConfig(context.Background(), AIRoleSupport)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if support.APIKey != "" || support.Source != SourceNone {
|
||
|
|
t.Fatalf("support should not use openai env: %+v", support)
|
||
|
|
}
|
||
|
|
vec, err := svc.ResolveAIConfig(context.Background(), AIRoleVectorization)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if vec.APIKey != "env-key-1234" || vec.Source != SourceEnv || vec.Model == "" {
|
||
|
|
t.Fatalf("vectorization env fallback: %+v", vec)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPatchAIExtras(t *testing.T) {
|
||
|
|
var extras map[string]string
|
||
|
|
val := "1536"
|
||
|
|
if err := patchAIExtras(&extras, map[string]*string{"dimensions": &val}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if extras["dimensions"] != "1536" {
|
||
|
|
t.Fatalf("got %#v", extras)
|
||
|
|
}
|
||
|
|
if err := patchAIExtras(&extras, map[string]*string{"dimensions": nil}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if extras != nil {
|
||
|
|
t.Fatalf("expected nil after delete, got %#v", extras)
|
||
|
|
}
|
||
|
|
if err := patchAIExtras(&extras, map[string]*string{"": &val}); err == nil {
|
||
|
|
t.Fatal("expected empty key error")
|
||
|
|
}
|
||
|
|
if err := patchAIExtras(&extras, map[string]*string{"bad key": &val}); err == nil {
|
||
|
|
t.Fatal("expected whitespace key error")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPatchAIConfig_keepsSecretWhenOmitted(t *testing.T) {
|
||
|
|
key := DeriveKey("test-ai-config-secret-material", "fallback")
|
||
|
|
enc, err := EncryptSecret(key, "keep-me-secret")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
svc := &Service{Key: key}
|
||
|
|
st := aiConfigStored{APIKeyEnc: enc, APIKeyLast4: last4("keep-me-secret"), Provider: "openai"}
|
||
|
|
model := "new-model"
|
||
|
|
if err := svc.patchAIConfig(&st, AIConfigUpdate{Model: &model}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if st.APIKeyEnc != enc || st.Model != "new-model" {
|
||
|
|
t.Fatalf("unexpected state: %+v", st)
|
||
|
|
}
|
||
|
|
empty := ""
|
||
|
|
if err := svc.patchAIConfig(&st, AIConfigUpdate{APIKey: &empty}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if st.APIKeyEnc != enc {
|
||
|
|
t.Fatal("empty api_key should keep existing")
|
||
|
|
}
|
||
|
|
}
|