package app import ( "context" "path/filepath" "testing" "agent-system-backend/internal/rag" "agent-system-backend/internal/settings" "agent-system-backend/internal/store" "agent-system-backend/pkg/extension" ) // EXT-11 红测:注入前 NewScopedHost fail-closed;注入后重建句柄与分发期同构 // (owner 作用域、PluginID、KV 隔离、知识删除前缀强制)。 func TestScopedHostFactory(t *testing.T) { // 注入前:fail-closed。 if _, err := extension.NewScopedHost("alice", "rdm"); err == nil { t.Fatal("before wiring, NewScopedHost must fail closed(工厂未注入)") } st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "t.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { st.Close() }) if err := st.EnsureUser("alice", ""); err != nil { t.Fatal(err) } set := settings.NewService(st) pipe := rag.NewPipeline(st, set, nil, nil) caps := hostcaps{st: st, pipe: pipe, set: set} // 组合根同款注入(app.Run 内以 wireScopedHostFactory 接线;此处直接调内部函数)。 wireScopedHostFactory(caps) t.Cleanup(func() { extension.SetScopedHostFactory(nil) }) h, err := extension.NewScopedHost("alice", "rdm") if err != nil { t.Fatalf("NewScopedHost: %v", err) } if h.Owner() != "alice" || h.PluginID() != "rdm" { t.Fatalf("Owner/PluginID = %s/%s, want alice/rdm", h.Owner(), h.PluginID()) } // KV 往返落 owner 作用域。 if err := h.KVPut("k1", "v1"); err != nil { t.Fatal(err) } if v, ok, err := h.KVGet("k1"); err != nil || !ok || v != "v1" { t.Fatalf("KVGet = %q ok=%v err=%v", v, ok, err) } // 同构性:知识删除前缀强制(manual 不可及)。 if err := h.KnowledgeDelete(context.Background(), "manual"); err == nil { t.Fatal("scoped host must keep the plugin: prefix enforcement") } // 空 owner/pluginID:拒绝。 if _, err := extension.NewScopedHost("", "rdm"); err == nil { t.Fatal("empty owner must be rejected") } }