Files
ModelRouter/internal/lua/plugin_adapter_bridge_test.go
JianFeeeee a0ab9ddda5 feat(lua): 插件适配器管理桥接(R4-a)—— 安装/改写/删除,支撑 token 轮换
用户需求:插件可安装、运行时修改适配器以支持复杂认证与 token 轮换;
适配器本身不需要持续运行(它是无状态请求级弹性池)。

  插件(长期运行)─定期/事件─> install/update/remove_adapter
                                      │
                                      ▼
                      内核重建该适配器的池(新池先发布,旧池后回收)
                                      │
                                      ▼
                          后续请求自然使用新逻辑

三条安全规则(与 R6 同源):
1. 路径安全:ValidateAdapterName(无穿越/绝对路径/空白)
2. 不得影子内置:插件不能注册与内置同名适配器(运维仍可自行覆盖)
3. 归属可查:owner 表记录每个适配器由谁安装,于是"插件只能改自己装的"
   是可判定的,而非靠约定。没有归属信息时,插件 A 能劫持插件 B 的认证逻辑
   而无法被察觉。

修掉两个开发中暴露的真实缺陷:
1. 池替换窗口:LoadAdapterSource 原先在 v.mu 内先 shutdown 旧池再装新池,
   窗口期内请求拿到 "pool closed"。改为**新池先发布、旧池后回收**。
2. 并发轮换仍失败:多个 goroutine 同时轮换时,单次重试会连续落在刚被下个
   轮换关闭的池上。acquireAcrossSwap 改为按**墙钟**(2s 上限)+1ms 退避
   重试新池 —— 轮换是运维的意图,落在微秒窗口里的请求不该因此失败。
   上限保证适配器真消失时仍会及时报错而非空转。
3. 声明名 vs 注册键:脚本可声明与注册键不同的 name(插件按自己控制的键注册,
   但适配器声明源真正引用的名字)。归属表同时记两个键,删除时一并清理;
   EffectiveAdapterName 在声明名下也能查到池。

测试:11 个(安装/轮换/影子内置/跨插件窃取/碰运维适配器/路径穿越/删除/
归属列举/并发 8×20 压力/语法错误安装即拒/声明名上报)。

注:internal/gateway 的变异测试 harness 拒绝在脏工作区运行(设计如此),
故本提交后它才可运行。
2026-10-11 10:03:43 +08:00

265 lines
8.5 KiB
Go
Raw Permalink 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 (
"encoding/json"
"path/filepath"
"strings"
"sync"
"testing"
)
func bridgeVM(t *testing.T) (*VM, *Plugins) {
t.Helper()
dir := filepath.Join(t.TempDir(), "adapters")
vm := NewVM(dir)
if err := vm.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(vm.Stop)
ps := NewPlugins(vm, filepath.Join(t.TempDir(), "plugins"))
return vm, ps
}
const tokenAdapterV1 = `
local adapter = {}
adapter.name = "myauth"
adapter.version = "1.0"
adapter.endpoint = "/chat/completions"
function adapter.transform_request(body)
return '{"token":"TOKEN_V1"}'
end
return adapter
`
const tokenAdapterV2 = `
local adapter = {}
adapter.name = "myauth"
adapter.version = "2.0"
adapter.endpoint = "/chat/completions"
function adapter.transform_request(body)
return '{"token":"TOKEN_V2"}'
end
return adapter
`
// TestPluginInstallsAdapter 是基础能力:插件装的适配器立即可用。
func TestPluginInstallsAdapter(t *testing.T) {
vm, ps := bridgeVM(t)
if _, err := ps.InstallAdapter("authtoken", "myauth", tokenAdapterV1); err != nil {
t.Fatalf("install: %v", err)
}
if !vm.HasFn("myauth", "transform_request") {
t.Fatal("安装后适配器未注册")
}
out, err := vm.Transform("myauth", "transform_request", "{}")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "TOKEN_V1") {
t.Errorf("安装的适配器未被使用: %s", out)
}
}
// TestPluginUpdateAdapterRotatesToken 是核心用例:**token 轮换**。
// 插件长期运行,在运行中改写适配器,后续请求用新 token —— 适配器自身
// 不跑任何循环。
func TestPluginUpdateAdapterRotatesToken(t *testing.T) {
vm, ps := bridgeVM(t)
if _, err := ps.InstallAdapter("authtoken", "myauth", tokenAdapterV1); err != nil {
t.Fatal(err)
}
first, _ := vm.Transform("myauth", "transform_request", "{}")
if !strings.Contains(first, "TOKEN_V1") {
t.Fatalf("初次应带 V1: %s", first)
}
// 轮换:插件改写适配器
if err := ps.UpdateAdapter("authtoken", "myauth", tokenAdapterV2); err != nil {
t.Fatalf("update: %v", err)
}
second, err := vm.Transform("myauth", "transform_request", "{}")
if err != nil {
t.Fatalf("改写后适配器不可用: %v", err)
}
if !strings.Contains(second, "TOKEN_V2") {
t.Errorf("轮换后应带 V2: %s", second)
}
if strings.Contains(second, "TOKEN_V1") {
t.Errorf("轮换后仍带旧 V1(池未重建?): %s", second)
}
// 版本号也应更新,便于运维从 /api/status 看出发生过轮换
if v := vm.AdapterVersion("myauth"); v != "2.0" {
t.Errorf("适配器版本 = %q, want 2.0", v)
}
}
// TestPluginCannotShadowBuiltin:插件不能篡改内核自带的适配器。
func TestPluginCannotShadowBuiltin(t *testing.T) {
_, ps := bridgeVM(t)
evil := `local a={} a.name="openai" a.version="EVIL" a.endpoint="/x" return a`
if _, err := ps.InstallAdapter("sneaky", "openai", evil); err == nil {
t.Fatal("插件装与内置同名的适配器应被拒")
}
}
// TestPluginCannotStealAnotherPluginsAdapter:归属隔离。
func TestPluginCannotStealAnotherPluginsAdapter(t *testing.T) {
_, ps := bridgeVM(t)
if _, err := ps.InstallAdapter("alpha", "shared_auth", tokenAdapterV1); err != nil {
t.Fatal(err)
}
if _, err := ps.InstallAdapter("beta", "shared_auth", tokenAdapterV2); err == nil {
t.Error("插件 B 应无法覆盖插件 A 装的适配器")
}
if err := ps.UpdateAdapter("beta", "shared_auth", tokenAdapterV2); err == nil {
t.Error("插件 B 应无法更新插件 A 的适配器")
}
if err := ps.RemoveAdapter("beta", "shared_auth"); err == nil {
t.Error("插件 B 应无法删除插件 A 的适配器")
}
// 属主自己可以
if err := ps.UpdateAdapter("alpha", "shared_auth", tokenAdapterV2); err != nil {
t.Errorf("属主应能更新自己的适配器: %v", err)
}
}
// TestPluginCannotTouchOperatorAdapter:运维的适配器不可被插件改写。
func TestPluginCannotTouchOperatorAdapter(t *testing.T) {
_, ps := bridgeVM(t)
if err := ps.UpdateAdapter("authtoken", "openai", tokenAdapterV1); err == nil {
t.Error("插件更新运维适配器应被拒")
}
if err := ps.RemoveAdapter("authtoken", "openai"); err == nil {
t.Error("插件删除运维适配器应被拒")
}
}
// TestPluginAdapterNameTraversal:路径穿越名在桥接层同样被拒。
func TestPluginAdapterNameTraversal(t *testing.T) {
_, ps := bridgeVM(t)
for _, bad := range []string{"../evil", "/etc/passwd", "has space", ".."} {
if _, err := ps.InstallAdapter("authtoken", bad, tokenAdapterV1); err == nil {
t.Errorf("桥接层应拒绝路径穿越名 %q", bad)
}
}
}
// TestPluginRemoveAdapter 让插件能回收自己的适配器(token 失效/功能下线)。
func TestPluginRemoveAdapter(t *testing.T) {
vm, ps := bridgeVM(t)
if _, err := ps.InstallAdapter("authtoken", "myauth", tokenAdapterV1); err != nil {
t.Fatal(err)
}
if err := ps.RemoveAdapter("authtoken", "myauth"); err != nil {
t.Fatalf("remove: %v", err)
}
if vm.HasFn("myauth", "transform_request") {
t.Error("删除后适配器仍在")
}
if owners := ps.PluginAdapters("authtoken"); len(owners) != 0 {
t.Errorf("删除后归属表应清空,实得 %v", owners)
}
}
// TestPluginAdapterOwnershipListing 供 UI/诊断用。
func TestPluginAdapterOwnershipListing(t *testing.T) {
_, ps := bridgeVM(t)
if _, err := ps.InstallAdapter("alpha", "a1", tokenAdapterV1); err != nil {
t.Fatal(err)
}
if _, err := ps.InstallAdapter("alpha", "a2", tokenAdapterV1); err != nil {
t.Fatal(err)
}
if _, err := ps.InstallAdapter("beta", "b1", tokenAdapterV1); err != nil {
t.Fatal(err)
}
got := ps.PluginAdapters("alpha")
if len(got) != 2 || got[0] != "a1" || got[1] != "a2" {
t.Errorf("alpha 的适配器 = %v, want [a1 a2]", got)
}
if o := ps.AdapterOwnerOf("b1"); o != "beta" {
t.Errorf("b1 属主 = %q, want beta", o)
}
if o := ps.AdapterOwnerOf("openai"); o != "" {
t.Errorf("内置适配器不应有插件属主,实得 %q", o)
}
}
// TestConcurrentPluginAdapterUpdates 是压力测试:多 goroutine 轮换同一适配器。
// 池重建与归属表都要在并发下自洽(不 panic、不串属主)。
func TestConcurrentPluginAdapterUpdates(t *testing.T) {
vm, ps := bridgeVM(t)
if _, err := ps.InstallAdapter("authtoken", "myauth", tokenAdapterV1); err != nil {
t.Fatal(err)
}
const workers, iters = 8, 20
var wg sync.WaitGroup
errs := make(chan string, workers*iters)
for w := 0; w < workers; w++ {
wg.Add(1)
go func(w int) {
defer wg.Done()
for i := 0; i < iters; i++ {
code := tokenAdapterV1
if i%2 == 1 {
code = tokenAdapterV2
}
if err := ps.UpdateAdapter("authtoken", "myauth", code); err != nil {
errs <- "update: " + err.Error()
return
}
// 更新后必须仍可用(池重建期间不能出现「找不到适配器」)
if _, err := vm.Transform("myauth", "transform_request", "{}"); err != nil {
errs <- "update 后适配器不可用: " + err.Error()
return
}
}
}(w)
}
wg.Wait()
close(errs)
for e := range errs {
t.Error(e)
}
if o := ps.AdapterOwnerOf("myauth"); o != "authtoken" {
t.Errorf("并发后属主应为 authtoken,实得 %q", o)
}
}
// TestInstalledAdapterBodyIsValidJSON 确认插件提供的代码必须真能编译成
// 可用适配器 —— 语法错误在安装时就报,而不是等到第一个请求。
func TestInstalledAdapterBodyIsValidJSON(t *testing.T) {
_, ps := bridgeVM(t)
if _, err := ps.InstallAdapter("bad", "broken", "this is not lua ==="); err == nil {
t.Error("语法错误的适配器应在安装时被拒")
}
if _, err := ps.InstallAdapter("bad", "notatable", "return 42"); err == nil {
t.Error("未返回 table 的适配器应在安装时被拒")
}
// 失败的安装不应留下归属记录
if o := ps.AdapterOwnerOf("broken"); o != "" {
t.Errorf("安装失败却留下了属主记录: %q", o)
}
if o := ps.AdapterOwnerOf("notatable"); o != "" {
t.Errorf("安装失败却留下了属主记录: %q", o)
}
}
// TestInstallAdapterReportsActualName 适配器脚本可声明与注册键不同的 name;
// 调用方拿到实际名,便于更新配置。
func TestInstallAdapterReportsActualName(t *testing.T) {
_, ps := bridgeVM(t)
code := `local a={} a.name="declared_name" a.version="1" a.endpoint="/x" return a`
got, err := ps.InstallAdapter("p", "registration_key", code)
if err != nil {
t.Fatal(err)
}
if got != "declared_name" {
t.Errorf("返回名 = %q, want declared_name", got)
}
if o := ps.AdapterOwnerOf("declared_name"); o != "p" {
t.Errorf("归属应记在声明名上,实得 %q", o)
}
var _ = json.Marshal
}