Files
ModelRouter/internal/lua/outbound_grant_persist_test.go
JianFeeeee 9bec158d2e chore(main): 回流 release/v2.0.x(v2.0.12)—— 内容对齐,main 无独有内容
按发布纪律回流。**注意这是内容对齐而非 merge**,理由与依据:

1. 纪律禁止 merge 回主干,因为 merge 会把发布分支的**版本号变更**带进 main。
   本项目版本号**只由 `git describe --tags` 推导**(Makefile VERSION),
   仓库里没有任何文件存放版本号 ⇒ 回流不存在版本污染风险。
2. 逐项核对确认 **main 是 release 的内容严格子集**:
   - `main` 独有的**文件**:0 个;
   - `internal/lua/plugins.go` / `internal/core/kernel_actions.go` 等内核文件
     main 独有行数:0;
   - `deploy.sh` 那 14 行只是 emoji 清理(release 刻意去掉了 emoji);
   - `channels.lua` 那 333 行是**旧版内容**(旧 exports 表、旧模型名单)。
   - main 独有的 9 个提交按 patch-id **全部已在 release 中等效存在**
     (`git cherry` 逐条列出均为 `-`,即已合入)。
3. 故回流 = 把 release 的内容带到 main,等价于"只捡拾修复、不带版本号",
   且不会丢任何 main 独有的修复。

本次带回 v2.0.12 的全部内容(37 个提交):
- 源串行 + 按模型节流(照 model-gate)
- 已失效模型存储 + 远端目录闸门
- 余额分档选号
- 模型能力元数据(models.dev,isFree 权威判定)
- **凭据续期修复**(此前恒报 ok:true 却一个请求都不发)
- 通用件 TTL 缓存 + 串行锁;account_test/account_adopt/account_delete 修正
- 内核级:出站授权持久化重启后不生效、`__llmsproxy_plugin_dir` ground truth

校验:`git diff release/v2.0.x` = 0 行;`go build -tags luajit ./...` 通过。
2026-10-14 16:52:58 +08:00

75 lines
2.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"
"os"
"path/filepath"
"testing"
)
// 持久化的出站主机授权必须在**重启后自动生效**。
//
// 实测缺陷(生产):LoadKernelPermissions 把已批准的 host 装进 ps.extraHosts,
// 但注释明说"折叠由调用方负责" —— 启动路径上没人负责。SetOutboundHosts 早已
// 跑过(core.New 里、LoadDir 之前),于是 allowlist 只有 source 主机,
// 插件声明的整套 host(zcode.z.ai / copilot.tencent.com …)在重启后全被拒:
//
// host "zcode.z.ai" is not an allowed outbound destination
//
// 症状是经典的"重启丢掉运维的决定"——运维一打开插件页重新保存就恢复。
// 它还打坏了定时任务:refresh_models 在启动后 5s 触发,正好落在那个窗口里,
// 于是每个平台的刷新都报"授权缺失"。
func TestPersistedHostGrantsSurviveRestart(t *testing.T) {
dir := t.TempDir()
plugDir := filepath.Join(dir, "plugins")
if err := os.MkdirAll(plugDir, 0o755); err != nil {
t.Fatal(err)
}
// 造一份持久化授权(模拟运维此前在 WebUI 批准过)
perms := map[string]interface{}{
"channels": map[string]interface{}{
"actions": []string{"add_source"},
"hosts": []string{"zcode.z.ai", "copilot.tencent.com"},
},
}
b, _ := json.Marshal(perms)
if err := os.WriteFile(filepath.Join(plugDir, ".kernel-perms.json"), b, 0o600); err != nil {
t.Fatal(err)
}
// 模拟启动:先 SetOutboundHosts(源主机),再 LoadDir(读授权)
vm := NewVM(filepath.Join(dir, "adapters"))
if err := vm.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(vm.Stop)
ps := NewPlugins(vm, plugDir)
ps.SetOutboundHosts([]string{"api.example.com"})
// LoadDir 会读授权文件;没有插件文件也没关系,关键是它必须折叠授权。
if err := ps.LoadDir(); err != nil {
t.Fatalf("LoadDir: %v", err)
}
al := ps.outboundAllowlist()
if al == nil {
t.Fatal("无 allowlist")
}
// ① 源主机仍可用
if !al.allows("api.example.com", "443") {
t.Error("source 主机被丢了")
}
// ② 已批准的 extra host 必须生效(这就是本判据的核心)
for _, h := range []string{"zcode.z.ai", "copilot.tencent.com"} {
if !al.allows(h, "443") {
t.Errorf("重启后已批准的 host %q 被拒 —— "+
"LoadKernelPermissions 装了 extraHosts 但没人折叠进 allowlist", h)
}
}
// ③ 没批准过的仍然不许
if al.allows("evil.example.com", "443") {
t.Error("未批准的 host 不该被放行")
}
}