Files
ModelRouter/internal/lua/store_dir_isolation_test.go
JianFeeeee de7553eaa2 feat(lua)!: 插件只通过 modelrouter SDK 与内核交互,且不再随内核安装
- 新增唯一公开接口表 `modelrouter`(sdk_version=1.0):http / secrets /
  kernel / adapters / plugin_dir。成员与内部桥是**同一对象**而非副本 ——
  复制会让凭据保险库分叉(插件写 A、内核读 B)。
- `llmsproxy` 与 `__llmsproxy_*` 降为同对象兼容别名,只为已装插件继续可用。
  仓库内 4 个插件(channels / billing / quota / usage)全部改用
  `modelrouter.*`:此前它们直接依赖内核私有全局,内核无法在不炸插件的前提下
  改实现(重命名、加守卫、换实现都算破坏性变更)。
- 插件不再由网关安装:`SeedBundled()` 变彻底 no-op,`core.New` 不再调用;
  `ReadBundledPlugin` 改为按源文件定位(脱离 cwd),只服务测试与开发工具。
- 桌面端删除 `BUILTIN_PLUGINS` 与 `syncBundledPluginsOnUpgrade`:升级不再
  删除、改名或覆盖用户插件文件。插件是运维资产,不是内核资源。
- `Builtin` 不再靠"源码与仓库示例逐字节相同"推断,插件一律视为外部文件。
- 判据:SDK 存在/版本/形状、别名与内部桥同一张表、适配器 VM 拿不到 SDK、
  经 SDK 真实写读凭据并驱动内核动作、GUI 升级不动插件文件、release 包不含插件。

说明:channels.lua / quota.lua 同时带有此前的渠道与额度改动(含 OpenCode 免费池
监控),与 SDK 改名同处一个文件,无法文件级拆分,故一并纳入。
2026-10-26 22:27:12 +08:00

62 lines
2.5 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 lua
import (
"os"
"strings"
"testing"
)
// **测试绝不允许写生产存储** —— 这条是真实事故的回归判据。
//
// 事故:channels 插件用 storeDir() 决定自己的存储位置,而 storeDir 有多级兜底:
// 环境变量 → settings → **读 /etc/modelrouter/config.yaml 的 plugin_dir** →
// 硬编码 /etc/modelrouter/plugins。测试台没设环境变量时,后两级直接命中**生产**:
//
// 测试插件目录 = /tmp/TestXXX/002/plugins
// storeDir 结果 = /etc/modelrouter/plugins ← 实测
//
// 于是测试加载真 channels.lua 时**读的是生产存储、写的也是生产存储**。一次全量
// `go test ./internal/lua` 就把生产账号池覆盖成测试的空 pool:
// /etc/modelrouter/plugins/.channels-store.json 从 1730 变 1986 字节,
// 13 个账号变成 0 个(凭据还留在保险库,所以症状是"账号凭空消失")。
//
// 同类前科(本文件存在的另一半理由):曾经有 26 个测试账号被写进生产
// .channels-store.json。两次都是"测试没隔离存储目录",所以这条判据必须存在。
func TestStoreDirIsolatedInTests(t *testing.T) {
_, ps, pdir := runtimePlugins(t)
src, err := ReadBundledPlugin("channels")
if err != nil {
t.Fatal(err)
}
if err := ps.LoadSource("channels", string(src)); err != nil {
t.Fatal(err)
}
out, err := ps.Call("channels", "__store_dir_probe", nil)
if err != nil {
t.Fatalf("探针: %v", err)
}
w, _ := out.(map[string]interface{})
j, _ := w["json"].(map[string]interface{})
if j == nil {
t.Fatalf("探针无 json: %#v", out)
}
got, _ := j["store_dir"].(string)
t.Logf("测试插件目录 = %s", pdir)
t.Logf("storeDir 结果 = %s", got)
// ① 绝不能是生产目录
if got == "/etc/modelrouter/plugins" || strings.HasPrefix(got, "/etc/llmsproxy") {
t.Fatalf("storeDir 指向了**生产**目录 %q —— 测试会覆盖生产账号池\n"+
"(真实事故:13 个账号被覆盖成 0 个)。修法:runtimePlugins 设\n"+
"MODELROUTER_PLUGIN_DIR 到 t.TempDir()", got)
}
// ② 必须落在测试的临时目录里
if !strings.Contains(got, pdir) && !strings.HasPrefix(got, os.TempDir()) {
t.Errorf("storeDir 应落在临时目录(%s 或 %s),实得 %q", pdir, os.TempDir(), got)
}
// ③ 环境变量必须真的被设上(机制而非巧合)
if os.Getenv("MODELROUTER_PLUGIN_DIR") == "" {
t.Error("runtimePlugins 必须设 MODELROUTER_PLUGIN_DIR(否则 storeDir 会读生产 config.yaml)")
}
}