ci / vet-and-test (pull_request) Failing after 44s
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)
100 lines
3.3 KiB
Go
100 lines
3.3 KiB
Go
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)
|
||
}
|
||
}
|