Files
Cyrene_v2/cmd/cyrene/wire_capability_surface_test.go
AskaEth 321cc3b763
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(backthink): 思考轮工具面补齐(裁决 15)—— 能力面接入 + 只读 capability_list + 本轮状态快照
- 缺口 d:状态在**思考前**读一次,同一份快照既喂思考轮又喂 Decide(读不到 = Known=false,绝不伪装成中性状态)
- 缺口 c:新增只读工具 capability_list(owner-only、三重输出边界、读写隔离),她可自查当下真实可用面
- 缺口 b:内置允许集补 transcode / capability_list(此前 CAPACITY_ENABLED=true 时新工具会从模型可见面消失)
- 缺口 e:guidancePrompt 旧文案承诺的「IoT/日历/提醒」在全仓根本没有对应工具(chat 侧也没有)⇒ 按诚实化改为「与主人对话同等工具面 + 指名自查入口」,不再有空头支票
- (a) 未做且**不自创**:那三把工具无任何设计规格(名字/schema/读写边界/输出上限)⇒ 已报回并登记进 design/20(裁决:走能力/插件/MCP 暴露路线)
- 变异实测:允许集去掉新工具 ⇒ TestDecide_AllowedSets + 组合根跨层测试红;丢掉状态快照 ⇒ TestLoopThinker_Think_InjectsRoundStateBlock 红
- 已知弱项(如实登记):组合根那行 wireCapabilitySurface() 调用本身无测试覆盖(与既有 wireTranscode 同惯例)
2026-10-05 13:54:57 +08:00

193 lines
6.3 KiB
Go
Raw Permalink 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 main
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
"git.yeij.top/AskaEth/Cyrene_v2/internal/iot"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/capacity"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool"
)
// surfaceApp 组装一个只含能力面所需的 app:注册表 + 分发表 + mock IoT 子系统。
// 它走的是与生产相同的两个入口(wireTranscode / wireCapabilitySurface),
// 因此断言的是真实接线,而不是测试自造的替身。
func surfaceApp(t *testing.T) *app {
t.Helper()
caps := registry.New()
d, err := tool.New(caps)
if err != nil {
t.Fatalf("tool.New: %v", err)
}
mgr, err := iot.New(config.IoTConfig{Provider: "mock"}, testLog())
if err != nil {
t.Fatalf("iot.New: %v", err)
}
a := &app{log: testLog(), caps: caps, dispatcher: d, iot: mgr}
a.cfg = &config.Config{}
a.cfg.Capacity.Enabled = true
a.wireTranscode()
a.wireCapabilitySurface()
return a
}
// toolNames 返回注册表里当前所有**可调用工具**的名字(模型真正会看到的那一面)。
func toolNames(a *app) []string {
caps := a.caps.List(registry.KindTool)
out := make([]string, 0, len(caps))
for _, c := range caps {
out = append(out, c.Name)
}
return out
}
// TestWireCapabilitySurface_RegistersIoTCapabilityAndListTool 断言 (a)+(c) 的组合根
// 落地:IoT 子系统以 KindIoT 进能力面(此前全仓无人注册该 kind)、只读的
// capability_list 进注册表并被广告给模型,且它读出的外围状态经真实分发器可用。
func TestWireCapabilitySurface_RegistersIoTCapabilityAndListTool(t *testing.T) {
t.Parallel()
a := surfaceApp(t)
iotCap, err := a.caps.Get(iotCapabilityName)
if err != nil {
t.Fatalf("IoT 子系统没进能力注册表: %v", err)
}
if iotCap.Kind != registry.KindIoT {
t.Fatalf("IoT 能力类型 = %q, want %q", iotCap.Kind, registry.KindIoT)
}
if _, ok := iotCap.Value.(*iot.Manager); !ok {
t.Fatalf("IoT 能力值类型 = %T, want *iot.Manager", iotCap.Value)
}
listCap, err := a.caps.Get(registry.ToolName)
if err != nil {
t.Fatalf("capability_list 没进能力注册表: %v", err)
}
if listCap.Kind != registry.KindTool {
t.Fatalf("清单能力类型 = %q, want %q", listCap.Kind, registry.KindTool)
}
if listCap.Policy.AllowInGroup || listCap.Policy.AllowInStranger {
t.Fatalf("清单工具的策略必须保持 fail-closed(owner-only),got %+v", listCap.Policy)
}
advertised := false
for _, s := range a.dispatcher.ToolSchemas() {
if s.Name == registry.ToolName {
advertised = true
}
}
if !advertised {
t.Fatalf("capability_list 未被广告给模型:%v", toolNames(a))
}
// 走真实分发器(含 JSON-Schema 校验与工具执行),断言外围只读状态真的读得出来。
devices, err := a.iot.List(context.Background())
if err != nil {
t.Fatalf("iot.List: %v", err)
}
val, err := a.dispatcher.Invoke(context.Background(), registry.ToolName, map[string]any{})
if err != nil {
t.Fatalf("Invoke(%s): %v", registry.ToolName, err)
}
raw, err := json.Marshal(val)
if err != nil {
t.Fatalf("marshal: %v", err)
}
text := string(raw)
if !strings.Contains(text, fmt.Sprintf(`"devices=%d`, len(devices))) {
t.Fatalf("清单没报出外围设备台数(want devices=%d):%s", len(devices), text)
}
if !strings.Contains(text, `"name":"iot"`) || !strings.Contains(text, `"kind":"iot"`) {
t.Fatalf("清单没列出 IoT 子系统条目:%s", text)
}
if !strings.Contains(text, `"name":"transcode"`) {
t.Fatalf("清单没列出已注册的 transcode 工具:%s", text)
}
// 只读:清单调用(含外围状态读取)不得改动任何设备。
after, err := a.iot.List(context.Background())
if err != nil {
t.Fatalf("iot.List (after): %v", err)
}
if got, want := deviceSnapshot(after), deviceSnapshot(devices); got != want {
t.Fatalf("清单调用改动了设备状态(只读红线):\nwant=%s\ngot=%s", want, got)
}
}
// deviceSnapshot 把设备清单渲染成可比较的快照(按 id 排序,状态键排序)。
func deviceSnapshot(devices []iot.Device) string {
lines := make([]string, 0, len(devices))
for _, d := range devices {
keys := make([]string, 0, len(d.State))
for k := range d.State {
keys = append(keys, k)
}
sort.Strings(keys)
pairs := make([]string, 0, len(keys))
for _, k := range keys {
pairs = append(pairs, fmt.Sprintf("%s=%v", k, d.State[k]))
}
lines = append(lines, fmt.Sprintf("%s|%s|%s|%t|%s", d.ID, d.Name, d.Type, d.Online, strings.Join(pairs, ",")))
}
sort.Strings(lines)
return strings.Join(lines, "\n")
}
// TestWireCapabilitySurface_NewToolsReachableUnderCapacity 断言 (b):CAPACITY_ENABLED=true
// 时**新工具**(transcode / capability_list)在静息与唤醒两个内置档位集的允许集内。
//
// 判据用**注册表里的真实工具名**驱动判定器:若 signals.go 的档位集字面量与注册名
// 漂移(例如把 capability_list 写成 capability-list),该名字就不在 AllToolNames 里、
// 交集后消失 ⇒ 本用例变红。
func TestWireCapabilitySurface_NewToolsReachableUnderCapacity(t *testing.T) {
t.Parallel()
a := surfaceApp(t)
dec := a.buildCapacity()
if dec == nil || !dec.Enabled() {
t.Fatal("CAPACITY_ENABLED=true 时判定器必须启用")
}
names := toolNames(a)
cases := []struct {
name string
msg string
want capacity.State
}{
{"静息", "你好", capacity.StateQuiet},
{"唤醒", "帮我查一下资料", capacity.StateAwake},
}
for _, c := range cases {
dec := dec.Decide(context.Background(), capacity.Input{
Messages: []string{c.msg},
CurrentState: capacity.StateQuiet,
AllToolNames: names,
})
if dec.State != c.want {
t.Fatalf("输入 %q 判定态 = %s, want %s", c.msg, dec.State, c.want)
}
for _, want := range []string{"transcode", registry.ToolName} {
if !containsTool(dec.AllowedTools, want) {
t.Fatalf("%s档位允许集缺 %q:%v(新工具被挡住 = 裁决 15 在安静态失效)",
c.name, want, dec.AllowedTools)
}
}
}
}
// containsTool 报告名字是否在允许集里。
func containsTool(names []string, want string) bool {
for _, n := range names {
if n == want {
return true
}
}
return false
}