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.
220 lines
8.0 KiB
Go
220 lines
8.0 KiB
Go
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"])
|
||
}
|
||
}
|