Files
Agent-System-Backend/internal/server/shared_projects_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

100 lines
3.3 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 server
// EXT-15 红测(提案 2026-10-09-ext-g1-b1-b3 §3):成员(经适配器放行)能在
// GET /api/projects 里看到共享项目(shared 标注、去重、不含未授权),并能在
// 共享项目下建会话;撤销后两者立即拒绝;未注册适配器=现状。
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
core "agent-system-core"
)
func TestSharedProjectListAndConversation(t *testing.T) {
srv, st := newAuthTestServer(t)
h := srv.Handler()
alice, err := st.CreateAPIKey("alice", "e2e")
if err != nil {
t.Fatal(err)
}
bob, err := st.CreateAPIKey("bob", "e2e")
if err != nil {
t.Fatal(err)
}
pj, err := st.CreateProject("alice", "共享项目X")
if err != nil {
t.Fatal(err)
}
pid := pj.ID
type pv struct {
ID string `json:"id"`
Shared bool `json:"shared"`
}
call := func(key, method, path string) (*httptest.ResponseRecorder, []pv) {
req := httptest.NewRequest(method, path, nil)
req.Header.Set("Authorization", "Bearer "+key)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
var list []pv
if rec.Body.Len() > 0 {
readJSON(t, rec.Body.Bytes(), &list)
}
return rec, list
}
// 未注册适配器:bob 看不到 alice 的项目(现状)。
rec, list := call(bob, http.MethodGet, "/api/projects")
if rec.Code != http.StatusOK {
t.Fatalf("list: %d", rec.Code)
}
if len(list) != 0 {
t.Fatalf("before adapter: bob sees %d projects, want 0", len(list))
}
// 放行 bob×pid。
srv.SetProjectAuthorizer(func(owner string, projectID core.ID) (bool, error) {
return owner == "bob" && string(projectID) == pid, nil
})
rec, list = call(bob, http.MethodGet, "/api/projects")
if rec.Code != http.StatusOK {
t.Fatalf("list after grant: %d", rec.Code)
}
if len(list) != 1 || list[0].ID != pid || !list[0].Shared {
t.Fatalf("after grant: bob sees %+v, want 1 item id=%s shared=true", list, pid)
}
// alice 本人:仍只看到自己的(无 shared 标注)。
_, alist := call(alice, http.MethodGet, "/api/projects")
if len(alist) != 1 || alist[0].ID != pid || alist[0].Shared {
t.Fatalf("alice own list = %+v, want own without shared", alist)
}
// bob 在共享项目下建会话(EXT-15 第二入口)。
req := httptest.NewRequest(http.MethodPost, "/api/projects/"+pid+"/conversations", bytes.NewReader(mustJSON2(t, map[string]string{"title": "成员会话"})))
req.Header.Set("Authorization", "Bearer "+bob)
req.Header.Set("Content-Type", "application/json")
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("member create conversation: %d %s(须放行)", rec.Code, rec.Body.String())
}
// 撤销:列表回到 0,建会话 404(不泄露存在性)。
srv.SetProjectAuthorizer(func(owner string, projectID core.ID) (bool, error) { return false, nil })
_, list = call(bob, http.MethodGet, "/api/projects")
if len(list) != 0 {
t.Fatalf("after revoke: bob sees %d, want 0", len(list))
}
req = httptest.NewRequest(http.MethodPost, "/api/projects/"+pid+"/conversations", bytes.NewReader(mustJSON2(t, map[string]string{"title": "x"})))
req.Header.Set("Authorization", "Bearer "+bob)
req.Header.Set("Content-Type", "application/json")
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("after revoke: create conversation = %d, want 404", rec.Code)
}
}