package agent import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // TestRunStep_InjectedToolSurfaceIsAdvertised 钉住审计 P0-12 的下传面: // ctx 上带着(由驱动本步的循环按 capacity 允许集 + 会话环境安全门过滤好的) // 本步工具面时,RunStep 必须**原样**广告它,而不是自己去取 dispatcher 全量 —— // 否则非流式/静音路径的广告面又会绕过能力层与安全闸。 func TestRunStep_InjectedToolSurfaceIsAdvertised(t *testing.T) { t.Parallel() td := dispatcherWith(t, mockTool{name: "terminal", desc: "terminal"}, mockTool{name: "file", desc: "file"}, mockTool{name: "web-search", desc: "web"}, ) ad := &mockAdapter{resp: model.Response{Content: "hi"}} a, err := New("instr", td, nil, WithModel(ad, nil)) if err != nil { t.Fatalf("New: %v", err) } surface := []model.ToolSpec{{Name: "file", Description: "已过滤后的本步工具面"}} ctx := WithToolSurface(context.Background(), surface) if _, err := a.RunStep(ctx, "s", []model.Message{model.TextMessage(model.RoleUser, "hi")}); err != nil { t.Fatalf("RunStep: %v", err) } if len(ad.gotReq.Tools) != 1 { t.Fatalf("req.Tools = %v, want exactly the injected surface", ad.gotReq.Tools) } got := ad.gotReq.Tools[0] if got.Name != "file" || got.Description != surface[0].Description { t.Fatalf("req.Tools[0] = %+v, want the injected spec verbatim", got) } } // TestRunStep_EmptyToolSurfaceAdvertisesNothing 钉住「已挂载但为空」的语义: // 循环算出本步无任何可见工具时(例如全部被安全门收掉),agent 不得回退到 // dispatcher 全量 —— 空面就是空面。 func TestRunStep_EmptyToolSurfaceAdvertisesNothing(t *testing.T) { t.Parallel() td := dispatcherWith(t, mockTool{name: "terminal", desc: "terminal"}) ad := &mockAdapter{resp: model.Response{Content: "hi"}} a, err := New("instr", td, nil, WithModel(ad, nil)) if err != nil { t.Fatalf("New: %v", err) } ctx := WithToolSurface(context.Background(), nil) if _, err := a.RunStep(ctx, "s", []model.Message{model.TextMessage(model.RoleUser, "hi")}); err != nil { t.Fatalf("RunStep: %v", err) } if len(ad.gotReq.Tools) != 0 { t.Fatalf("req.Tools = %v, want none (empty surface must not fall back to the dispatcher)", ad.gotReq.Tools) } } // TestRunStep_NoToolSurfaceKeepsDispatcherFace 钉住回退分支未改变既有行为: // ctx 未挂载工具面(agent 被独立使用、没有循环在过滤)时,仍按 dispatcher // 注册的全量广告 —— 与接上下传面之前逐字节一致。 func TestRunStep_NoToolSurfaceKeepsDispatcherFace(t *testing.T) { t.Parallel() td := dispatcherWith(t, mockTool{name: "terminal", desc: "terminal"}, mockTool{name: "file", desc: "file"}, ) ad := &mockAdapter{resp: model.Response{Content: "hi"}} a, err := New("instr", td, nil, WithModel(ad, nil)) if err != nil { t.Fatalf("New: %v", err) } if _, err := a.RunStep(context.Background(), "s", []model.Message{model.TextMessage(model.RoleUser, "hi")}); err != nil { t.Fatalf("RunStep: %v", err) } if len(ad.gotReq.Tools) != 2 { t.Fatalf("req.Tools = %v, want the two registered tools", ad.gotReq.Tools) } } // TestToolSurfaceFrom_AbsentVersusEmpty 钉住读侧对「未挂载」与「挂载了空面」 // 的区分:前者 ok=false(回退全量),后者 ok=true 且面为空(不广告任何工具)。 func TestToolSurfaceFrom_AbsentVersusEmpty(t *testing.T) { t.Parallel() if _, ok := ToolSurfaceFrom(context.Background()); ok { t.Fatal("ToolSurfaceFrom(background) ok = true, want false") } if _, ok := ToolSurfaceFrom(WithToolSurface(context.Background(), nil)); !ok { t.Fatal("ToolSurfaceFrom(injected empty) ok = false, want true") } ctx := WithToolSurface(context.Background(), []model.ToolSpec{{Name: "file"}}) got, ok := ToolSurfaceFrom(ctx) if !ok || len(got) != 1 || got[0].Name != "file" { t.Fatalf("ToolSurfaceFrom = %v ok=%v, want the mounted surface", got, ok) } }