Files
ModelRouter/internal/lua/store_dir_isolation_test.go
JianFeeeee af8b93bd7c 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 ./...` 后生产存储**未被改动**(此前必被改)。
2026-10-14 11:31:41 +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/llmsproxy/config.yaml 的 plugin_dir** →
// 硬编码 /etc/llmsproxy/plugins。测试台没设环境变量时,后两级直接命中**生产**:
//
// 测试插件目录 = /tmp/TestXXX/002/plugins
// storeDir 结果 = /etc/llmsproxy/plugins ← 实测
//
// 于是测试加载真 channels.lua 时**读的是生产存储、写的也是生产存储**。一次全量
// `go test ./internal/lua` 就把生产账号池覆盖成测试的空 pool:
// /etc/llmsproxy/plugins/.channels-store.json 从 1730 变 1986 字节,
// 13 个账号变成 0 个(凭据还留在保险库,所以症状是"账号凭空消失")。
//
// 同类前科(本文件存在的另一半理由):曾经有 26 个测试账号被写进生产
// .channels-store.json。两次都是"测试没隔离存储目录",所以这条判据必须存在。
func TestStoreDirIsolatedInTests(t *testing.T) {
_, ps, pdir := runtimePlugins(t)
src, err := bundledAdapters.ReadFile("plugins/channels.lua")
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/llmsproxy/plugins" || strings.HasPrefix(got, "/etc/llmsproxy") {
t.Fatalf("storeDir 指向了**生产**目录 %q —— 测试会覆盖生产账号池\n"+
"(真实事故:13 个账号被覆盖成 0 个)。修法:runtimePlugins 设\n"+
"LLMSPROXY_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("LLMSPROXY_PLUGIN_DIR") == "" {
t.Error("runtimePlugins 必须设 LLMSPROXY_PLUGIN_DIR(否则 storeDir 会读生产 config.yaml)")
}
}