Files
Agent-System/internal/sysadmin/service_test.go
T
weichen f1404d5bd2 feat(sysadmin): add system admin aggregation API and system_settings store
- new internal/sysadmin package (overview/kb/features/settings) with small
  port interfaces; business packages stay unaware of sysadmin
- new routes /api/v1/admin/system/{overview,kb,features,settings} guarded
  by requireSystemAdmin
- goose 00049: system_settings table (key/jsonb value, updated_by/at)
- SettingSpec whitelist registry with DB > env > default precedence
- register sysadmin in module-boundaries.yaml (archtest rules)
2026-09-17 06:56:46 +00:00

152 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package sysadmin
import (
"context"
"encoding/json"
"strings"
"testing"
"chat-agent-system/internal/config"
)
type fakeStatus struct {
ready bool
version int64
}
func (f fakeStatus) DatabaseReady() bool { return f.ready }
func (f fakeStatus) SchemaVersion() int64 {
return f.version
}
func (f fakeStatus) LiveSchemaVersion(_ context.Context) int64 { return f.version + 1 }
type fakeKB struct{ online bool }
func (f fakeKB) Online() bool { return f.online }
func testConfig() *config.Config {
return &config.Config{
Env: "test",
AgentBackend: "stub",
AllowInMemory: true,
KbDbHost: "kb.internal",
KbDbPort: 5432,
KbDbUser: "regkb",
KbDbPassword: "super-secret",
KbDbName: "regulations",
}
}
func TestOverview_AggregatesSources(t *testing.T) {
svc := NewService(testConfig(), Options{
Status: fakeStatus{ready: true, version: 48},
KB: fakeKB{online: true},
TaskStoreReady: func(context.Context) bool {
return true
},
})
ov := svc.Overview(context.Background())
if ov.Status != "ok" {
t.Fatalf("expected ok, got %s", ov.Status)
}
if ov.Env != "test" || ov.AgentBackend != "stub" {
t.Fatalf("unexpected env/backend: %+v", ov)
}
if !ov.DatabaseReady || !ov.TaskStoreReady || !ov.KBOnline {
t.Fatalf("expected all ready: %+v", ov)
}
// dbReady 时使用 LiveSchemaVersion(fake 返回 version+1)
if ov.SchemaVersion != 49 {
t.Fatalf("expected live schema version 49, got %d", ov.SchemaVersion)
}
if ov.UptimeSeconds < 0 {
t.Fatalf("uptime must be >= 0, got %d", ov.UptimeSeconds)
}
}
func TestOverview_DegradedWhenDBDownAndNotInMemory(t *testing.T) {
cfg := testConfig()
cfg.AllowInMemory = false
cfg.ContextDbHost = "127.0.0.1"
svc := NewService(cfg, Options{Status: fakeStatus{ready: false, version: 48}})
ov := svc.Overview(context.Background())
if ov.Status != "degraded" {
t.Fatalf("expected degraded, got %s", ov.Status)
}
// 未就绪时回退启动缓存版本
if ov.SchemaVersion != 48 {
t.Fatalf("expected cached schema version 48, got %d", ov.SchemaVersion)
}
if ov.TaskStoreReady || ov.KBOnline {
t.Fatalf("expected nil probes to report not-ready: %+v", ov)
}
}
func TestKBInfo_NeverLeaksSecrets(t *testing.T) {
svc := NewService(testConfig(), Options{KB: fakeKB{online: true}})
info := svc.KBInfo()
if !info.Online {
t.Fatal("expected online")
}
if info.Host != "kb.internal" || info.Port != 5432 || info.Database != "regulations" || info.User != "regkb" {
t.Fatalf("unexpected connection view: %+v", info)
}
if info.SSLMode != "disable" {
t.Fatalf("expected sslMode=disable (KbDSN fixed), got %s", info.SSLMode)
}
if info.RerankerEnabled {
t.Fatal("reranker endpoint empty must report disabled")
}
// 结构体无密码字段;序列化结果也不得出现密码值
raw, err := json.Marshal(info)
if err != nil {
t.Fatalf("marshal: %v", err)
}
if s := string(raw); strings.Contains(s, "super-secret") || strings.Contains(s, "assword") {
t.Fatalf("KBInfo leaks secret material: %s", s)
}
}
func TestFeatures_CoversCatalogAndInjectedValues(t *testing.T) {
svc := NewService(testConfig(), Options{
FeatureValues: func() map[string]string {
return map[string]string{
"AGENT_BACKEND": "stub",
"FAV_PUBLISHER_ENABLED": "true",
"MULTI_INTENT_PLAN_TIER": "shadow",
}
},
})
features := svc.Features()
if len(features) != len(featureCatalog) {
t.Fatalf("expected %d features, got %d", len(featureCatalog), len(features))
}
byKey := map[string]Feature{}
for _, f := range features {
byKey[f.Key] = f
if f.Source != "env" || !f.RequiresRestart {
t.Fatalf("feature %s must be env-sourced and restart-bound: %+v", f.Key, f)
}
if f.Label == "" || f.Group == "" {
t.Fatalf("feature %s missing label/group: %+v", f.Key, f)
}
}
if byKey["FAV_PUBLISHER_ENABLED"].Value != "true" {
t.Fatalf("expected injected FAV value, got %+v", byKey["FAV_PUBLISHER_ENABLED"])
}
if byKey["FAV_PUBLISHER_ENABLED"].Group != "artifact-vault" {
t.Fatalf("unexpected group: %+v", byKey["FAV_PUBLISHER_ENABLED"])
}
if byKey["MULTI_INTENT_PLAN_TIER"].Value != "shadow" {
t.Fatalf("expected injected plan tier, got %+v", byKey["MULTI_INTENT_PLAN_TIER"])
}
if byKey["INTENT_V2_SHADOW"].Value != "false" {
t.Fatalf("expected catalog fallback false, got %+v", byKey["INTENT_V2_SHADOW"])
}
for _, key := range []string{"AGENT_BACKEND", "AUTH_REGISTRATION_ENABLED", "FAV_DEGRADED_READ", "SEMANTIC_SUMMARY_SHADOW", "PI_CHANGE_REPORT_NARRATIVE"} {
if _, ok := byKey[key]; !ok {
t.Fatalf("catalog must cover %s", key)
}
}
}