Files
Agent-System-Backend/internal/app/ext13_shared_search_test.go
T
weichen 439865616a
ci / vet-and-test (pull_request) Failing after 44s
Share retrieval inside runs and open project listing/conversations to members (EXT-13/15)
Why: A member's agent cannot see published project knowledge inside a run
(runCtx.Retrieve and hostcaps.search are owner-only), and members can
neither see shared projects nor open conversations in them - both are P0
prerequisites for RDM G1 (EXT-13/EXT-15, proposal
2026-10-09-ext-g1-b1-b3-exec-backend).
Evidence: Red tests first - TestRunContextRetrieveShared (undefined
SetSharedKnowledgeAllow), TestInprocHostKnowledgeSearchShared (0 shared
hits), TestSharedProjectListAndConversation (empty list) all failed before
and pass after; go test ./... 32/32 packages ok.
Impact: runmgr gains SetSharedKnowledgeAllow and routes Retrieve through
RetrieveShared when the predicate is registered (nil = unchanged); hostcaps
search consults the same EXT-8 slot; GET /api/projects appends
adapter-authorized shared projects (projectView with shared flag, deduped);
POST /api/projects/{id}/conversations falls back to
CreateConversationInProject when the own-project path misses and the
adapter authorizes; store gains ListAllProjects (candidates only, callers
must filter through the adapter) and CreateConversationInProject.
Revocation takes effect immediately on both paths; absence of an adapter
is byte-for-byte the previous behavior. No pkg/extension surface change.
Refs: docs/proposals/2026-10-09-ext-g1-b1-b3-exec-backend.md (EXT-13, EXT-15)
2026-10-09 15:44:48 +00:00

70 lines
2.7 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 app
// EXT-13 红测(提案 2026-10-09-ext-g1-b1-b3 §1):进程内 Host.KnowledgeSearch
// 的检索面在适配器放行后吃到他人项目的共享知识;未放行保持本人两路、零泄露。
import (
"context"
"path/filepath"
"testing"
"agent-system-backend/internal/rag"
"agent-system-backend/internal/settings"
"agent-system-backend/internal/store"
core "agent-system-core"
)
// newExt13Host EXT-13 装配:真实 store + rag 管线 + authz 槽 + inprocHost(bob 视角)。
func newExt13Host(t *testing.T) (interface {
KnowledgeSearch(ctx context.Context, projectID core.ID, text string, topK int) ([]core.RetrievalHit, error)
}, *projectAuthz) {
t.Helper()
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
set := settings.NewService(st)
pipe := rag.NewPipeline(st, set, nil, nil)
authz := &projectAuthz{}
h := inprocHost{caps: hostcaps{st: st, pipe: pipe, set: set, authz: authz}, pluginID: "rdm", owner: "bob"}
// alice 已发布的项目知识(带 ProjectID;模拟发布状态机产物)。
if err := st.AddKnowledge(core.KnowledgeDoc{Owner: "alice", ProjectID: "p-1",
Source: "plugin:rdm", Ref: "ext13-1", Chunk: "EXT13共享独占令牌陈述"}); err != nil {
t.Fatal(err)
}
return h, authz
}
func TestInprocHostKnowledgeSearchShared(t *testing.T) {
h, authz := newExt13Host(t)
ctx := context.Background()
q := core.RetrievalQuery{Text: "EXT13共享独占令牌", TopK: 5}
if hits, err := h.KnowledgeSearch(ctx, "", q.Text, q.TopK); err != nil || len(hits) != 0 {
t.Fatalf("before grant: hits=%d err=%v, want 0(未放行零泄露)", len(hits), err)
}
// 适配器放行 bob × p-1 → Host 检索面吃到 alice 的共享命中。
// (槽为首个注册者生效——切换授权改闭包内变量,不重注册。)
allowBob := true
authz.set("rdm", func(owner string, projectID core.ID) (bool, error) {
return allowBob && owner == "bob" && string(projectID) == "p-1", nil
})
hits, err := h.KnowledgeSearch(ctx, "", q.Text, q.TopK)
if err != nil {
t.Fatal(err)
}
if len(hits) == 0 {
t.Fatal("after grant: host search got 0 shared hits, want ≥1(EXT-13)")
}
// 指定他人 ProjectID 时同样经适配器(发布边界在下游状态机)。
if hits, err := h.KnowledgeSearch(ctx, "p-1", q.Text, q.TopK); err != nil || len(hits) == 0 {
t.Fatalf("scoped to shared project: hits=%d err=%v, want ≥1", len(hits), err)
}
// 撤销(同一注册内翻转授权)→ 共享命中即停、零泄露。
allowBob = false
if hits, err := h.KnowledgeSearch(ctx, "", q.Text, q.TopK); err != nil || len(hits) != 0 {
t.Fatalf("after revoke: hits=%d err=%v, want 0", len(hits), err)
}
}