Files
HomeAgent/internal/agent/core/inputch_test.go
JianFeeeee 3c961b7bd7 feat(channel): N1a —— inputch 一等化(归属插件/归属 agent/容量/共享登记表)+ 单工具多视图总览
设计:docs/zh/resident-subagent-design.md §4.6(里程碑 N1a)。

## 用户要求

「父 agent 可以看到所有已注册的 inputch 以及 inputch 的划分情况,用**单工具多视图**方式构筑」

## 登记层(internal/agent/io/inputch.go)

inputch 是**最基本的输入路由单位**(由插件注册,一个插件可注册多个),
所以登记表以 inputch 为键,每条记录:

    名字 / 归属插件 / 归属 agent(被划给谁) / 容量 / 默认回程 outputch / 记忆策略(ChannelDef)

- 新增 `ChannelRegistry`,设计成**可共享对象**(`*ChannelRegistry`):
  根 agent 与驻留子共用同一份,"划入/授权"才有意义;`SetChannelRegistry` 注入。
- **插件重载不得抹掉划分**:重复登记只更新「归属插件 + 策略」,
  保留已有 Owner/Capacity/Output(否则一次 reload 就把父做的划分清空)。
- `Assign` 语义按设计 R6 默认:**读写授权,不转移所有权**(Plugin 与 Owner 分别记录)。
- 原 `inputChannels map[string]ChannelDef` 被登记表取代;`GetInputChannelDef` 保持兼容。
- `plugin.Registry` 注册时带上**归属插件名**(此前完全无归属信息)。

## 总览工具(internal/agent/core/inputch.go)

单工具 **`input_channels`** + `view` 参数(不是一堆小工具):

    all(默认)= 全部已注册(带归属插件)
    mine       = 划给本 agent 的
    unassigned = 尚未划出的
    by_agent   = 划分情况总览(按归属分组)
    detail     = 单个 inputch 全字段(需 name)

未知 view **报错并列出可用值**(拼错不得被静默当成默认视图);登记表为空时明确说明。

## 验收

- `internal/agent/io/inputch_test.go`:归属记录、重载保划分、Assign/视图数据面、
  **跨 manager 共享登记表**、策略查询向后兼容 —— 5 项
- `internal/agent/core/inputch_test.go`:单工具多视图逐视图断言(含未知 view 与空表)—— 2 项
- 全仓 `go test ./...` 37 包 ok / 0 FAIL;`-race ./internal/agent/... ./internal/plugin/...` 干净
2026-09-13 09:06:50 +08:00

105 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package core
// inputch 总览工具(单工具多视图)的验收。
//
// 需求:父 agent 能看到**所有已注册的 inputch**以及**它们的划分情况**。
// 构筑方式单工具input_channels+ 多视图view=all|mine|unassigned|by_agent|detail
import (
"strings"
"testing"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
)
func inputChToolCall(args map[string]interface{}) agentAPI.ToolCall {
return agentAPI.ToolCall{ID: "ic1", Name: "input_channels", Arguments: args}
}
func TestInputChannelsTool_SingleToolMultipleViews(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
me := string(a.id)
// 同一个插件注册多个 inputch最基本的输入路由单位另一个插件再注册一个。
if err := a.io.RegisterInputChannelFrom("qq", "qq", agentIO.ChannelDef{NoMemory: true}); err != nil {
t.Fatal(err)
}
if err := a.io.RegisterInputChannelFrom("qq", "qq/device-2", agentIO.ChannelDef{}); err != nil {
t.Fatal(err)
}
if err := a.io.RegisterInputChannelFrom("sub", "sub/in", agentIO.ChannelDef{}); err != nil {
t.Fatal(err)
}
// 划分qq 与 sub/in 归本 agentqq/device-2 留未分配。
if err := a.io.AssignInputChannel("qq", me, 64); err != nil {
t.Fatal(err)
}
if err := a.io.AssignInputChannel("sub/in", me, 0); err != nil {
t.Fatal(err)
}
// view=all默认全部已注册且带归属插件。
all := a.executeInputChannels(inputChToolCall(nil))
for _, want := range []string{"qq", "qq/device-2", "sub/in", "插件 qq", "插件 sub", "无记忆"} {
if !strings.Contains(all, want) {
t.Fatalf("view=all 缺少 %q\n%s", want, all)
}
}
// view=mine只有划给本 agent 的。
mine := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "mine"}))
if !strings.Contains(mine, "qq") || !strings.Contains(mine, "sub/in") {
t.Fatalf("view=mine 应含 qq 与 sub/in\n%s", mine)
}
if strings.Contains(mine, "qq/device-2") {
t.Fatalf("view=mine 不应含未分配的 qq/device-2\n%s", mine)
}
// view=unassigned只有尚未划出的。
un := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "unassigned"}))
if !strings.Contains(un, "qq/device-2") {
t.Fatalf("view=unassigned 应含 qq/device-2\n%s", un)
}
if strings.Contains(un, "sub/in") {
t.Fatalf("view=unassigned 不应含已划分的 sub/in\n%s", un)
}
// view=by_agent划分情况总览按归属分组
byAgent := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "by_agent"}))
if !strings.Contains(byAgent, me+":") {
t.Fatalf("view=by_agent 应列出归属 %q\n%s", me, byAgent)
}
if !strings.Contains(byAgent, "未分配") {
t.Fatalf("view=by_agent 应列出未分配一组:\n%s", byAgent)
}
// view=detail单个 inputch 的全字段(容量 / 策略 / 回程 / 归属)。
detail := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "detail", "name": "qq"}))
for _, want := range []string{"inputch: qq", "注册插件: qq", "容量: 64", "无记忆", me} {
if !strings.Contains(detail, want) {
t.Fatalf("view=detail 缺少 %q\n%s", want, detail)
}
}
if miss := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "detail", "name": "nope"})); !strings.Contains(miss, "未注册") {
t.Fatalf("detail 查未注册的 inputch 应明确报错:%s", miss)
}
if noName := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "detail"})); !strings.Contains(noName, "需要 name") {
t.Fatalf("detail 缺 name 应提示:%s", noName)
}
// 未知视图必须报错并列出可用值(不要把拼错静默当成默认视图)。
bad := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "whatever"}))
if !strings.Contains(bad, "未知 view") || !strings.Contains(bad, "by_agent") {
t.Fatalf("未知 view 应报错并列出可用值:%s", bad)
}
}
// 一个 inputch 都没有时应给出明确说明,而不是空串。
func TestInputChannelsTool_NoChannels(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
if out := a.executeInputChannels(inputChToolCall(nil)); !strings.Contains(out, "没有任何已注册") {
t.Fatalf("空登记表应明确说明:%q", out)
}
}