X2 C1-3(自排工具面与提示词): - request_think(thinktool.go,照 transcode/tool.go 范式)+ reason 必填/urgency 闭集 + 双守门 (安全闸 + owner 判定:群聊/陌生人一律拒) - **成对**进两张唤醒态允许集(quiet + awake;CAPACITY_TOOL_SETS 是整体替换) - 提示词抽共用段落 + 自排前缀优先(不枚举工具清单);面板加 self_wake 标签与筛选轴 - 4 个变异自检(拿掉 owner 判定/只加一张表/放宽 urgency/前端标签恒等) X3 收口(与 X2 共用 wire_services.go ⇒ 一并落): - 删掉第三份 SSRF 实现(guard.go),改由 io.ValidateRequestURL 唯一判定 + 薄门面(保两个错误身份) - 浏览器会话挂进 a.close(真机:真 Chrome 被关、无残留、生产未扰动) - a.wireBrowser() 归位到 wire_runtime.go,删掉 ioToolOptions 的临时挂点 - 5 个变异自检 顺带补提交 C1-2 漏掉的 internal/config/runtimeconfig/backthink_test.go(ed50053 漏 add)。
252 lines
9.6 KiB
Go
252 lines
9.6 KiB
Go
package main
|
||
|
||
// 本文件钉住切片 V5 的组合根接线:`browser` 工具**只在有可用来源时**进注册面,
|
||
// 每一个「不注册」的原因都**分类**写进日志(照 web-search `off` 的诚实口径),
|
||
// 而 profile 目录的凭据守卫(不得进 git 工作树)在注册之前生效。
|
||
//
|
||
// 断言落在**注册表**(模型真正看到的那一面)与**日志字段**上,而不是源码字符串。
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/browsercfg"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||
browsertools "git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/browser"
|
||
)
|
||
|
||
// browserTestApp 造一个「有注册面 + 有日志出口」的 app:browser 的接线只依赖这三件。
|
||
func browserTestApp(t *testing.T, cfg *config.Config, out *bytes.Buffer) *app {
|
||
t.Helper()
|
||
return &app{
|
||
cfg: cfg,
|
||
log: logger.New(logger.Options{Out: out, Level: "debug"}),
|
||
caps: registry.New(),
|
||
}
|
||
}
|
||
|
||
// writeBrowserConfig 把一份 browser.json 写进 $DATA_DIR/config/plugins/(原样字节)。
|
||
func writeBrowserConfig(t *testing.T, cfg *config.Config, raw string) string {
|
||
t.Helper()
|
||
path := browsercfg.DefaultConfigPath(cfg.DataDir)
|
||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||
t.Fatalf("建配置目录: %v", err)
|
||
}
|
||
if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
|
||
t.Fatalf("写 %s: %v", path, err)
|
||
}
|
||
return path
|
||
}
|
||
|
||
// browserEntry 报告注册面里有没有 browser 工具。
|
||
func browserEntry(a *app) (registry.Capability, bool) {
|
||
cap, err := a.caps.Get(browsertools.ToolName)
|
||
return cap, err == nil
|
||
}
|
||
|
||
// TestWireBrowser_InvalidConfigDoesNotRegister 钉住「配置非法 ⇒ 不注册 + 分类原因」:
|
||
// 静默拿默认值顶替会让面板里那份坏配置永远看不见。
|
||
func TestWireBrowser_InvalidConfigDoesNotRegister(t *testing.T) {
|
||
t.Parallel()
|
||
cfg := wsLoadConfig(t)
|
||
writeBrowserConfig(t, cfg, "{ this is not json")
|
||
var out bytes.Buffer
|
||
a := browserTestApp(t, cfg, &out)
|
||
|
||
a.wireBrowser()
|
||
|
||
if _, ok := browserEntry(a); ok {
|
||
t.Fatal("配置非法时 browser 不该进注册面")
|
||
}
|
||
if got := wsLogField(t, out.String(), "reason"); got != browserReasonInvalidConfig {
|
||
t.Errorf("reason = %q, want %q", got, browserReasonInvalidConfig)
|
||
}
|
||
}
|
||
|
||
// TestWireBrowser_MissingExecutableDoesNotRegister 钉住「面板把路径写错 ⇒ 不注册」,
|
||
// 且原因来自 browsercfg 的分类码(不偷偷回退到自动探测)。
|
||
func TestWireBrowser_MissingExecutableDoesNotRegister(t *testing.T) {
|
||
t.Parallel()
|
||
cfg := wsLoadConfig(t)
|
||
raw, _ := json.Marshal(map[string]any{"executable": filepath.Join(t.TempDir(), "no-such-browser.exe")})
|
||
writeBrowserConfig(t, cfg, string(raw))
|
||
var out bytes.Buffer
|
||
a := browserTestApp(t, cfg, &out)
|
||
|
||
a.wireBrowser()
|
||
|
||
if _, ok := browserEntry(a); ok {
|
||
t.Fatal("可执行文件不存在时 browser 不该进注册面")
|
||
}
|
||
if got := wsLogField(t, out.String(), "reason"); got != string(browsercfg.ReasonExecutableMissing) {
|
||
t.Errorf("reason = %q, want %q", got, browsercfg.ReasonExecutableMissing)
|
||
}
|
||
}
|
||
|
||
// TestWireBrowser_UnreachableEndpointDoesNotRegister 钉住 CDP 端点连不上 ⇒ 不注册,
|
||
// 原因分类为 cdp_unreachable(配置显式给了端点却不可达,就是「我配的容器没生效」,
|
||
// 必须让人看见)。
|
||
func TestWireBrowser_UnreachableEndpointDoesNotRegister(t *testing.T) {
|
||
t.Parallel()
|
||
cfg := wsLoadConfig(t)
|
||
// 127.0.0.1 上一个确定没人听的端口:连不上会立刻返回(不是超时)。
|
||
raw, _ := json.Marshal(map[string]any{"cdpEndpoint": "http://127.0.0.1:1"})
|
||
writeBrowserConfig(t, cfg, string(raw))
|
||
var out bytes.Buffer
|
||
a := browserTestApp(t, cfg, &out)
|
||
|
||
a.wireBrowser()
|
||
|
||
if _, ok := browserEntry(a); ok {
|
||
t.Fatal("CDP 端点不可达时 browser 不该进注册面")
|
||
}
|
||
if got := wsLogField(t, out.String(), "reason"); got != string(browsercfg.ReasonCDPUnreachable) {
|
||
t.Errorf("reason = %q, want %q", got, browsercfg.ReasonCDPUnreachable)
|
||
}
|
||
}
|
||
|
||
// TestWireBrowser_UsableExecutableRegisters 钉住正向路径:配置给出的可执行文件存在
|
||
// ⇒ 工具进注册面,且 profile 目录被按 0700 建好(含凭据的目录不能是 0755)。
|
||
func TestWireBrowser_UsableExecutableRegisters(t *testing.T) {
|
||
t.Parallel()
|
||
cfg := wsLoadConfig(t)
|
||
exe := filepath.Join(t.TempDir(), "fake-browser.exe")
|
||
if err := os.WriteFile(exe, []byte("#!/bin/sh\n"), 0o700); err != nil {
|
||
t.Fatalf("造假可执行文件: %v", err)
|
||
}
|
||
profileDir := filepath.Join(cfg.DataDir, "browser", "profile-x")
|
||
raw, _ := json.Marshal(map[string]any{"executable": exe, "profileDir": profileDir})
|
||
writeBrowserConfig(t, cfg, string(raw))
|
||
var out bytes.Buffer
|
||
a := browserTestApp(t, cfg, &out)
|
||
|
||
a.wireBrowser()
|
||
|
||
cap, ok := browserEntry(a)
|
||
if !ok {
|
||
t.Fatalf("可用时 browser 应进注册面(日志:%s)", out.String())
|
||
}
|
||
if cap.Kind != registry.KindTool {
|
||
t.Errorf("Kind = %q, want tool", cap.Kind)
|
||
}
|
||
// 默认权限口径:owner-only(两布尔皆 false)。
|
||
if cap.Policy.AllowInGroup || cap.Policy.AllowInStranger {
|
||
t.Errorf("Policy = %+v,默认应是 owner-only(design/28 §4 第 2 条)", cap.Policy)
|
||
}
|
||
if _, err := os.Stat(profileDir); err != nil {
|
||
t.Errorf("profile 目录应先被建好: %v", err)
|
||
}
|
||
// 工具名/动作面:注册进去的那把工具就是薄层那一个。
|
||
if tool, ok := cap.Value.(interface{ Name() string }); !ok || tool.Name() != browsertools.ToolName {
|
||
t.Errorf("注册的值不是 browser 工具: %#v", cap.Value)
|
||
}
|
||
}
|
||
|
||
// TestWireBrowser_ProfileInGitTreeIsRejected 钉住凭据守卫:profile 落在 git 工作树里
|
||
// ⇒ **不注册**(被 `git add` 收走就是把登录态提交出去,design/28 §4)。
|
||
func TestWireBrowser_ProfileInGitTreeIsRejected(t *testing.T) {
|
||
t.Parallel()
|
||
cfg := wsLoadConfig(t)
|
||
exe := filepath.Join(t.TempDir(), "fake-browser.exe")
|
||
if err := os.WriteFile(exe, []byte("x"), 0o700); err != nil {
|
||
t.Fatalf("造假可执行文件: %v", err)
|
||
}
|
||
// 造一个「像仓库」的目录:里面有 `.git`,profile 落在它下面、且在 dataDir 之外。
|
||
repo := t.TempDir()
|
||
if err := os.MkdirAll(filepath.Join(repo, ".git"), 0o755); err != nil {
|
||
t.Fatalf("造 .git: %v", err)
|
||
}
|
||
profileDir := filepath.Join(repo, "browser-profile")
|
||
raw, _ := json.Marshal(map[string]any{"executable": exe, "profileDir": profileDir})
|
||
writeBrowserConfig(t, cfg, string(raw))
|
||
var out bytes.Buffer
|
||
a := browserTestApp(t, cfg, &out)
|
||
|
||
a.wireBrowser()
|
||
|
||
if _, ok := browserEntry(a); ok {
|
||
t.Fatal("profile 位于 git 工作树内时 browser 不该进注册面")
|
||
}
|
||
if got := wsLogField(t, out.String(), "reason"); got != browserReasonProfileRejected {
|
||
t.Errorf("reason = %q, want %q", got, browserReasonProfileRejected)
|
||
}
|
||
}
|
||
|
||
// TestIOToolOptions_DoesNotWireBrowser 钉住**临时挂点已删**(切片 X3 把挂点归位到
|
||
// `wireRuntime`):browser 的接线原先借 `ioToolOptions()` 转交(V5 改不了
|
||
// wire_runtime.go 时的权宜),代价是「取一份 io 选项」这件只读的事会顺带探测并注册
|
||
// 浏览器工具 —— 所有只为要选项而调本函数的路径(web-search 的接线测试就是)都会连带
|
||
// 起一次浏览器探测。归位后的挂点在 wire_runtime.go,由
|
||
// TestWireRuntime_CallsWireBrowser 钉住;这里钉住 io 侧**不再有第二个挂点** ——
|
||
// 两处并存不会报错(函数幂等),只会静默多一次探测,因此只有测试能拦住它回流。
|
||
func TestIOToolOptions_DoesNotWireBrowser(t *testing.T) {
|
||
t.Parallel()
|
||
cfg := wsLoadConfig(t)
|
||
exe := filepath.Join(t.TempDir(), "fake-browser.exe")
|
||
if err := os.WriteFile(exe, []byte("x"), 0o700); err != nil {
|
||
t.Fatalf("造假可执行文件: %v", err)
|
||
}
|
||
raw, _ := json.Marshal(map[string]any{"executable": exe})
|
||
writeBrowserConfig(t, cfg, string(raw))
|
||
var out bytes.Buffer
|
||
a := browserTestApp(t, cfg, &out)
|
||
|
||
a.ioToolOptions()
|
||
|
||
if _, ok := browserEntry(a); ok {
|
||
t.Fatalf("ioToolOptions 不该再挂 browser(挂点已归位 wire_runtime.go;日志:%s)", out.String())
|
||
}
|
||
}
|
||
|
||
// TestWireBrowser_Idempotent 钉住**幂等**:同一个 app 上重复装配只发现工具已在注册表里
|
||
// 并跳过,不重复注册、不重复探测。挂点归位后幂等仍是唯一的保险(两条路径万一偶遇)。
|
||
func TestWireBrowser_Idempotent(t *testing.T) {
|
||
t.Parallel()
|
||
cfg := wsLoadConfig(t)
|
||
exe := filepath.Join(t.TempDir(), "fake-browser.exe")
|
||
if err := os.WriteFile(exe, []byte("x"), 0o700); err != nil {
|
||
t.Fatalf("造假可执行文件: %v", err)
|
||
}
|
||
raw, _ := json.Marshal(map[string]any{"executable": exe})
|
||
writeBrowserConfig(t, cfg, string(raw))
|
||
var out bytes.Buffer
|
||
a := browserTestApp(t, cfg, &out)
|
||
|
||
a.wireBrowser()
|
||
first, ok := browserEntry(a)
|
||
if !ok {
|
||
t.Fatalf("可用时 browser 应进注册面(日志:%s)", out.String())
|
||
}
|
||
a.wireBrowser()
|
||
second, ok := browserEntry(a)
|
||
if !ok {
|
||
t.Fatal("第二次调用后 browser 不该消失")
|
||
}
|
||
if first.Name != second.Name || first.Kind != second.Kind {
|
||
t.Errorf("重复装配改写了条目:%+v -> %+v", first, second)
|
||
}
|
||
// 注册面里只该有一个 browser(registry 重名会被拒;这里确认没被静默换掉)。
|
||
count := 0
|
||
for _, c := range a.caps.List(registry.KindTool) {
|
||
if c.Name == browsertools.ToolName {
|
||
count++
|
||
}
|
||
}
|
||
if count != 1 {
|
||
t.Errorf("注册面里 browser 条目数 = %d, want 1", count)
|
||
}
|
||
}
|
||
|
||
// TestWireBrowser_NilRegistryIsSkipped 钉住夹具友好性:没有注册面时**不 panic**
|
||
// (部分既有测试夹具只填了 app 的几个字段)。
|
||
func TestWireBrowser_NilRegistryIsSkipped(t *testing.T) {
|
||
t.Parallel()
|
||
a := &app{}
|
||
a.wireBrowser()
|
||
}
|