mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-31 22:37:28 +00:00
批 1,不含 SDK 仓与目录改名。 - go.mod 第 1 行 module 路径 - 366 个 .go 文件的完整前缀 github.com/JianFeeeee/HomeAgent 机械替换 (857 处,仅完整前缀;展示名/注释里的 HomeAgent 字样留给批 5) - 未改:third_party 目录名、SDK 自身 go.mod module、go.mod replace 目标、 homed/waiter 可执行名、.hmap 后缀
194 lines
7.3 KiB
Go
194 lines
7.3 KiB
Go
package api
|
||
|
||
// 装配顺序判据。
|
||
//
|
||
// ## 为什么这个顺序值得单独立判据
|
||
//
|
||
// 安全模块的初始化必须在**任何 provider 注册之前**。装晚了的后果是:
|
||
// 先注册的那批 provider 没被装饰 ⇒ **静默漏掉一条出网路径**,
|
||
// 而且没有任何报错、日志也正常 —— 属于最难发现的那类失效。
|
||
//
|
||
// 而这个坑在本仓是真实存在的:
|
||
// · `cmd/homed/main.go` 的 initLLMProviders(主路径)
|
||
// · `internal/sdk/llm_impl.go` 的 ReloadFromConfig(配置热重载)
|
||
// · `cmd/homed/failback.go` 的 runFailbackRecovery(降级救援)
|
||
//
|
||
// 三个地方都会 Register。前两个在 security 之后;failback 也必须之后
|
||
// (main.go 里它是第 174 行,安全模块是 162 行)。
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/security"
|
||
)
|
||
|
||
// ★★ 判据:**晚注册的 provider 也自动被装饰**(因为装饰挂在 Register 上)。
|
||
//
|
||
// 这条钉住的是"挂载点选择正确":如果装饰挂在某个初始化函数里
|
||
// (而不是 Register),那么后注册的 provider 就会漏掉。
|
||
func TestLateRegistrationStillDecorated(t *testing.T) {
|
||
dir := t.TempDir()
|
||
store, err := security.InitGlobal(security.Options{
|
||
Path: filepath.Join(dir, "v.json"),
|
||
KeyFn: func() ([]byte, error) { return []byte("0123456789abcdef0123456789abcdef"), nil },
|
||
Enabled: true,
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() { security.SetGlobal(nil) })
|
||
|
||
const tok = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
m := NewProviderManager()
|
||
|
||
// ① 先注册一个
|
||
var first string
|
||
m.Register("early", &recordingProvider{name: "early", capture: &first})
|
||
|
||
// ② 模拟"热重载":Reset 后注册全新一批(ReloadFromConfig 的真实流程)
|
||
m.Reset()
|
||
var second string
|
||
m.Register("late", &recordingProvider{name: "late", capture: &second})
|
||
|
||
// ③ 再模拟 failback 的 rescue provider
|
||
var third string
|
||
m.Register("rescue", &recordingProvider{name: "rescue", capture: &third})
|
||
|
||
// 三个都必须被装饰
|
||
for _, tc := range []struct {
|
||
name string
|
||
got *string
|
||
}{{"late", &second}, {"rescue", &third}} {
|
||
if _, err := m.Get(tc.name).Chat(context.Background(),
|
||
&CompletionRequest{Messages: []Message{{Role: "user", Content: "t=" + tok}}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if strings.Contains(*tc.got, tok) {
|
||
t.Errorf("provider %s 未被装饰(明文泄漏): %s", tc.name, *tc.got)
|
||
}
|
||
}
|
||
_ = first
|
||
if store.Count() == 0 {
|
||
t.Error("应登记变量")
|
||
}
|
||
}
|
||
|
||
// ★ 判据:**已装饰的不再包第二层**(避免热路径白跑)。
|
||
func TestNoDoubleWrapping(t *testing.T) {
|
||
dir := t.TempDir()
|
||
if _, err := security.InitGlobal(security.Options{
|
||
Path: filepath.Join(dir, "v.json"),
|
||
KeyFn: func() ([]byte, error) { return []byte("0123456789abcdef0123456789abcdef"), nil },
|
||
Enabled: true,
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() { security.SetGlobal(nil) })
|
||
|
||
m := NewProviderManager()
|
||
base := &recordingProvider{name: "x", capture: new(string)}
|
||
m.Register("a", base)
|
||
inner := m.Get("a")
|
||
|
||
// 把已装饰的实例再注册一次(某些装配路径会这么干)
|
||
m.Register("b", inner)
|
||
if _, isDecorator := m.Get("b").(*secureProviderDecorator); !isDecorator {
|
||
t.Fatal("应当仍是装饰实例")
|
||
}
|
||
// 再包一层的话,类型仍是装饰器但会嵌套 —— 用指针比较确认是同一个
|
||
if m.Get("b") != inner {
|
||
t.Error("已装饰的实例不应被再包一层(会白跑一遍正则扫描)")
|
||
}
|
||
}
|
||
|
||
// ★★ 判据:**main.go 里安全模块的初始化必须早于 initLLMProviders**。
|
||
//
|
||
// 这是源码级判据(读文件而不是跑逻辑),因为顺序错误在单测里
|
||
// 构造不出来(main 的装配流程没法在测试里复现)。
|
||
// 源码判据在此是有意为之:它盯的是一个**真实的、曾经坑过的**装配顺序。
|
||
func TestMainInitsSecurityBeforeProviders(t *testing.T) {
|
||
src, err := os.ReadFile("../../../cmd/homed/main.go")
|
||
if err != nil {
|
||
t.Skipf("读不到 main.go(相对路径变了?): %v", err)
|
||
}
|
||
text := string(src)
|
||
|
||
iSec := strings.Index(text, "security.InitGlobal")
|
||
// ★ 锚点已随"内核模块化"改写(2026-10-16)。
|
||
//
|
||
// 原先锚 `initLLMProviders(cfg,`(那时它直接写在 main 里)。
|
||
// 模块化后 **provider 注册搬进了 llm 模块**(`internal/kernelmod/llm`),
|
||
// 它在 `RunKernelModules` 内部执行 ⇒ 旧的字符串在 main.go 里**已经不存在**,
|
||
// 判据因"找不到"而失败(假红)。
|
||
//
|
||
// **注意:这不是放宽判据,而是把锚点换到新的真实装配点。**
|
||
// 不变量本身没变:安全模块必须早于**任何 provider 注册**。
|
||
// 新的等价表述:安全模块早于 `RunKernelModules`(llm 模块在它内部)。
|
||
//
|
||
// 为防止"换个锚点就变绿"式的偷懒,另加两条硬校验(见下)。
|
||
iProv := strings.Index(text, "RunKernelModules(")
|
||
if iSec < 0 {
|
||
t.Fatal("main.go 里找不到 security.InitGlobal —— 安全模块根本没被装配")
|
||
}
|
||
if iProv < 0 {
|
||
t.Fatal("main.go 里找不到 RunKernelModules —— 内核模块装配点没了")
|
||
}
|
||
if iSec > iProv {
|
||
t.Fatalf("装配顺序错误:security.InitGlobal 在第 %d 字节,RunKernelModules 在第 %d 字节\n"+
|
||
"必须先初始化安全模块 —— 内核模块装配时会注册 LLM provider,"+
|
||
"晚于它则 provider 不会被装饰(静默漏掉一条出网路径)",
|
||
iSec, iProv)
|
||
}
|
||
// ★ 加固 ①:provider 注册**确实**在内核模块里(否则新锚点就是空的 ——
|
||
// 那等于把判据锚在一个与 provider 无关的位置上)。
|
||
// 判据:kernelmod/llm 模块必须注册,且其 Init 里出现 initLLMProviders。
|
||
llmSrc, lerr := os.ReadFile("../../kernelmod/llm/mod.go")
|
||
if lerr != nil {
|
||
t.Fatalf("读不到 llm 模块(provider 注册应在这里): %v", lerr)
|
||
}
|
||
if !strings.Contains(string(llmSrc), "initLLMProviders") {
|
||
t.Fatal("★ llm 模块里没有 initLLMProviders —— " +
|
||
"那么上面锚 RunKernelModules 就与 provider 注册无关了(判据会变空)")
|
||
}
|
||
// ★ 加固 ②:llm 模块必须是**真注册过**的(不是死代码)
|
||
allSrc, aerr := os.ReadFile("../../kernelmod/all/all.go")
|
||
if aerr != nil {
|
||
t.Fatalf("读不到模块总表: %v", aerr)
|
||
}
|
||
if !strings.Contains(string(allSrc), "kernelmod/llm") {
|
||
t.Fatal("★ llm 模块没被 import ⇒ 它的 init() 不跑 ⇒ provider 永不注册")
|
||
}
|
||
|
||
// failback 也要在之后
|
||
iFail := strings.Index(text, "runFailbackRecovery")
|
||
if iFail >= 0 && iSec > iFail {
|
||
t.Error("runFailbackRecovery 在安全模块之前 —— rescue provider 会漏装饰")
|
||
}
|
||
}
|
||
|
||
// ★ 判据:阶段断言 —— 源码里装饰发生在 Register 内部(挂载点正确)。
|
||
func TestDecorateHappensInsideRegister(t *testing.T) {
|
||
src, err := os.ReadFile("provider.go")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
text := string(src)
|
||
// Register 函数体内必须出现 WrapProviderForSecurity
|
||
i := strings.Index(text, "func (m *ProviderManager) Register(")
|
||
if i < 0 {
|
||
t.Fatal("找不到 Register")
|
||
}
|
||
// 取到下一个顶层 func 为止
|
||
rest := text[i:]
|
||
if j := strings.Index(rest[1:], "\nfunc "); j > 0 {
|
||
rest = rest[:j+1]
|
||
}
|
||
if !strings.Contains(rest, "WrapProviderForSecurity") {
|
||
t.Fatal("Register 函数体内没有装饰调用 —— 装饰被挪走了(后注册的 provider 会漏掉)")
|
||
}
|
||
}
|