Files
Agent-System/internal/sysadmin/service_test.go
T

210 lines
7.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)
}
}
if byKey["PI_CHANGE_REPORT_NARRATIVE"].ImplementationState != "shadow" || byKey["PI_CHANGE_REPORT_NARRATIVE"].AdapterReadiness != "degraded" {
t.Fatalf("narrative readiness must stay degraded while the flag is off (shadow default), even though pi-agent already mounts --no-tools: %+v", byKey["PI_CHANGE_REPORT_NARRATIVE"])
}
if !strings.Contains(byKey["SEMANTIC_SUMMARY_SHADOW"].AdapterReason, "chat_summaries") {
t.Fatalf("semantic summary reason must reflect schema 61 chat_summaries: %+v", byKey["SEMANTIC_SUMMARY_SHADOW"])
}
if strings.Contains(byKey["SEMANTIC_SUMMARY_SHADOW"].AdapterReason, "尚未在运行环境执行") ||
strings.Contains(byKey["PI_CHANGE_REPORT_NARRATIVE"].AdapterReason, "尚未切到") {
t.Fatalf("readiness reasons must not claim missing migration or missing --no-tools: summary=%+v narrative=%+v",
byKey["SEMANTIC_SUMMARY_SHADOW"], byKey["PI_CHANGE_REPORT_NARRATIVE"])
}
if !strings.Contains(byKey["PI_CHANGE_REPORT_NARRATIVE"].AdapterReason, "待放量授权") {
t.Fatalf("narrative reason must note retest-complete and awaiting rollout auth: %+v", byKey["PI_CHANGE_REPORT_NARRATIVE"])
}
}
func TestFeatures_ConfiguredValueDoesNotImplyReadiness(t *testing.T) {
svc := NewService(testConfig(), Options{
FeatureValues: func() map[string]string {
return map[string]string{
"SEMANTIC_SUMMARY_REPLACE": "true",
"MULTI_INTENT_PLAN_TIER": "confirm",
"MULTI_INTENT_U9_SEGMENT": "true",
}
},
})
byKey := map[string]Feature{}
for _, f := range svc.Features() {
byKey[f.Key] = f
if f.ConfiguredValue != f.Value || f.EffectiveValue != f.Value {
t.Fatalf("env-only flag %s must report the same configured and effective value: %+v", f.Key, f)
}
if f.RolloutScope != "environment" {
t.Fatalf("flag %s rollout scope = %q", f.Key, f.RolloutScope)
}
if f.ImplementationState == "" || f.AdapterReadiness == "" {
t.Fatalf("flag %s missing readiness: %+v", f.Key, f)
}
}
replace := byKey["SEMANTIC_SUMMARY_REPLACE"]
if replace.Value != "true" || replace.ImplementationState != "placeholder" || replace.AdapterReadiness != "unavailable" {
t.Fatalf("replace=true must stay unwired: %+v", replace)
}
tier := byKey["MULTI_INTENT_PLAN_TIER"]
if tier.Value != "confirm" || tier.ImplementationState != "live" || tier.AdapterReadiness != "degraded" {
t.Fatalf("confirm tier value must not imply rollout-ready: %+v", tier)
}
u9 := byKey["MULTI_INTENT_U9_SEGMENT"]
if u9.Value != "true" || u9.ImplementationState != "live" || u9.AdapterReadiness != "ready" {
t.Fatalf("U9 flag on must reflect live HandleTurn wiring: %+v", u9)
}
u10 := byKey["MULTI_INTENT_U10_MUTATE"]
if u10.ImplementationState != "live" || u10.AdapterReadiness != "ready" {
t.Fatalf("U10 live mutate adapter: %+v", u10)
}
if byKey["MULTI_INTENT_ENABLED"].ImplementationState != "live" || byKey["MULTI_INTENT_ENABLED"].AdapterReadiness != "ready" {
t.Fatalf("legacy multi-intent remains the live path: %+v", byKey["MULTI_INTENT_ENABLED"])
}
}