ci / vet-and-test (pull_request) Successful in 1m1s
Why: The active secret_keys row overrides settings plaintext, so a replaced chat key kept returning 401. Migration also mutated the settings map in place, so clearing a matching plaintext never reached the database. Evidence: go test ./internal/settings/ ./internal/runtime/ -count=1 (SELF, worktree). Impact: System settings writes adopt a changed secret; conflicting plaintext is kept and logged; 401 errors name the key layer and an 8-hex fingerprint. Refs: fix/model-key-secret-overlay Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
3.9 KiB
Go
130 lines
3.9 KiB
Go
package settings
|
|
|
|
import (
|
|
"bytes"
|
|
"log"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"agent-system-backend/internal/store"
|
|
)
|
|
|
|
func TestMigrateKeepsPlaintextWhenActiveSecretDiffers(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "secrets.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
svc := NewService(st)
|
|
if _, err := st.AddSecret(OwnerSystemRow, "models.chat", "迁入", "sk-old"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.MergeSettingsScope(OwnerSystemRow, ScopeSystem, map[string]any{
|
|
"models": map[string]any{"chat": map[string]any{"apiKey": "sk-new"}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
prev := log.Default().Writer()
|
|
log.SetOutput(&buf)
|
|
t.Cleanup(func() { log.SetOutput(prev) })
|
|
if err := svc.SnapshotAndMigrateSecrets(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cur, err := st.GetSettingsScope(OwnerSystemRow, ScopeSystem)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, ok := lookupString(cur, "models.chat.apiKey"); !ok || got != "sk-new" {
|
|
t.Fatalf("conflicting plaintext = %q ok=%v", got, ok)
|
|
}
|
|
active, ok := st.ActiveSecret(OwnerSystemRow, "models.chat")
|
|
if !ok || active != "sk-old" {
|
|
t.Fatalf("active secret = %q ok=%v", active, ok)
|
|
}
|
|
if !strings.Contains(buf.String(), "models.chat") {
|
|
t.Fatalf("missing conflict warning: %s", buf.String())
|
|
}
|
|
var snaps int
|
|
if err := st.DB().QueryRow(`SELECT COUNT(*) FROM settings_snapshots WHERE owner=? AND scope=?`, OwnerSystemRow, ScopeSystem).Scan(&snaps); err != nil || snaps == 0 {
|
|
t.Fatalf("startup snapshot count=%d err=%v", snaps, err)
|
|
}
|
|
}
|
|
|
|
func TestMigrateClearsPlaintextWhenActiveSecretMatches(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "secrets.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
svc := NewService(st)
|
|
if _, err := st.AddSecret(OwnerSystemRow, "models.chat", "迁入", "sk-same"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.MergeSettingsScope(OwnerSystemRow, ScopeSystem, map[string]any{
|
|
"models": map[string]any{"chat": map[string]any{"apiKey": "sk-same", "model": "keep-me"}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := svc.SnapshotAndMigrateSecrets(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cur, err := st.GetSettingsScope(OwnerSystemRow, ScopeSystem)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := lookupString(cur, "models.chat.apiKey"); ok {
|
|
t.Fatalf("matching plaintext still stored: %#v", cur)
|
|
}
|
|
if got, _ := lookupString(cur, "models.chat.model"); got != "keep-me" {
|
|
t.Fatalf("non-secret field = %q", got)
|
|
}
|
|
active, ok := st.ActiveSecret(OwnerSystemRow, "models.chat")
|
|
if !ok || active != "sk-same" {
|
|
t.Fatalf("active secret = %q ok=%v", active, ok)
|
|
}
|
|
}
|
|
|
|
func TestSystemMergeAdoptsChangedSecret(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "secrets.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
svc := NewService(st)
|
|
if _, err := st.AddSecret(OwnerSystemRow, "models.chat", "迁入", "sk-old"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := svc.MergeSystem(map[string]any{
|
|
"models": map[string]any{"chat": map[string]any{"apiKey": "sk-new"}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
active, ok := st.ActiveSecret(OwnerSystemRow, "models.chat")
|
|
if !ok || active != "sk-new" {
|
|
t.Fatalf("active secret = %q ok=%v", active, ok)
|
|
}
|
|
var label string
|
|
if err := st.DB().QueryRow(`SELECT label FROM secret_keys WHERE owner=? AND service=? AND active=1`, OwnerSystemRow, "models.chat").Scan(&label); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if label != "播种" {
|
|
t.Fatalf("label = %q", label)
|
|
}
|
|
if err := svc.MergeSystem(map[string]any{
|
|
"models": map[string]any{"chat": map[string]any{"apiKey": "sk-new"}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var n int
|
|
if err := st.DB().QueryRow(`SELECT COUNT(*) FROM secret_keys WHERE owner=? AND service=?`, OwnerSystemRow, "models.chat").Scan(&n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 2 {
|
|
t.Fatalf("secret rows = %d, want the original plus one adoption", n)
|
|
}
|
|
}
|