package lua import ( "testing" "time" ) // 本文件验证 R4 的 Lua 侧绑定:**插件自己的 Lua 代码**能在运行时安装/改写/ // 删除自己的适配器。 // // 为什么需要单独一层测试:Go 侧的 InstallAdapter/UpdateAdapter 在 // plugin_adapter_bridge_test.go 里已直接验证过,但那证明的是"Go API 能工作"。 // token 轮换的真实形态是插件在定时任务或钩子里**从 Lua 发起**这个调用,所以 // 绑定本身(__llmsproxy_adapters 表)必须被证明可用 —— 而且它曾经在 cgo 层 // SIGSEGV,所以"不崩"本身就是一条断言。 const bridgeProbe = ` local plugin = { name = "authtoken", version = "1.0" } plugin.exports = { "install_via_lua", "update_via_lua", "remove_via_lua", "install_operator_owned" } local function src(ver, tok) return 'return { name = "myauth", version = "' .. ver .. '", endpoint = "/x", token = "' .. tok .. '" }' end function plugin.install_via_lua(args) local name, err = __llmsproxy_adapters.install("myauth", src("1.0", args.tok)) return { name = name, err = err } end function plugin.update_via_lua(args) local ok, err = __llmsproxy_adapters.update("myauth", src("2.0", args.tok)) return { ok = ok, err = err } end function plugin.remove_via_lua() local ok, err = __llmsproxy_adapters.remove("myauth") return { ok = ok, err = err } end -- 试图替换一个非自己拥有的适配器:必须被拒(不能影子内置) function plugin.install_operator_owned() local name, err = __llmsproxy_adapters.install("openai", src("9.9", "evil")) return { name = name, err = err } end return plugin ` // TestPluginInstallsAdapterFromLua 是 R4 绑定的主断言。 func TestPluginInstallsAdapterFromLua(t *testing.T) { _, ps, _ := runtimePlugins(t) if err := ps.LoadSource("authtoken", bridgeProbe); err != nil { t.Fatal(err) } out, err := ps.Call("authtoken", "install_via_lua", map[string]interface{}{"tok": "T1"}) if err != nil { t.Fatalf("Lua 侧 install 调用失败: %v", err) } m, ok := out.(map[string]interface{}) if !ok { t.Fatalf("返回值不是 table: %#v", out) } if m["name"] != "myauth" { t.Fatalf("install 应返回生效名 myauth, 实得 %#v (err=%#v)", m["name"], m["err"]) } // 归属确实被记录(这条规则是"插件只能改自己装的"的基础) if owner := ps.AdapterOwnerOf("myauth"); owner != "authtoken" { t.Errorf("归属应为 authtoken, 实得 %q", owner) } } // TestPluginRotatesTokenViaLua 是 token 轮换的完整形态: // 同一个插件先装、再改,改后新版本生效。 func TestPluginRotatesTokenViaLua(t *testing.T) { _, ps, _ := runtimePlugins(t) if err := ps.LoadSource("authtoken", bridgeProbe); err != nil { t.Fatal(err) } if _, err := ps.Call("authtoken", "install_via_lua", map[string]interface{}{"tok": "OLD"}); err != nil { t.Fatalf("install: %v", err) } // 轮换 out, err := ps.Call("authtoken", "update_via_lua", map[string]interface{}{"tok": "NEW"}) if err != nil { t.Fatalf("update: %v", err) } m := out.(map[string]interface{}) if m["ok"] != true { t.Fatalf("Lua 侧 update 未成功: %#v", m) } // 新代码必须真的生效 —— 这是"轮换有意义"的定义 if rev := ps.vm.AdapterRevision("myauth"); rev < 2 { t.Errorf("适配器修订号应 >=2(已被替换一次),实得 %d", rev) } } // TestPluginRemoveAdapterFromLua:插件能删掉自己装的。 func TestPluginRemoveAdapterFromLua(t *testing.T) { _, ps, _ := runtimePlugins(t) if err := ps.LoadSource("authtoken", bridgeProbe); err != nil { t.Fatal(err) } if _, err := ps.Call("authtoken", "install_via_lua", map[string]interface{}{"tok": "T"}); err != nil { t.Fatal(err) } out, err := ps.Call("authtoken", "remove_via_lua", map[string]interface{}{}) if err != nil { t.Fatalf("remove: %v", err) } if out.(map[string]interface{})["ok"] != true { t.Fatalf("Lua 侧 remove 未成功: %#v", out) } if owner := ps.AdapterOwnerOf("myauth"); owner != "" { t.Errorf("删除后归属应清空, 实得 %q", owner) } } // TestLuaBridgeCannotShadowBuiltin:从 Lua 侧同样不能影子内置适配器 —— // 否则绑定就成了绕过 Go 侧校验的后门。 func TestLuaBridgeCannotShadowBuiltin(t *testing.T) { _, ps, _ := runtimePlugins(t) if err := ps.LoadSource("authtoken", bridgeProbe); err != nil { t.Fatal(err) } out, err := ps.Call("authtoken", "install_operator_owned", map[string]interface{}{}) if err != nil { t.Fatalf("调用本身不应报错(错误应作为返回值): %v", err) } m := out.(map[string]interface{}) if m["name"] != nil { t.Fatalf("影子内置 openai 必须被拒,却返回了 name=%#v", m["name"]) } if m["err"] == nil || m["err"] == "" { t.Fatal("影子内置应带回错误信息") } t.Logf("拒绝信息: %v", m["err"]) } // TestScheduledTaskRotatesTokenEndToEnd 是 R4 的闭环验收。 // // 它把三个特性串成一条链,任何一个环节坏掉都会让它失败: // 1. 内核 ticker 驱动定时任务(无外部触发) // 2. 任务与钩子跑在同一个 VM(所以任务的 install 与 state 写入对钩子可见) // 3. Lua 侧适配器桥接可用(__llmsproxy_adapters.install) // // 这正是用户描述的 token 轮换形态:插件自己按时间拿到新 token,重写适配器, // 后续请求自然使用新逻辑 —— 全程没有任何浏览器或人工操作。 func TestScheduledTaskRotatesTokenEndToEnd(t *testing.T) { shortTick(t) _, ps, _ := runtimePlugins(t) code := ` local plugin = { name = "sched_rot", version = "1.0" } plugin.state = { token = "OLD", rotates = 0 } plugin.schedule = { { fn = "rotate", every = 1 } } function plugin.rotate() plugin.state.rotates = plugin.state.rotates + 1 local tok = "T" .. tostring(plugin.state.rotates) local src = 'return { name = "rotauth", version = "1.0", endpoint = "/x", token = "' .. tok .. '" }' plugin.state.last_adapter, plugin.state.rotate_err = __llmsproxy_adapters.install("rotauth", src) plugin.state.token = tok end plugin.hooks = { routed = "observe" } function plugin.observe(payload) payload.token = plugin.state.token payload.rotates = plugin.state.rotates payload.adapter = plugin.state.last_adapter return payload end return plugin ` if err := ps.LoadSource("sched_rot", code); err != nil { t.Fatal(err) } deadline := time.Now().Add(4 * time.Second) for time.Now().Before(deadline) { out := ps.Fire(StageRouted, map[string]interface{}{}) if n, ok := out["rotates"].(float64); ok && n >= 1 { t.Logf("任务轮换了 %v 次; 钩子看到 token=%v adapter=%v", n, out["token"], out["adapter"]) if out["adapter"] != "rotauth" { t.Fatalf("任务里 install 未生效: adapter=%v", out["adapter"]) } return } time.Sleep(30 * time.Millisecond) } t.Fatal("定时任务未完成 token 轮换") } // TestShardVMHasNoAdapterBridge 锁定一条边界:分片 VM(ShardCall 用的私有 VM) // **不能**拿到适配器管理桥接。 // // 理由:分片是文档化的「只读、可丢弃」工作单元,而 install/update/remove 是 // **全局**副作用(改的是共享的适配器注册表,不是本 VM 的状态)。若把桥接给 // 分片,一个被声明为只读的按需调用就能悄悄改写所有请求共用的适配器 —— // 正是分片契约要防的那类不可见变更。 // // 断言方式是:分片里的插件代码检查 __llmsproxy_adapters 是否为 nil,返回结果。 func TestShardVMHasNoAdapterBridge(t *testing.T) { _, ps, _ := runtimePlugins(t) code := ` local plugin = { name = "sharded", version = "1.0" } plugin.exports = { "probe" } -- 只读探测:分片里这个全局必须是 nil function plugin.probe(args) return { has_bridge = __llmsproxy_adapters ~= nil } end return plugin ` if err := ps.LoadSource("sharded", code); err != nil { t.Fatal(err) } // 主 VM 上应当有桥接(对照) mainOut, err := ps.Call("sharded", "probe", map[string]interface{}{}) if err != nil { t.Fatalf("主 VM 调用失败: %v", err) } if m, ok := mainOut.(map[string]interface{}); !ok || m["has_bridge"] != true { t.Errorf("主 VM 上应有 __llmsproxy_adapters, 实得 %#v", mainOut) } // 分片 VM 上不应有 outs, err := ps.ShardCall("sharded", "probe", []map[string]interface{}{{}, {}}) if err != nil { t.Fatalf("ShardCall: %v", err) } for i, o := range outs { m, ok := o.(map[string]interface{}) if !ok { t.Fatalf("分片 %d 返回非 table: %#v", i, o) } if m["has_bridge"] == true { t.Errorf("分片 %d 不应拿到适配器桥接(分片是只读可丢弃的)", i) } } }