mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 12:53:35 +00:00
内核缺口(都是 P3 落地时暴露的真实缺陷): · **GetAllTools 丢 ParallelSafe**:它只带出 Name/Description/Parameters, 插件看到的设备工具一律"不可并发" ⇒ 设备工具的并发声明**对插件不可见**。 · **ToolAPI 缺按名查**:新增 ToolDefByName。插件需要在**运行前**判断目标 是否存在/是否并发安全(工具动态注册,"不存在"是常态), 而 GetAllTools 只能拿到全量列表。查不到返回 nil,不 panic。 seq 插件: · plugin.go:插件骨架 + kernelRunner(把 sdk.ToolAPI 收窄成三个方法, 判据因此能用假实现驱动,不必构造整个内核) · tools.go:六个工具定义(独立真相源,注册/判据/文档都从它取) · handlers.go:seq_create/list/delete/run/call/when_call 的实现 · register.go + all.go:按 skillmgr 同一范式 init 注册 ★ 过程中解决一个**我自己的设计矛盾**: 判据原先要求 `seq_call` / `seq_when_call` 进黑名单,但"按名调用 group/序列"恰恰是本包的核心能力——禁掉它,序列就退化成单层脚本。 分层澄清后:黑名单只管**对外发消息 / 起子 agent / 改插件表 / 再跑整条 序列**;seq_call 系列留给序列内部组合,其递归由 maxCallDepth + 环检测 负责(设计文档 §8.3 本来就是这么定的,我把两层混了)。 `seq_run` 留在黑名单:序列内再跑整条序列语义上是递归。 **六个工具一律不声明 ParallelSafe**:seq_run/seq_call 会执行一串工具, 其中可能含写操作;标成并发安全会让内核把两条 seq_run 并发跑, 两个序列的执行顺序交错、变量表互相污染。 安全性:序列名与文件路径都做穿越防护(`..` 段、分隔符、空名)。 过程中四次自伤: 1. 臆造 `jsonMarshalIndent`(不存在)→ 改 encoding/json.MarshalIndent; 并把 execGroup 的 runner 传错成 p(应 p.runner)。 2. seq 判据里写了 `black(name)`,而 blacklisted 是**谓词**不是函数。 3. 一次 python 替换删漏,把「跨序列目标存在性检查」那段从 CheckNew 里整段摘掉又贴回原处——靠编译错误发现。 4. ★ 注册失败我写了 panic:内置插件在 main() 装配期加载,panic 会 **直接拖垮内核启动**,而"某个工具没注册上"只该让该工具不可用。 已改为 log.Printf + 继续(与 clawhubadapter / mcp 一致)。 变异验证:把 seq_run 移出黑名单 ⇒ 黑名单判据 FAIL。 判据(plugin_test.go,6 条):六个工具全部注册且 description/参数 schema 非空;seq_create 声明 required 并说明 groups/file 二选一; seq_run 说明"按数组顺序";六个工具均未声明 ParallelSafe; 黑名单含递归风险项且**不误伤** seq_call 与普通工具。 回归:seq -race 全绿;internal/sdk/... internal/plugins/... 12 包全绿。
161 lines
5.7 KiB
Go
161 lines
5.7 KiB
Go
package seq
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// 阶段 P4:seq_* 工具与插件装配。
|
||
//
|
||
// 这一层的判据关注**可观测契约**(给模型看的东西对不对),
|
||
// 执行语义已由 P1/P2/P3 的判据覆盖。
|
||
|
||
// ① 六个工具全部注册,且都带可用的 description 与参数 schema。
|
||
//
|
||
// 工具是**模型可见面**:description 缺失或为空白 ⇒ 模型不知道何时该用它。
|
||
func TestAllSixSeqToolsRegistered(t *testing.T) {
|
||
p := newTestPlugin(t)
|
||
want := []string{
|
||
"seq_create", "seq_list", "seq_delete", "seq_run", "seq_call", "seq_when_call",
|
||
}
|
||
defs := p.toolDefs()
|
||
for _, name := range want {
|
||
def, ok := defs[name]
|
||
if !ok {
|
||
t.Errorf("工具 %s 未注册(已注册:%v)", name, keysOf(defs))
|
||
continue
|
||
}
|
||
if strings.TrimSpace(def.Description) == "" {
|
||
t.Errorf("工具 %s 的 description 为空 —— 模型无从判断何时使用", name)
|
||
}
|
||
if def.Parameters == nil {
|
||
t.Errorf("工具 %s 缺参数 schema", name)
|
||
continue
|
||
}
|
||
if typ, _ := def.Parameters["type"].(string); typ != "object" {
|
||
t.Errorf("工具 %s 的 schema type = %q,期望 object", name, typ)
|
||
}
|
||
}
|
||
if len(defs) != len(want) {
|
||
t.Errorf("应恰好注册 %d 个工具,实际 %d(%v)—— 多余的导出工具会稀释工具面", len(want), len(defs), keysOf(defs))
|
||
}
|
||
}
|
||
|
||
// ② seq_create 的 schema 必须声明 required,否则模型会漏传。
|
||
func TestSeqCreateSchemaDeclaresRequired(t *testing.T) {
|
||
p := newTestPlugin(t)
|
||
def := p.toolDefs()["seq_create"]
|
||
if def.Parameters == nil {
|
||
t.Fatal("seq_create 缺 schema")
|
||
}
|
||
req, _ := def.Parameters["required"].([]string)
|
||
if len(req) == 0 {
|
||
t.Fatalf("seq_create 未声明 required:模型会漏传 name/groups/file")
|
||
}
|
||
got := map[string]bool{}
|
||
for _, r := range req {
|
||
got[r] = true
|
||
}
|
||
if !got["name"] {
|
||
t.Error("required 应包含 name")
|
||
}
|
||
}
|
||
|
||
// ③ seq_create 的 description 必须说明 groups 与 file **二选一**。
|
||
//
|
||
// 这是最容易让模型犯错的地方:两个都传或都不传该怎么<E6808E><E4B988><EFBFBD>,必须写清。
|
||
func TestSeqCreateExplainsMutuallyExclusiveInput(t *testing.T) {
|
||
p := newTestPlugin(t)
|
||
desc := p.toolDefs()["seq_create"].Description
|
||
for _, want := range []string{"groups", "file"} {
|
||
if !strings.Contains(desc, want) {
|
||
t.Errorf("description 未提到 %s:%s", want, desc)
|
||
}
|
||
}
|
||
if !strings.Contains(desc, "二选一") && !strings.Contains(desc, "或") {
|
||
t.Errorf("description 未说明 groups 与 file 是二选一:%s", desc)
|
||
}
|
||
}
|
||
|
||
// ④ 六个工具都**不得**声明并发安全。
|
||
//
|
||
// seq_run / seq_call 会执行**一串**工具,其中可能含写操作;把它们标成
|
||
// 并发安全,会让内核把两条 seq_run 并发跑起来 ⇒ 两个序列的执行顺序
|
||
// 交错、变量表互相污染。
|
||
func TestSeqToolsAreNotParallelSafe(t *testing.T) {
|
||
p := newTestPlugin(t)
|
||
for name := range p.toolDefs() {
|
||
def := p.toolDefs()[name]
|
||
if def.ParallelSafe {
|
||
t.Errorf("工具 %s 声明了 ParallelSafe —— seq 会执行一串工具,并发会污染执行序列", name)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ⑤ seq_run 的 description 必须说明"按 groups 数组顺序执行"。
|
||
//
|
||
// 组的执行顺序是**语义**的一部分:条件依赖前面的槽,顺序反了结果就错。
|
||
func TestSeqRunExplainsOrdering(t *testing.T) {
|
||
p := newTestPlugin(t)
|
||
desc := p.toolDefs()["seq_run"].Description
|
||
if !strings.Contains(desc, "顺序") {
|
||
t.Errorf("seq_run 未说明组按顺序执行:%s", desc)
|
||
}
|
||
}
|
||
|
||
// ⑥ 黑名单:序列**内部**不得调用这些工具(与子 agent 的黑名单同源,防递归)。
|
||
//
|
||
// ⚠️ 这里曾有一个我自己的设计矛盾:判据原先把 `seq_call` / `seq_when_call`
|
||
// 也要求进黑名单,但「按名调用 group/序列」恰恰是本包的核心能力——
|
||
// 若禁掉它,序列就退化成单层脚本,功能归零。
|
||
//
|
||
// 分层澄清(这也是正确语义):
|
||
//
|
||
// · `seq_call` / `seq_when_call` 作为**模型直接调用**的入口是正常的
|
||
// (模型可以单独调某个 group),不算黑名单;
|
||
// · 序列**内部**若写 seq_call,那是按名组合,走 runGroup 的专门分支
|
||
// 并受 maxCallDepth 约束(§8.3 的环检测与深度上界),
|
||
// 不靠黑名单防递归。
|
||
// · 真正要禁的是:对外发消息(output_send__)、起子 agent(spawn_child)、
|
||
// 改插件表(plgreload)、以及再次 seq_run 整条序列(会绕过深度计数的语义)。
|
||
func TestSeqBlacklistCoversRecursionRisks(t *testing.T) {
|
||
for _, name := range []string{
|
||
"output_send__qq", "output_send__x", "spawn_child", "plgreload", "seq_run",
|
||
} {
|
||
if !blacklisted(name) {
|
||
t.Errorf("黑名单未覆盖 %q —— 序列能调它就是递归/绕过风险", name)
|
||
}
|
||
}
|
||
// seq_call / seq_when_call 必须**不在**黑名单,否则按名调用能力归零
|
||
for _, name := range []string{"seq_call", "seq_when_call"} {
|
||
if blacklisted(name) {
|
||
t.Errorf("黑名单误伤了 %q —— 它是序列组合的核心能力(递归由 maxCallDepth + 环检测负责)", name)
|
||
}
|
||
}
|
||
// 普通工具不应被误伤
|
||
for _, name := range []string{"cmd_run", "knowledge_search", "output_list_channels"} {
|
||
if blacklisted(name) {
|
||
t.Errorf("黑名单误伤了正常工具 %q", name)
|
||
}
|
||
}
|
||
}
|
||
|
||
func keysOf(m map[string]toolDefInfo) []string {
|
||
out := make([]string, 0, len(m))
|
||
for k := range m {
|
||
out = append(out, k)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ---- 测试替身 ----
|
||
|
||
// newTestPlugin 造一个**不依赖内核**的 Plugin。
|
||
//
|
||
// 刻意不走 Start(那需要真实 *sdk.PluginSDK):本组判据只测
|
||
// 「工具定义与可观测契约」,不测执行语义(后者由 exec/store 的判据覆盖)。
|
||
func newTestPlugin(t *testing.T) *Plugin {
|
||
t.Helper()
|
||
return &Plugin{name: "seq", store: NewStore(t.TempDir())}
|
||
}
|