Files
weichen d537778b60 fix(sysadmin): refresh feature maturity catalog to post-rollout wiring facts
The static adapter catalog still read I2-era states (degraded/shadow) after
the 2026-09-26 dev rollout, so the admin features page showed 降级 for flags
already enabled. Update six entries to live+ready with evidence-dated
reasons (I2/I3/P8 acceptance, P8 per-key gates kept visible, shadow
visibility digest now persisted, REPLACE wildcard wiring, PI narrative dev
rollout); keep FAV red lines untouched. Tests updated; FAV now carries the
configured-value-must-not-imply-readiness assertion.
2026-09-26 02:45:05 +00:00

236 lines
9.1 KiB
Go
Raw Permalink 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 != "live" || byKey["PI_CHANGE_REPORT_NARRATIVE"].AdapterReadiness != "ready" {
t.Fatalf("narrative must read live+ready after the 2026-09-26 dev rollout (real MiniMax retest PASS; dev flag on): %+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, "2026-09-26") {
t.Fatalf("narrative reason must note the dev rollout date: %+v", byKey["PI_CHANGE_REPORT_NARRATIVE"])
}
for _, key := range []string{"MULTI_INTENT_PLAN_TIER", "MULTI_INTENT_U11_DOCGOV", "MULTI_INTENT_U12_RUN_CONTROL", "SEMANTIC_SUMMARY_SHADOW", "SEMANTIC_SUMMARY_REPLACE"} {
if byKey[key].AdapterReadiness != "ready" || byKey[key].ImplementationState != "live" {
t.Fatalf("%s must read live+ready after I2/I3/P8 wiring and the 2026-09-26 dev rollout: %+v", key, byKey[key])
}
}
if !strings.Contains(byKey["MULTI_INTENT_U11_DOCGOV"].AdapterReason, "逐键") || !strings.Contains(byKey["MULTI_INTENT_U12_RUN_CONTROL"].AdapterReason, "逐键") {
t.Fatalf("U11/U12 reasons must keep the per-key rollout gate visible: %+v %+v", byKey["MULTI_INTENT_U11_DOCGOV"], byKey["MULTI_INTENT_U12_RUN_CONTROL"])
}
}
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",
"FAV_PUBLISHER_ENABLED": "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); after the dev rollout the catalog
// reads live+ready while the empty SESSIONS value still shows the
// defense-in-depth default (replaces nothing until an allowlist is set).
if replace.Value != "true" || replace.ImplementationState != "live" || replace.AdapterReadiness != "ready" {
t.Fatalf("replace must read live+ready after full fail-closed wiring: %+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)
}
// Static wiring facts must not flip with configured values: FAV stays
// dry_run/unavailable even when configured true (red line, no live yet).
fav := byKey["FAV_PUBLISHER_ENABLED"]
if fav.Value != "true" || fav.ImplementationState != "dry_run" || fav.AdapterReadiness != "unavailable" {
t.Fatalf("FAV must keep dry_run/unavailable regardless of configured value: %+v", fav)
}
tier := byKey["MULTI_INTENT_PLAN_TIER"]
if tier.Value != "confirm" || tier.ImplementationState != "live" || tier.AdapterReadiness != "ready" {
t.Fatalf("confirm tier value with I2/I3/P8 wiring must read live+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"])
}
}