package core import ( "os/exec" "regexp" "strings" "testing" ) // ★ channels.lua 必须留出顶层 local 余量 —— 否则**下一次顶层改动就编译失败**。 // // 实测事实(2026-10-16):该文件的主 chunk 曾**顶到 Lua 上限 200**, // 往顶部加**哪怕 1 个**顶层 local 就报 // // main function has more than 200 local variables // // 而这炸的不是某个功能 —— 是**整个渠道插件**(插件加载失败 ⇒ 全渠道不可用)。 // // ⚠️ 判据为什么用 luajit 而不是 luac: // // 内核链接的是 libluajit-5.1.so.2;而 `luac`(Lua 5.4)对同一文件给出 // **不同的**结论。用错工具会得到假绿。 // // ⚠️ 判据为什么是「注入 N 个 local 仍能编译」而不是「数一遍 local」: // // 主 chunk 的 local 数受**作用域嵌套**影响,静态计数不可靠(同一个文件 // 用不同脚本数出 228/232/248 三个数)。而"能不能再加"是**决定性**的, // 也正是我们真正需要的性质。 func TestChannelsLuaHasLocalHeadroom(t *testing.T) { src, err := readBundledChannels(t) if err != nil { t.Fatal(err) } if _, err := exec.LookPath("luajit"); err != nil { t.Skip("luajit 不在 PATH(判据需要与内核相同的 Lua 实现)") } const headroom = 10 for _, n := range []int{1, headroom} { probe := strings.Repeat("local __headroom = 1\n", n) + src cmd := exec.Command("luajit", "-e", "local f,e=loadfile(arg[1]); if not f then io.stderr:write(tostring(e)) os.exit(1) end", "/dev/stdin") cmd.Stdin = strings.NewReader(probe) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("★ 顶层 local 余量不足:加 %d 个 local 就无法编译 ⇒ "+ "下一次顶层改动会**整插件加载失败**(全渠道不可用)\n %s\n"+ "修法:把纯常量收进 plugin.K 表(挂表字段不占 local 槽位)", n, strings.TrimSpace(string(out))) } } t.Logf("✓ channels.lua 仍有 ≥%d 个顶层 local 余量", headroom) } // 配对判据:常量必须确实在 plugin.K 里(防止有人把余量"省"出来的同时又 // 把常量改回裸 local —— 那会让下一个改动的余量再次归零)。 func TestChannelsConstantsLiveInPluginK(t *testing.T) { src, err := readBundledChannels(t) if err != nil { t.Fatal(err) } s := string(src) if !strings.Contains(s, "plugin.K = {") { t.Fatal("必须有 plugin.K 常量表(它是 local 余量的来源)") } // 抽样:这些曾占用 local 槽位的纯常量必须已挂表 for _, n := range []string{"STORE_NAME", "SESSION_TTL", "B64_ALPHABET", "MODELS_DEV_URL"} { if !regexp.MustCompile(`(?m)^\s+` + n + ` = `).MatchString(s) { t.Errorf("%s 应定义在 plugin.K 表内", n) } if regexp.MustCompile(`(?m)^local ` + n + ` = `).MatchString(s) { t.Errorf("%s 又变回裸 local 了(会吃掉 local 余量)", n) } } t.Log("✓ 纯常量已集中在 plugin.K") }