Why: RDM delegated rename rebuilt the creator Host and could commit a Backend project rename after the delegated operator was disabled.
Evidence: SQLite and isolated PostgreSQL targeted checks cover operator/creator disable order, two processes, sorted locks, auth-off local compatibility, and HTTP/command Host wiring; exact committed-head replay remains pending.
Impact: Adds a request-bound single-project extension capability and checks operator plus creator in the final Backend transaction. Global ScopedHost cannot derive delegated authority; RDM cross-database atomicity remains outside the promise.
Refs: Approved Documents delegated-projection amendment 5783940, U3 A1-A4 candidate adcd8fe, Dev feature/iam-u3-account-directory-recovery.
64 lines
2.1 KiB
Go
64 lines
2.1 KiB
Go
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())
|
||
}
|
||
if _, ok := h.(extension.DelegatedProjectProjectionHost); ok {
|
||
t.Fatal("globally reconstructed Host exposed request-bound projection authority")
|
||
}
|
||
// 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:<id> prefix enforcement")
|
||
}
|
||
// 空 owner/pluginID:拒绝。
|
||
if _, err := extension.NewScopedHost("", "rdm"); err == nil {
|
||
t.Fatal("empty owner must be rejected")
|
||
}
|
||
}
|