Why: Returning true from scratchProject on a lookup error closed the isolation gates correctly, but the same helper also skipped RDM observation and project context for a normal project during a transient store failure. Evidence: go test ./internal/runmgr ./internal/cmdbus ./internal/app ./internal/server ./internal/store pass locally (SELF); CI is authoritative. Impact: confirmedScratchProject (lookup succeeded and marked scratch) drives the relaxing gates; scratchProject (lookup error => true) keeps driving the tightening ones. Refs: Agent-System-Backend#241; agent-exec receipt u2d-run (pi, success) Co-authored-by: Cursor <cursoragent@cursor.com>
465 lines
20 KiB
Go
465 lines
20 KiB
Go
package runmgr
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"agent-native/runtime/memory"
|
||
"agent-system-backend/internal/settings"
|
||
"agent-system-backend/internal/store"
|
||
"agent-system-backend/pkg/extension"
|
||
core "agent-system-core"
|
||
)
|
||
|
||
// 临时会话隔离门禁(U2,runmgr 侧)。产品规则:临时会话是“只属于自己的通用代理
|
||
// 会话”——属主的回合像普通聊天一样跑,但它不读检索、不带 owner/项目级上下文、
|
||
// 不读不写跨会话记忆、也不与任何会话互相读。
|
||
|
||
type scratchRunEnv struct {
|
||
mgr *Manager
|
||
st *store.Store
|
||
svc *settings.Service
|
||
project core.Project // scratch 宿主项目
|
||
scratch core.Conversation
|
||
normal core.Project
|
||
normalAt core.Conversation // 普通项目的调用方会话
|
||
peer core.Conversation // 普通项目的同伴会话
|
||
}
|
||
|
||
func newScratchRunEnv(t *testing.T) *scratchRunEnv {
|
||
t.Helper()
|
||
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "scratch.db"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() { st.Close() })
|
||
if err := st.CreateAccount("alice", "Alice", "", false); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := st.CreateAccount("bob", "Bob", "", false); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
env := &scratchRunEnv{st: st, mgr: &Manager{st: st}, svc: settings.NewService(st)}
|
||
project, conversation, err := st.CreateScratchSessionAccountChecked("alice", "临时会话", "临时会话", "", false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
env.project, env.scratch = project, conversation
|
||
normal, err := st.CreateProject("alice", "普通项目")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
env.normal = normal
|
||
if env.normalAt, err = st.CreateConversation("alice", normal.ID, "调用方", ""); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if env.peer, err = st.CreateConversation("alice", normal.ID, "同伴", ""); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
msg := core.Message{ConversationID: env.peer.ID, Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "同伴内容"}}}
|
||
if err := st.AppendMessage(&msg); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return env
|
||
}
|
||
|
||
// runCtx 造一次“回合视角”:项目 + 会话 + 属主。
|
||
func (e *scratchRunEnv) runCtx(projectID, conversationID, owner string) *runCtx {
|
||
return &runCtx{mgr: e.mgr, eff: e.svc.Effective(owner, projectID),
|
||
run: core.Run{ID: "run-1", Owner: core.ID(owner), ProjectID: core.ID(projectID), ConversationID: core.ID(conversationID)}}
|
||
}
|
||
|
||
func (e *scratchRunEnv) scratchCtx() *runCtx {
|
||
return e.runCtx(string(e.project.ID), e.scratch.ID, "alice")
|
||
}
|
||
|
||
func (e *scratchRunEnv) normalCtx() *runCtx {
|
||
return e.runCtx(string(e.normal.ID), e.normalAt.ID, "alice")
|
||
}
|
||
|
||
// 门禁①:runmgr.authorizeRunWork —— 领域 provider 恒 false 时,scratch 属主照常
|
||
// 工作(provider 从未见过这个项目),其他人 NotFound;普通项目仍由 provider 决定。
|
||
func TestScratchRunWorkUsesOwnerInsteadOfDomainProvider(t *testing.T) {
|
||
env := newScratchRunEnv(t)
|
||
asked := 0
|
||
env.mgr.SetProjectWorkAuthorization(func(context.Context, string, core.ID) (bool, error) {
|
||
asked++
|
||
return false, nil // RDM 视角:永远没有这个项目
|
||
})
|
||
scratchID := core.ID(env.project.ID)
|
||
|
||
if err := env.mgr.authorizeRunWork(context.Background(), "alice", scratchID); err != nil {
|
||
t.Fatalf("scratch owner blocked by the domain provider: %v", err)
|
||
}
|
||
if asked != 0 {
|
||
t.Fatalf("scratch owner work consulted the provider %d times", asked)
|
||
}
|
||
if err := env.mgr.authorizeRunWork(context.Background(), "bob", scratchID); !errors.Is(err, store.ErrNotFound) {
|
||
t.Fatalf("non-owner reached a scratch run: %v", err)
|
||
}
|
||
// 负面:普通项目没有被这条门禁放宽。
|
||
if err := env.mgr.authorizeRunWork(context.Background(), "alice", core.ID(env.normal.ID)); !errors.Is(err, store.ErrNotFound) {
|
||
t.Fatalf("normal project bypassed the provider decision: %v", err)
|
||
}
|
||
// 正面:provider 放行的普通项目照旧放行。
|
||
env.mgr.SetProjectWorkAuthorization(func(context.Context, string, core.ID) (bool, error) { return true, nil })
|
||
if err := env.mgr.authorizeRunWork(context.Background(), "alice", core.ID(env.normal.ID)); err != nil {
|
||
t.Fatalf("provider-approved normal project blocked: %v", err)
|
||
}
|
||
// 无 provider 注册时,非属主仍然进不去 scratch。
|
||
env.mgr.projectWorkAuthorization = nil
|
||
if err := env.mgr.authorizeRunWork(context.Background(), "bob", scratchID); !errors.Is(err, store.ErrNotFound) {
|
||
t.Fatalf("unregistered host opened a scratch run for a non-owner: %v", err)
|
||
}
|
||
}
|
||
|
||
// 门禁②:RDM 预检/观察通道不适用于临时会话。
|
||
func TestScratchRunSkipsDomainPreflightGate(t *testing.T) {
|
||
env := newScratchRunEnv(t)
|
||
preflights := 0
|
||
env.mgr.SetProjectWorkGate(
|
||
func(context.Context, string, string) (extension.ProjectWorkDecision, error) {
|
||
preflights++
|
||
return extension.ProjectWorkDecision{State: extension.ProjectWorkProceed, DecisionID: "d1"}, nil
|
||
},
|
||
func(context.Context, extension.ProjectRiskAnswer) (extension.ProjectWorkDecision, error) {
|
||
return extension.ProjectWorkDecision{State: extension.ProjectWorkProceed, DecisionID: "d1"}, nil
|
||
},
|
||
)
|
||
rc := env.scratchCtx()
|
||
rc.h = &runHandle{}
|
||
if err := rc.requireProjectWorkDecision("conversation.run"); err != nil {
|
||
t.Fatalf("scratch run hit the domain preflight: %v", err)
|
||
}
|
||
if preflights != 0 {
|
||
t.Fatalf("scratch run called the domain preflight %d times", preflights)
|
||
}
|
||
if rc.projectWorkObservationApplies() {
|
||
t.Fatal("scratch run must not enter the RDM observation channel")
|
||
}
|
||
}
|
||
|
||
// 门禁③:项目上下文(X1)即使 provider 已注册也不装配。
|
||
func TestScratchProjectContextDisabled(t *testing.T) {
|
||
env := newScratchRunEnv(t)
|
||
resolved := 0
|
||
env.mgr.SetProjectContextResolver(func(context.Context, extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
||
resolved++
|
||
return extension.ProjectContextBundle{ProjectID: env.project.ID}, nil
|
||
})
|
||
if env.scratchCtx().ProjectContextEnabled() {
|
||
t.Fatal("scratch run assembled project context")
|
||
}
|
||
if resolved != 0 {
|
||
t.Fatalf("scratch run resolved project context %d times", resolved)
|
||
}
|
||
// 正面:普通项目照旧启用。
|
||
if !env.normalCtx().ProjectContextEnabled() {
|
||
t.Fatal("normal project lost project context")
|
||
}
|
||
}
|
||
|
||
// 门禁③bis:ProjectContext 本身(llm.go refresh.ProjectContext、GuardProjectStream
|
||
// 与其它调用方走的同一个入口)在临时会话回合里一次 resolver 都不调;普通项目的
|
||
// 回合照旧调用。
|
||
func TestScratchProjectContextNeverReachesResolver(t *testing.T) {
|
||
env := newScratchRunEnv(t)
|
||
env.mgr.SetProjectWorkAuthorization(func(context.Context, string, core.ID) (bool, error) { return true, nil })
|
||
resolved := 0
|
||
env.mgr.SetProjectContextResolver(func(_ context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
||
resolved++
|
||
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.ProjectID}, nil
|
||
})
|
||
|
||
bundle, active, err := env.scratchCtx().ProjectContext(context.Background(), "临时会话的目标", 900)
|
||
if err != nil || active {
|
||
t.Fatalf("scratch ProjectContext = active:%v err:%v", active, err)
|
||
}
|
||
if bundle.Status != "" || bundle.ProjectID != "" || len(bundle.Items) != 0 || len(bundle.Evidence) != 0 {
|
||
t.Fatalf("scratch ProjectContext returned a bundle: %+v", bundle)
|
||
}
|
||
if resolved != 0 {
|
||
t.Fatalf("scratch run called the project context resolver %d times", resolved)
|
||
}
|
||
// ProjectEvidence 同样不得借此拿到材料。
|
||
if _, err := env.scratchCtx().ProjectEvidence(context.Background(), "ref-a"); err == nil {
|
||
t.Fatal("scratch run read project evidence")
|
||
}
|
||
|
||
// 正面:普通项目的已持久化 run 照旧装一次上下文。
|
||
run := core.Run{ConversationID: env.normalAt.ID, ProjectID: env.normal.ID, Owner: env.normalAt.Owner,
|
||
Kind: core.RunConversation, Status: core.RunPending, Phase: core.PhaseThinking}
|
||
if err := env.st.CreateRun(&run); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
normalRC := env.runCtx(string(env.normal.ID), string(env.normalAt.ID), string(env.normalAt.Owner))
|
||
normalRC.run = run
|
||
if _, active, err := normalRC.ProjectContext(context.Background(), "普通项目的目标", 900); err != nil || !active {
|
||
t.Fatalf("normal project ProjectContext = active:%v err:%v", active, err)
|
||
}
|
||
if resolved != 1 {
|
||
t.Fatalf("normal run called the project context resolver %d times, want 1", resolved)
|
||
}
|
||
}
|
||
|
||
// 门禁③ter:基座项目读不到时 fail closed——本次调用按临时会话处理,且不把这个
|
||
// 未知答案缓存下来(否则一次读失败就会为整个 run 放开记忆、上下文条目与跨会话读取)。
|
||
func TestScratchProjectFailsClosedOnLookupError(t *testing.T) {
|
||
env := newScratchRunEnv(t)
|
||
rc := env.runCtx("prj-missing", string(env.scratch.ID), "alice")
|
||
if !rc.scratchProject() {
|
||
t.Fatal("unreadable host project was treated as a normal project")
|
||
}
|
||
// 未缓存未知答案:换成能读到的项目后重新判定(缓存 false 会在这里露馅)。
|
||
rc.run.ProjectID = env.project.ID
|
||
if !rc.scratchProject() {
|
||
t.Fatal("real scratch project was not recognised after a failed lookup")
|
||
}
|
||
// 正面:读得到时按存储标记判定,普通项目不受影响。
|
||
normal := env.runCtx(string(env.normal.ID), string(env.normalAt.ID), "alice")
|
||
if normal.scratchProject() {
|
||
t.Fatal("normal project was treated as scratch")
|
||
}
|
||
scratch := env.runCtx(string(env.project.ID), string(env.scratch.ID), "alice")
|
||
if !scratch.scratchProject() {
|
||
t.Fatal("scratch project was not recognised")
|
||
}
|
||
// 无项目 run 不受影响(没有基座项目就没有临时会话可谈)。
|
||
if env.runCtx("", string(env.scratch.ID), "alice").scratchProject() {
|
||
t.Fatal("projectless run was treated as scratch")
|
||
}
|
||
}
|
||
|
||
// 门禁③quater:同一件事(基座项目读不到)在两个方向上都 fail closed。收紧型门禁
|
||
// (记忆、上下文条目、检索、跨会话读取、记忆端口)按临时会话处理;放宽型门禁(RDM
|
||
// 观察/预检、项目上下文)按普通项目处理。读到标记之前谁都不能被一次读失败固定下来。
|
||
func TestScratchGatesFailClosedBothWaysOnLookupError(t *testing.T) {
|
||
env := newScratchRunEnv(t)
|
||
preflights := 0
|
||
configure := func(mgr *Manager) {
|
||
mgr.SetProjectWorkGate(
|
||
func(context.Context, string, string) (extension.ProjectWorkDecision, error) {
|
||
preflights++
|
||
return extension.ProjectWorkDecision{}, errors.New("domain preflight unreachable")
|
||
},
|
||
func(context.Context, extension.ProjectRiskAnswer) (extension.ProjectWorkDecision, error) {
|
||
return extension.ProjectWorkDecision{}, errors.New("domain preflight unreachable")
|
||
},
|
||
)
|
||
mgr.SetProjectContextResolver(func(context.Context, extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
||
return extension.ProjectContextBundle{}, nil
|
||
})
|
||
}
|
||
configure(env.mgr)
|
||
|
||
// 基线:普通项目上,两个方向本来就是收紧(不隔离)/ 放宽门生效(观察+上下文)。
|
||
before := env.normalCtx()
|
||
if before.scratchProject() {
|
||
t.Fatal("normal project was treated as scratch")
|
||
}
|
||
if !before.projectWorkObservationApplies() {
|
||
t.Fatal("normal project lost the RDM observation channel")
|
||
}
|
||
if !before.ProjectContextEnabled() {
|
||
t.Fatal("normal project lost project context")
|
||
}
|
||
|
||
// 关掉存储句柄:项目行没变(仍是普通项目),只是 host 标记读不到了。
|
||
if err := env.st.Close(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
rc := env.normalCtx()
|
||
if !rc.scratchProject() {
|
||
t.Fatal("unreadable host project stopped tightening the isolation gates")
|
||
}
|
||
if !rc.projectWorkObservationApplies() {
|
||
t.Fatal("a failed scratch lookup skipped the RDM observation/preflight gate")
|
||
}
|
||
if !rc.ProjectContextEnabled() {
|
||
t.Fatal("a failed scratch lookup disabled project context for a normal project")
|
||
}
|
||
// 观察门真的走到领域预检:门没有因为读不到标记而被整体跳过。
|
||
rc.h = &runHandle{}
|
||
if err := rc.requireProjectWorkDecision("conversation.run"); err == nil {
|
||
t.Fatal("project work gate skipped the domain preflight after a failed lookup")
|
||
}
|
||
if preflights != 1 {
|
||
t.Fatalf("domain preflight consulted %d times after a failed lookup, want 1", preflights)
|
||
}
|
||
|
||
// 未缓存未知答案:读失败后在能读到的存储上重新判定,两个方向都只认存储标记。
|
||
again := newScratchRunEnv(t)
|
||
configure(again.mgr)
|
||
probe := again.runCtx("prj-missing", string(again.scratch.ID), "alice")
|
||
if !probe.scratchProject() || probe.confirmedScratchProject() {
|
||
t.Fatalf("failed lookup did not split the two directions: isolation=%t relaxing=%t",
|
||
probe.scratchProject(), probe.confirmedScratchProject())
|
||
}
|
||
probe.run.ProjectID = again.project.ID
|
||
if !probe.scratchProject() || !probe.confirmedScratchProject() {
|
||
t.Fatal("real scratch project was not recognised after a failed lookup")
|
||
}
|
||
normal := again.normalCtx()
|
||
if normal.scratchProject() || normal.confirmedScratchProject() {
|
||
t.Fatal("normal project was cached as scratch after a failed lookup")
|
||
}
|
||
if !normal.projectWorkObservationApplies() || !normal.ProjectContextEnabled() {
|
||
t.Fatal("readable normal project lost the observation gate or project context")
|
||
}
|
||
// 真实临时会话依旧拿不到这些放宽面。
|
||
if scratch := again.scratchCtx(); scratch.projectWorkObservationApplies() || scratch.ProjectContextEnabled() {
|
||
t.Fatal("scratch project gained the RDM observation gate or project context")
|
||
}
|
||
}
|
||
|
||
// 门禁④:检索为空 —— @knowledge / knowledge_search 在临时会话里拿不到任何东西。
|
||
func TestScratchRunRetrievalReturnsNoHits(t *testing.T) {
|
||
env := newScratchRunEnv(t)
|
||
docs := []core.KnowledgeDoc{
|
||
{Owner: "alice", ProjectID: core.ID(env.project.ID), Source: "manual", Title: "临时项目文档", Chunk: "临时会话项目内的文档"},
|
||
{Owner: "alice", Source: "manual", Title: "属主文档", Chunk: "属主全局文档"},
|
||
}
|
||
if err := env.st.AddKnowledge(docs...); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
query := core.RetrievalQuery{Text: "文档", TopK: 10}
|
||
|
||
hits, err := env.scratchCtx().Retrieve(query)
|
||
if err != nil || len(hits) != 0 {
|
||
t.Fatalf("scratch retrieval must return no hits: %+v %v", hits, err)
|
||
}
|
||
// 工具面(toolCtx.Retrieve)同样空手。
|
||
tc := &toolCtx{rc: env.scratchCtx(), callID: "call-1"}
|
||
if hits, err := tc.Retrieve(query); err != nil || len(hits) != 0 {
|
||
t.Fatalf("scratch tool retrieval must return no hits: %+v %v", hits, err)
|
||
}
|
||
// 正面:普通项目照旧检索得到(这里是项目文档 + 属主文档)。
|
||
hits, err = env.normalCtx().Retrieve(query)
|
||
if err != nil || len(hits) == 0 {
|
||
t.Fatalf("normal project retrieval lost hits: %+v %v", hits, err)
|
||
}
|
||
}
|
||
|
||
// 门禁⑤:上下文注入只留本会话条目;记忆不读不写。
|
||
func TestScratchRunKeepsNoSharedContextAndNoMemory(t *testing.T) {
|
||
env := newScratchRunEnv(t)
|
||
// 三种作用域各一条:属主级、项目级(本临时会话项目)、本会话级。
|
||
for _, entry := range []store.ContextEntry{
|
||
{Keys: []string{"owner"}, Content: "属主级上下文", Enabled: true},
|
||
{Keys: []string{"project"}, ProjectID: env.project.ID, Content: "项目级上下文", Enabled: true},
|
||
{Keys: []string{"conversation"}, ProjectID: env.project.ID, ConversationID: env.scratch.ID, Content: "本会话上下文", Enabled: true},
|
||
} {
|
||
if _, err := env.st.SaveContextEntry("alice", entry); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
got := map[string]string{}
|
||
for _, inj := range env.scratchCtx().Injections() {
|
||
for _, key := range inj.Keys {
|
||
got[key] = inj.Content
|
||
}
|
||
}
|
||
if got["conversation"] != "本会话上下文" {
|
||
t.Fatalf("scratch run lost its own context entry: %+v", got)
|
||
}
|
||
if _, ok := got["owner"]; ok {
|
||
t.Fatalf("owner-wide context entry leaked into a scratch run: %+v", got)
|
||
}
|
||
if _, ok := got["project"]; ok {
|
||
t.Fatalf("project-wide context entry leaked into a scratch run: %+v", got)
|
||
}
|
||
// 正面:普通项目的回合仍然带属主级上下文。
|
||
normalInj := map[string]bool{}
|
||
for _, inj := range env.normalCtx().Injections() {
|
||
for _, key := range inj.Keys {
|
||
normalInj[key] = true
|
||
}
|
||
}
|
||
if !normalInj["owner"] {
|
||
t.Fatalf("normal run lost owner-wide context: %+v", normalInj)
|
||
}
|
||
|
||
// 记忆:临时会话既不读也不写 user/project 记忆。
|
||
if _, err := env.st.PutMemory("alice", core.MemoryEntry{Scope: "user", Type: "note", Content: "属主记忆", Confidence: 0.9}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if entries := env.scratchCtx().Memories(); len(entries) != 0 {
|
||
t.Fatalf("scratch run read user memory: %+v", entries)
|
||
}
|
||
if entries := env.normalCtx().Memories(); len(entries) != 1 || entries[0].Content != "属主记忆" {
|
||
t.Fatalf("normal run lost user memory: %+v", entries)
|
||
}
|
||
|
||
scratchPort := env.scratchCtx().StoreMemory()
|
||
if entries, err := scratchPort.Recall(context.Background(), memory.RecallQuery{Scope: "user"}); err != nil || len(entries) != 0 {
|
||
t.Fatalf("scratch memory port recalled: %+v %v", entries, err)
|
||
}
|
||
if err := scratchPort.Append(context.Background(), memory.Entry{Scope: "user", Type: "note", Content: "临时记忆"}); !errors.Is(err, ErrScratchMemory) {
|
||
t.Fatalf("scratch memory port accepted a user-scope write: %v", err)
|
||
}
|
||
if err := scratchPort.Append(context.Background(), memory.Entry{Scope: "project", Type: "note", Content: "临时项目记忆"}); !errors.Is(err, ErrScratchMemory) {
|
||
t.Fatalf("scratch memory port accepted a project-scope write: %v", err)
|
||
}
|
||
if entries, err := env.st.ListMemory("alice", "", ""); err != nil || len(entries) != 1 {
|
||
t.Fatalf("refused writes reached the store: %+v %v", entries, err)
|
||
}
|
||
// 正面:普通项目的记忆端口照常读写。
|
||
normalPort := env.normalCtx().StoreMemory()
|
||
if err := normalPort.Append(context.Background(), memory.Entry{Scope: "user", Type: "note", Content: "普通记忆"}); err != nil {
|
||
t.Fatalf("normal memory port write: %v", err)
|
||
}
|
||
if entries, err := normalPort.Recall(context.Background(), memory.RecallQuery{Scope: "user"}); err != nil || len(entries) < 2 {
|
||
t.Fatalf("normal memory port recall: %+v %v", entries, err)
|
||
}
|
||
}
|
||
|
||
// 门禁⑥:跨会话读取 —— 临时会话读不到别人;别人的列表里也没有临时会话。
|
||
func TestScratchCrossSessionReadIsIsolated(t *testing.T) {
|
||
env := newScratchRunEnv(t)
|
||
// 临时会话自己的项目开关:即使显式开闸也不放行。
|
||
if err := env.svc.MergeProject("alice", string(env.project.ID), map[string]any{"crossSession": map[string]any{"read": true}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := env.svc.MergeProject("alice", string(env.normal.ID), map[string]any{"crossSession": map[string]any{"read": true}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
scratchTool := &toolCtx{rc: env.scratchCtx(), callID: "call-1"}
|
||
if _, err := scratchTool.PeerConversations(); !errors.Is(err, ErrCrossReadDisabled) {
|
||
t.Fatalf("scratch run listed peers: %v", err)
|
||
}
|
||
if _, err := scratchTool.PeerConversationMessages(env.peer.ID, 0); !errors.Is(err, ErrCrossReadDisabled) {
|
||
t.Fatalf("scratch run read a peer: %v", err)
|
||
}
|
||
|
||
normalTool := &toolCtx{rc: env.normalCtx(), callID: "call-2"}
|
||
convs, err := normalTool.PeerConversations()
|
||
if err != nil {
|
||
t.Fatalf("normal peer listing: %v", err)
|
||
}
|
||
for _, c := range convs {
|
||
if c.ID == env.scratch.ID {
|
||
t.Fatalf("scratch conversation showed up in a normal peer list: %+v", convs)
|
||
}
|
||
}
|
||
found := false
|
||
for _, c := range convs {
|
||
if c.ID == env.peer.ID {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("normal peer listing lost the real peer: %+v", convs)
|
||
}
|
||
if _, err := normalTool.PeerConversationMessages(env.scratch.ID, 0); !errors.Is(err, ErrCrossReadForbidden) {
|
||
t.Fatalf("normal run read a scratch conversation: %v", err)
|
||
}
|
||
// 正面:普通项目的同伴会话照旧可读。
|
||
if msgs, err := normalTool.PeerConversationMessages(env.peer.ID, 0); err != nil || len(msgs) != 1 {
|
||
t.Fatalf("normal peer read: %+v %v", msgs, err)
|
||
}
|
||
}
|