First Commit
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s

This commit is contained in:
dela
2026-10-09 08:38:16 +08:00
commit 0335d572de
756 changed files with 201663 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
package db
import (
"testing"
"time"
)
// A profile's retry override must survive a full round trip through the real
// column list — this is what catches a mis-ordered scan/insert after adding six
// columns at once. Also pins the "empty api key on update keeps the row usable"
// path, since that UPDATE has its own parameter numbering.
func TestProfileRetryRoundTrip(t *testing.T) {
d, err := Open(testDSN(t))
if err != nil {
t.Skipf("postgres unavailable (%v) — skipping", err)
}
defer d.Close()
want := RetryOverride{
Connect: RetryRule{Attempts: 5, IntervalMS: 2000},
Empty: RetryRule{Attempts: -1},
Stream: RetryRule{IntervalMS: 250},
}
id, err := d.SaveProfile(&LLMProfile{
Name: "t-retry-roundtrip", Format: "openai", Model: "m", APIKey: "k", Retry: want,
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { d.Exec(`DELETE FROM llm_profiles WHERE id=$1`, id) })
got, err := d.ProfileByID(id)
if err != nil || got == nil {
t.Fatalf("ProfileByID: %v", err)
}
if got.Retry != want {
t.Fatalf("retry=%+v, want %+v", got.Retry, want)
}
// Keyless update path (the UI sends no key when the user didn't retype it).
got.APIKey = ""
got.Retry.Stream = RetryRule{Attempts: 3, IntervalMS: 700}
if _, err := d.SaveProfile(got); err != nil {
t.Fatal(err)
}
after, err := d.ProfileByID(id)
if err != nil || after == nil {
t.Fatalf("ProfileByID after update: %v", err)
}
if after.Retry.Stream != (RetryRule{Attempts: 3, IntervalMS: 700}) {
t.Fatalf("stream=%+v after update", after.Retry.Stream)
}
if after.Retry.Connect != want.Connect || after.Retry.Empty != want.Empty {
t.Fatalf("untouched rules changed: %+v", after.Retry)
}
// The listing query reads a different column list — it must agree.
profiles, err := d.ListProfiles()
if err != nil {
t.Fatal(err)
}
for _, p := range profiles {
if p.ID == id && p.Retry.Connect != want.Connect {
t.Fatalf("ListProfiles retry=%+v, want %+v", p.Retry, want)
}
}
}
// Out-of-range values are clamped on the way in, so the DB CHECK constraint is
// never what the user hears about.
func TestProfileRetryClampedOnSave(t *testing.T) {
d, err := Open(testDSN(t))
if err != nil {
t.Skipf("postgres unavailable (%v) — skipping", err)
}
defer d.Close()
id, err := d.SaveProfile(&LLMProfile{
Name: "t-retry-clamp", Format: "openai", Model: "m", APIKey: "k",
Retry: RetryOverride{Connect: RetryRule{Attempts: -99, IntervalMS: -5}},
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { d.Exec(`DELETE FROM llm_profiles WHERE id=$1`, id) })
got, err := d.ProfileByID(id)
if err != nil || got == nil {
t.Fatalf("ProfileByID: %v", err)
}
if got.Retry.Connect != (RetryRule{Attempts: -1}) {
t.Fatalf("connect=%+v, want attempts -1 and no interval", got.Retry.Connect)
}
}
// The global policy round-trips through settings, and an unset key reads back as
// "everything on its built-in default".
func TestLLMRetryPolicyRoundTrip(t *testing.T) {
d, err := Open(testDSN(t))
if err != nil {
t.Skipf("postgres unavailable (%v) — skipping", err)
}
defer d.Close()
before, hadBefore, err := d.GetSetting(settingLLMRetryPolicy)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if hadBefore {
d.SetSetting(settingLLMRetryPolicy, before)
} else {
d.Exec(`DELETE FROM settings WHERE key=$1`, settingLLMRetryPolicy)
}
})
want := LLMRetryPolicy{
Connect: RetryRule{Attempts: 4, IntervalMS: 1500},
Breaker: RetryRule{Attempts: 2, IntervalMS: 90_000},
Intent: RetryRule{Attempts: -1},
}
if err := d.SetLLMRetryPolicy(want); err != nil {
t.Fatal(err)
}
got := d.LLMRetryPolicy()
if got != want {
t.Fatalf("policy=%+v, want %+v", got, want)
}
if d := got.Breaker.Interval(); d != 90*time.Second {
t.Fatalf("breaker interval=%v, want 90s", d)
}
// 越界值写进去也会被夹回区间,读出来是夹紧后的值。
if err := d.SetLLMRetryPolicy(LLMRetryPolicy{Stream: RetryRule{Attempts: 999, IntervalMS: 99_999_999}}); err != nil {
t.Fatal(err)
}
if got := d.LLMRetryPolicy().Stream; got.Attempts != 20 || got.IntervalMS != 3600_000 {
t.Fatalf("stream=%+v, want the 20 / 1h caps", got)
}
// 键不存在 = 全默认。
if _, err := d.Exec(`DELETE FROM settings WHERE key=$1`, settingLLMRetryPolicy); err != nil {
t.Fatal(err)
}
if got := d.LLMRetryPolicy(); got != (LLMRetryPolicy{}) {
t.Fatalf("unset policy=%+v, want zero", got)
}
}