Files
Agent-System/internal/sysadmin/service_test.go
T
weichen ffa41d78df feat(ctxmgr): wire fail-closed gray REPLACE for chat semantic narrative
Unseal SEMANTIC_SUMMARY_REPLACE under explicit user authorization with a
defense-in-depth session allowlist (SEMANTIC_SUMMARY_REPLACE_SESSIONS):
swap happens only for allowlisted sessions after result digests and served
watermark validation, bounded narrative size; facts/counts stay deterministic
and the daily digest stays advisory. Master flag alone replaces nothing.
2026-09-25 22:56:36 +00:00

220 lines
8.0 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"]
// 2026-09-26 fail-closed gray wiring: the master bit alone replaces
// nothing (session allowlist required); configured=true must not read as
// rollout-ready.
if replace.Value != "true" || replace.ImplementationState != "shadow" || replace.AdapterReadiness != "degraded" {
t.Fatalf("replace must report shadow/degraded under fail-closed gray: %+v", replace)
}
if !strings.Contains(replace.Description, "白名单") {
t.Fatalf("replace description must surface the allowlist requirement: %+v", replace)
}
allow := byKey["SEMANTIC_SUMMARY_REPLACE_SESSIONS"]
if allow.Value != "" {
t.Fatalf("replace sessions allowlist must default to empty (replaces nothing): %+v", allow)
}
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"])
}
}