- 原缺陷:流式路径经 loop.toolSchemas(capacity 允许集 + restrict)过滤,而 agent 静音/内部路径
(RunStep,被 services/automations 等使用)广告 dispatcher **全量** ⇒ 两条路径不一致
- 修法(方案 1):**过滤实现只留一份**(loop.toolSchemas);l.agentStep 调用同一个函数,结果经
agent.WithToolSurface(ctx, …) 下传,agent 的 buildRequest 原样广告(取不到才回退全量,保持既有回退语义)
· 按构造相同:同一次函数调用、同一份输入 ⇒ 不存在第二份可漂移的实现
· 未选「抽共享函数两侧各调一次」:loop 的过滤输入含 loop 私有会话环境 ctx key 与 capacity 判定器,
agent 侧凑齐等于再造一套 ctx 管线,漂移面更大
- 复核:agent/loop 包测试绿;新增 parity 用例(owner 与 group 两组,静音 == 流式);
变异(撤掉下传行)→ parity 必红且精确复现「静音=全量 4 个」,已还原
- 附注:全仓仅剩 `services/automations` 的「无 Loop 回退」仍广告全量(没有循环就没有闸可言);
子代理 ToolWhitelist 是执行期否决、本不参与 schema 过滤(未改)
102 lines
3.9 KiB
Go
102 lines
3.9 KiB
Go
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)
|
|
}
|
|
}
|