Files
HomeAgent/internal/plugin/proc/e2e_secrets_test.go
JianFeeeee 3f834794c5 refactor: SDK module 路径统一改为 hologramagentsdk(59 文件 + go.mod)
## 为什么必须做

SDK 仓(独立仓库)已把 module 从 `github.com/JianFeeeee/homeagentsdk`
改名为 `hologramagentsdk`。主仓通过 `replace` 直接引用它的**物理目录**:

    replace github.com/JianFeeeee/homeagentsdk => ./third_party/homeagent-sdk

一旦 SDK 侧改成新 module 名,主仓立刻炸:

    go: ./third_party/homeagent-sdk@ used for two different module paths
        (github.com/JianFeeeee/hologramagentsdk and github.com/JianFeeeee/homeagentsdk)

Go **不允许**同一目录被两个 module 路径 replace。所以不能"新旧并存",
只能把主仓 59 个文件的 import 全部改到新名,并删掉旧 replace。

## 一个被证伪的前提

我最初按"third_party/homeagent-sdk 是独立仓库,不能在此仓修改其源码"
来处理,并因此判断两个仓的改动可以隔离。实际上主仓是靠 `replace` 指向
该目录的物理副本 —— **两个仓的改动不可能真正隔离**:SDK 侧改 module 名,
主仓必然跟着改 import。

这个前提应该在做任何改动之前用 `go list -m` 验证,而不是假设。

## 连带修掉两处

| 缺陷 | 性质 |
|---|---|
| `TestLuaSDKMockSingleSource` 失败 | SDK 的 `sdk/lua/sdk.lua` 变了,主仓内嵌副本未同步(跑 `scripts/sync-lua-sdk.sh` 修复) |
| `sdkModuleNames` 分散两处 | 已在 SDK 仓合并为单一数据源(见 SDK 仓 725732b) |

第一条是**副本一致性判据抓到的**,不是我自己发现的 —— 这类判据的价值在此:
两个仓必然有副本,靠人记住"改了 A 要同步 B"是做不到的。

## 验证

- `go build ./...` / `go vet ./...` 干净
- `go test ./... -count=1`:**58 包 0 失败**
- `internal/plugin`(Lua SDK 副本一致性)+ `internal/sdk` 全绿
- 主仓 `third_party/homeagent-sdk/` 与 SDK 仓 `main` 无差异
2026-10-23 15:51:02 +08:00

218 lines
6.6 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 proc
// 端到端:插件的**变量库**能力(SDK `Secrets()`)。
//
// ## 这条链路为什么要真机验证
//
// 它与 settings 的 setSecret 是**同一个坑的翻版**:新增 proc method 要改
// **4 个地方**(协议常量 / 外层路由 / 内层分发 / 能力表),少一处就报
// "未知 method"。单测只能证明各段对,证不了接上了。
//
// 而且它跨了**三层**:
//
// ① 插件调 sdk.Secrets().Store
// ② 模板 procSecrets → RPC "secrets.store"
// ③ 内核 coreHandler.secrets → sdk.SecretsAPI(secrets_impl)
// ④ → internal/security.Global() 单例(真加密落盘)
//
// ## 为什么要有这个 API(去重的意义)
//
// 在此之前 commondev 插件自带一套 AES-256-GCM 存储,与内核完全重复。
// 两套库**互不可见**是这个设计最有害的地方 —— 现在统一到一个单例。
import (
"os"
"path/filepath"
"strings"
"testing"
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
internalSDK "github.com/JianFeeeee/HologramAgent/internal/sdk"
internalSecurity "github.com/JianFeeeee/HologramAgent/internal/security"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// secretsE2EPlugin 用真实模板编译:只做变量库往返。
const secretsE2EPlugin = `package main
import (
sdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
type secPlugin struct{ name string }
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &secPlugin{name: name}, nil
}
func (p *secPlugin) Name() string { return p.name }
func (p *secPlugin) Stop() error { return nil }
func (p *secPlugin) Start(s *sdk.PluginSDK) error {
s.RegisterTool("e2e_secrets", sdk.ToolDef{
Description: "变量库往返",
Parameters: map[string]interface{}{"type": "object"},
}, func(args map[string]interface{}) (interface{}, error) {
sec := s.Secrets()
if sec == nil || !sec.Available() {
return "UNAVAILABLE", nil
}
alias, err := sec.Store("e2e-secret-value-9876543210", "e2e")
if err != nil {
return "STORE_ERR: " + err.Error(), nil
}
// ★ 只应拿到别名,拿不到明文
if alias == "" {
return "EMPTY_ALIAS", nil
}
out, n, _ := sec.Fill("token=@{" + alias + "}")
if n != 1 {
return "FILL_MISS", nil
}
return out, nil
})
return nil
}
`
// ★★ 判据:真实模板插件的变量库往返全链路可用。
func TestE2E_RealTemplateSecretsRoundTrip(t *testing.T) {
bin := buildPluginWithRealTemplate(t, secretsE2EPlugin)
dir := t.TempDir()
setTestCodec(t)
// 装配**进程级单例**(内核对所有插件共享的那一份)
if _, err := internalSecurity.InitGlobal(internalSecurity.Options{
Path: filepath.Join(dir, "variables.json"),
KeyFn: func() ([]byte, error) { return []byte("0123456789abcdef0123456789abcdef"), nil },
Enabled: true,
}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { internalSecurity.SetGlobal(nil) })
_ = internalConfig.NewConfigRegistry
host, err := NewHost()
if err != nil {
t.Fatalf("NewHost: %v", err)
}
defer host.Close()
core := newFakeCore()
core.secretsAPI = internalSDK.NewSecretsAPI()
p := New("e2e", bin, dir, nil, host, nil)
if err := p.Start(core); err != nil {
t.Fatalf("Start: %v", err)
}
defer p.Close()
core.mu.Lock()
handler, ok := core.tools["e2e_secrets"]
core.mu.Unlock()
if !ok {
t.Fatal("插件未注册 e2e_secrets 工具")
}
res, err := handler(map[string]interface{}{})
if err != nil {
t.Fatalf("调用失败: %v", err)
}
got, _ := res.(string)
if strings.HasPrefix(got, "UNAVAILABLE") || strings.HasPrefix(got, "STORE_ERR") ||
strings.HasPrefix(got, "EMPTY_ALIAS") || strings.HasPrefix(got, "FILL_MISS") {
t.Fatalf("变量库往返失败: %s", got)
}
// Fill 应把别名换回明文
if !strings.Contains(got, "e2e-secret-value-9876543210") {
t.Fatalf("Fill 未换回明文: %q", got)
}
// 单例里应真的登记了
if internalSecurity.Global().Count() != 1 {
t.Fatalf("单例应有 1 条,实际 %d", internalSecurity.Global().Count())
}
t.Logf("✅ 插件经内核变量库完成往返: %s", got)
}
// ★★ 判据:插件写入的是**内核那一个**变量库(不是自己又建了一套)。
//
// 这是去重的核心断言:插件 Store 之后,内核侧能立刻看到。
func TestE2E_PluginAndKernelShareOneVault(t *testing.T) {
bin := buildPluginWithRealTemplate(t, secretsE2EPlugin)
dir := t.TempDir()
setTestCodec(t)
st, err := internalSecurity.InitGlobal(internalSecurity.Options{
Path: filepath.Join(dir, "variables.json"),
KeyFn: func() ([]byte, error) { return []byte("0123456789abcdef0123456789abcdef"), nil },
Enabled: true,
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { internalSecurity.SetGlobal(nil) })
host, _ := NewHost()
defer host.Close()
core := newFakeCore()
core.secretsAPI = internalSDK.NewSecretsAPI()
p := New("e2e", bin, dir, nil, host, nil)
if err := p.Start(core); err != nil {
t.Fatal(err)
}
defer p.Close()
core.mu.Lock()
handler := core.tools["e2e_secrets"]
core.mu.Unlock()
if _, err := handler(map[string]interface{}{}); err != nil {
t.Fatal(err)
}
// ★ 内核侧(同一个单例)应看到插件登记的那条
infos := st.List()
if len(infos) != 1 {
t.Fatalf("内核单例应有 1 条(插件写的),实际 %d —— 说明插件自建了一套库", len(infos))
}
if infos[0].Note != "e2e" {
t.Errorf("备注未透传: %q", infos[0].Note)
}
// 内核的 Fill 也能用它
if out, n, _ := st.Fill("x=@{" + infos[0].Alias + "}"); n != 1 ||
!strings.Contains(out, "e2e-secret-value") {
t.Errorf("内核 Fill 用不了插件登记的变量: n=%d out=%q", n, out)
}
t.Logf("✅ 插件与内核共用同一个变量库(别名 %s)", infos[0].Alias)
}
// ★ 判据:安全模块未启用时 Available()==false(而非 nil panic)。
func TestE2E_SecretsUnavailableWhenDisabled(t *testing.T) {
bin := buildPluginWithRealTemplate(t, secretsE2EPlugin)
dir := t.TempDir()
internalSecurity.SetGlobal(nil)
t.Cleanup(func() { internalSecurity.SetGlobal(nil) })
host, _ := NewHost()
defer host.Close()
core := newFakeCore()
core.secretsAPI = internalSDK.NewSecretsAPI()
p := New("e2e", bin, dir, nil, host, nil)
if err := p.Start(core); err != nil {
t.Fatal(err)
}
defer p.Close()
core.mu.Lock()
handler := core.tools["e2e_secrets"]
core.mu.Unlock()
res, err := handler(map[string]interface{}{})
if err != nil {
t.Fatalf("不该报错(应优雅报告不可用): %v", err)
}
if got, _ := res.(string); got != "UNAVAILABLE" {
t.Fatalf("未启用时应报 UNAVAILABLE,实际 %q", got)
}
_ = os.Remove
_ = pubsdk.SecretInfo{}
}