Files
ModelRouter/internal/lua/plugin_shard_config_test.go
JianFeeeee 8f2c5572e7 chore!: 移除厂商私有协议适配器与渠道业务插件(合规清理)
G-Star 合规要求下,把"模拟官方客户端、复刻私有协议、自动领取平台额度"这一类
能力整体移出仓库。这些实现依赖各平台未公开的端点、签名算法与官方 client id,
属于绕过客户端校验获取额度,风险高于普通协议适配。

移除(93 个文件):
- 适配器:opencodezen(伪装 UA 取免费池)、trae、buddy、kimicode
- 插件:channels.lua(12 个平台登录/领额度/签到)、quota.lua(厂商额度探针)
- 随之删除的 78 个渠道/额度专用测试与 3 份渠道文档

同时修掉的既有问题:
- **硬编码第三方 OAuth client_secret**(Google Cloud Code 的 GOCSPX-…,两处)
  随 channels.lua 一起消失。
- `deploy.sh` 的 `sync_plugins` 整段删除:它仍按"内置插件是发行物"的旧前提
  把插件复制/覆盖进 plugin_dir,与"插件属运维资产、网关不安装不覆盖"的契约
  冲突。现在部署脚本只同步适配器与二进制/配置。
- 适配器清单去掉已移除项;e2e 配置改用显式 `auto:` 链(`models.priority` 早已
  移除,该测试此前已失效)。

保留(公开协议):openai / anthropic / gemini / deepseek / github / groq /
mistral / ollama / qwen38 / sensenova = 10 个适配器;插件示例 billing / usage。

验证:go build -tags luajit ./... OK;internal/lua 21.4s、provider、core 51s、
gateway 122.8s、e2e 9.0s 全绿。
2026-10-27 09:25:58 +08:00

161 lines
5.0 KiB
Go

package lua
import (
"os"
"path/filepath"
"testing"
)
// A shard VM is booted from the same plugin SOURCE but has no state file and no
// plugin identity, so anything a plugin reads from its `prices` table
// (configuration, as opposed to accumulated state) is empty unless the pool puts
// it there.
//
// That asymmetry is invisible until a plugin's behaviour depends on its
// configuration. The OpenDesign adapter is the first: it reads
// prices.control_key, and without seeding it concluded "not configured" for every
// host and reported a skip — while the identical call through the shared VM
// worked.
//
// It mattered because the two paths do different jobs: the page probes through
// the FANOUT (shard) path and records results through the shared one. A feature
// broken on the shard path is dead in production and green in every test that
// calls the shared VM.
const shardConfigProbe = `
local P = {}
P.name = "cfgprobe"
P.version = "1.0.0"
P.exports = { "read_config" }
P.read_config = function(payload)
local c = P.prices
return { key = (type(c) == "table") and c.control_key or nil }, nil
end
return P
`
func shardConfigPlugins(t *testing.T) *Plugins {
t.Helper()
dir := t.TempDir()
pluginDir := filepath.Join(dir, "plugins")
if err := os.MkdirAll(pluginDir, 0o755); err != nil {
t.Fatal(err)
}
vm := NewVM(filepath.Join(dir, "adapters"))
if err := vm.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(vm.Stop)
if err := os.WriteFile(filepath.Join(pluginDir, "cfgprobe.lua"),
[]byte(shardConfigProbe), 0o644); err != nil {
t.Fatal(err)
}
ps := NewPlugins(vm, pluginDir)
t.Cleanup(ps.Close)
if err := ps.LoadDir(); err != nil {
t.Fatal(err)
}
return ps
}
func configKeyOf(t *testing.T, out interface{}) string {
t.Helper()
m, ok := out.(map[string]interface{})
if !ok {
t.Fatalf("result is %T (%v), want a table", out, out)
}
s, _ := m["key"].(string)
return s
}
func TestShardSeesPluginConfiguration(t *testing.T) {
ps := shardConfigPlugins(t)
if err := ps.SetState("cfgprobe", map[string]interface{}{
"prices": map[string]interface{}{"control_key": "secret-value"},
}); err != nil {
t.Fatal(err)
}
// The shared path is the control: every pre-existing test exercises it, and
// it passes whether or not the pool seeds anything.
out, err := ps.Call("cfgprobe", "read_config", map[string]interface{}{})
if err != nil {
t.Fatalf("shared call: %v", err)
}
if got := configKeyOf(t, out); got != "secret-value" {
t.Fatalf("shared VM cannot read prices.control_key (%q) — the test itself "+
"is wrong, not the shard path", got)
}
// The shard path is the one the page uses.
outs, err := ps.ShardCall("cfgprobe", "read_config",
[]map[string]interface{}{{}, {}, {}})
if err != nil {
t.Fatalf("ShardCall: %v", err)
}
for i, o := range outs {
if got := configKeyOf(t, o); got != "secret-value" {
t.Fatalf("shard %d read prices.control_key as %q, want the configured "+
"value — a plugin that branches on its configuration behaves "+
"differently on the path the page actually uses", i, got)
}
}
}
// A pooled VM is reused, so the seeding has to happen on every checkout rather
// than only on boot, or a configuration change would be invisible to the pool
// until the process restarted.
func TestShardConfigFollowsAConfigurationChange(t *testing.T) {
ps := shardConfigPlugins(t)
if err := ps.SetState("cfgprobe", map[string]interface{}{
"prices": map[string]interface{}{"control_key": "first"},
}); err != nil {
t.Fatal(err)
}
if _, err := ps.ShardCall("cfgprobe", "read_config", []map[string]interface{}{{}}); err != nil {
t.Fatal(err)
}
if err := ps.SetState("cfgprobe", map[string]interface{}{
"prices": map[string]interface{}{"control_key": "second"},
}); err != nil {
t.Fatal(err)
}
outs, err := ps.ShardCall("cfgprobe", "read_config",
[]map[string]interface{}{{}, {}, {}, {}})
if err != nil {
t.Fatal(err)
}
for i, o := range outs {
if got := configKeyOf(t, o); got != "second" {
t.Fatalf("shard %d still reads %q after the configuration changed to "+
"%q — pooled VMs keep a stale configuration", i, got, "second")
}
}
}
// Reading must not disturb the shared value. That is what makes it acceptable to
// seed configuration into a VM whose state is otherwise throwaway, and it is why
// the seeding is a one-way copy rather than a reference to the shared table.
func TestShardConfigSeedingDoesNotAliasSharedState(t *testing.T) {
ps := shardConfigPlugins(t)
if err := ps.SetState("cfgprobe", map[string]interface{}{
"prices": map[string]interface{}{"control_key": "keep-me"},
}); err != nil {
t.Fatal(err)
}
outs, err := ps.ShardCall("cfgprobe", "read_config", []map[string]interface{}{{}})
if err != nil {
t.Fatal(err)
}
if got := configKeyOf(t, outs[0]); got != "keep-me" {
t.Fatalf("shard read %q", got)
}
out, err := ps.Call("cfgprobe", "read_config", map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
if got := configKeyOf(t, out); got != "keep-me" {
t.Fatalf("the shared configuration changed to %q after a shard read", got)
}
}