mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
fix(内核+插件): 测试写生产账号池 —— 13 个账号被覆盖成 0 个(真实数据事故)
用户报"完全不可用"。逐平台查 account/list,发现 13 个账号里只有 1 个有凭据,
其余是占位壳子 —— 再查发现**账号池被清空了**,而保险库里凭据还在
(症状正是"账号凭空消失")。本提交修的是这个数据事故的根因。
## 根因(全程实测,非推断)
channels 插件用 `storeDir()` 决定自己的存储位置,它有多级兜底:
env → settings → **读 /etc/llmsproxy/config.yaml 的 plugin_dir** → 硬编码。
测试台没设 `LLMSPROXY_PLUGIN_DIR` 时,后两级直接命中**生产**:
测试插件目录 = /tmp/TestXXX/002/plugins
storeDir 结果 = /etc/llmsproxy/plugins ← 实测输出
于是测试加载真 channels.lua 时**读的是生产存储、写的也是生产存储**。
一次全量 `go test ./internal/lua` 就把
`/etc/llmsproxy/plugins/.channels-store.json` 从 1730 覆盖成 1986 字节、
账号池清空(inode 都换了)。
同类前科:曾把 26 个测试账号写进生产 store。两次同一类错误。
## 修法(两层,第二层才是根治)
1. **内核把插件的真实目录交给插件**(ground truth,不再靠猜):
新增 `__llmsproxy_plugin_dir` 全局,在 bootPluginState 里注册。
`storeDir()` 优先用它 —— 这样**即使没有环境变量**也不会落到生产路径。
实测:撤掉 env 后 storeDir 仍落在临时目录。
2. 测试台补隔离:`runtimePlugins` 设 env + `DisableStatePersistence()`
(插件声明了 schedule 时内核会自动起调度器,**后台 goroutine** 比
`t.Setenv` 生命周期长,测试结束后仍可能落盘)。
## 判据 + 变异验证
`internal/lua/store_dir_isolation_test.go`:
- storeDir 绝不能是 /etc/llmsproxy 前缀
- 必须落在临时目录
- env 必须真的被设上(机制而非巧合)
**变异验证**:把隔离设成生产路径 → 判据变红并打印
「storeDir 指向了**生产**目录」;恢复 → 变绿。
全量回归:9 个包全绿,且 `go test ./...` 后生产存储**未被改动**(此前必被改)。
This commit is contained in:
@ -24,6 +24,27 @@ import (
|
||||
// secretGlobal 是凭据保险库的 Lua 名字。
|
||||
const secretGlobal = "__llmsproxy_secrets"
|
||||
|
||||
// pluginDirGlobal carries the plugin's OWN directory as ground truth.
|
||||
//
|
||||
// Why it must come from the kernel (live defect): the channels plugin decides
|
||||
// where to keep its state by reading /etc/llmsproxy/config.yaml when
|
||||
// LLMSPROXY_PLUGIN_DIR is unset. Tests load the real plugin but never set that
|
||||
// variable, so they read AND WROTE the production store — one full
|
||||
// `go test ./internal/lua` replaced the production account pool with the test's
|
||||
// empty one (13 accounts → 0; the credentials stayed in the vault, so the
|
||||
// symptom was "accounts vanished"). The kernel already knows the true directory
|
||||
// (ps.dir), so handing it over removes the guess entirely: a plugin no longer
|
||||
// has to infer its own location from a config file it might not own.
|
||||
const pluginDirGlobal = "__llmsproxy_plugin_dir"
|
||||
|
||||
// registerPluginDirBridge installs the plugin's own directory as a plain string
|
||||
// global. Deliberately a string, not a table: there is nothing to call, and a
|
||||
// bare value cannot be shadowed by a later SetField on a shared table.
|
||||
func registerPluginDirBridge(L *golua.State, dir string) {
|
||||
L.PushString(dir)
|
||||
L.SetGlobal(pluginDirGlobal)
|
||||
}
|
||||
|
||||
// kernelGlobal 是内核动作面的 Lua 名字。
|
||||
const kernelGlobal = "__llmsproxy_kernel"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user