package core import ( "encoding/json" "testing" "llmsproxy/internal/lua" ) // TestChannelsPluginEndToEnd 是「同类插件」的完整验收: // 内置的 channels 插件在**真实内核**上跑通「登录 → 存凭据 → 接入为上游源」。 // // 这条链路横跨本次所有新增能力,所以它是最有信息量的一个测试: // // __llmsproxy_kernel add_source 把平台注册为上游源 // 内核权限 默认拒绝,需运维授权 // 额外出站主机授权 登录域不在源列表里,默认被出站白名单拒绝(plugin.hosts) // 登录会话 插件自持(plugin.state.sessions),内核无登录概念 func TestChannelsPluginEndToEnd(t *testing.T) { c := kernelTestCore(t) ps := c.Plugins() src, err := lua.ReadBundledPlugin("channels") if err != nil { t.Fatalf("读取内置 channels 插件: %v", err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatalf("加载 channels: %v", err) } // 测试夹具经运行时注入:生产目录只含真实厂商,夹具不再内联进源码 // (否则会出现在生产 UI 的平台列表里)。 injectTestPlatforms(t, ps, examplePlatformFixture) // 1. 平台清单可用(未授权任何东西也应能列出) out, err := ps.InvokeHandler("channels", "list_platforms", &lua.HandlerRequest{ Method: "GET", Role: "admin", }) if err != nil { t.Fatalf("list_platforms: %v", err) } body, _ := out.JSON.(map[string]interface{}) plats, _ := body["platforms"].([]interface{}) if len(plats) < 3 { t.Fatalf("应至少 3 个平台, 实得 %d", len(plats)) } // 2. 发起登录 → 拿到会话与 URL out, err = ps.InvokeHandler("channels", "start_login", &lua.HandlerRequest{ Method: "POST", Body: `{"platform":"example"}`, Role: "admin", }) if err != nil { t.Fatalf("start_login: %v", err) } m := out.JSON.(map[string]interface{}) if m["error"] != nil { t.Fatalf("发起登录失败: %v", m["error"]) } if m["url"] == nil || m["url"] == "" { t.Fatal("应返回用户要打开的登录 URL") } sess := m["session"].(map[string]interface{}) id, _ := sess["id"].(string) if id == "" { t.Fatal("应返回会话 id") } // 3. 轮询到 pending out, _ = ps.InvokeHandler("channels", "poll_login", &lua.HandlerRequest{ Method: "GET", Query: map[string]string{"id": id}, Role: "admin", }) if st := out.JSON.(map[string]interface{})["state"]; st != "pending" { t.Fatalf("应为 pending, 实得 %v", st) } // 4. 未授权内核动作时,接入源必须失败且**如实报告**(不是假装成功) out, err = ps.InvokeHandler("channels", "complete_login", &lua.HandlerRequest{ Method: "POST", Body: `{"platform":"example","credential":"tok-1","account":"alice"}`, Role: "admin", }) if err != nil { t.Fatalf("complete_login: %v", err) } done := out.JSON.(map[string]interface{}) if done["source_error"] == nil { t.Fatal("未授权 add_source 时应报告 source_error") } t.Logf("未授权时的如实报告: %v", done["source_error"]) // 凭据必须已在保险库(登录本身成功了) tok, ok, _ := ps.GetSecret("channels", "channels:example") if !ok || tok != "tok-1" { t.Fatalf("凭据应已存保险库: ok=%v tok=%q", ok, tok) } // 且不得出现在 state 里 if stateHasSecret(ps, "channels", "tok-1") { t.Fatal("★ 凭据泄漏进了 plugin.state") } // 5. 运维授权后,重新接入应当真的把源注册进内核 if err := ps.SetPluginKernelPermission("channels", []string{"add_source", "list_sources", "status"}, false); err != nil { t.Fatal(err) } out, err = ps.InvokeHandler("channels", "complete_login", &lua.HandlerRequest{ Method: "POST", Body: `{"platform":"example","credential":"tok-1","account":"alice"}`, Role: "admin", }) if err != nil { t.Fatalf("complete_login(授权后): %v", err) } done = out.JSON.(map[string]interface{}) if done["source_error"] != nil { t.Fatalf("授权后仍失败: %v", done["source_error"]) } if done["source"] != "channels-example" { t.Fatalf("源名不对: %v", done["source"]) } // 内核侧必须真的有这个源 found := false for _, s := range c.Sources() { if s.Name == "channels-example" { found = true if s.BaseURL != "https://api.example.com/v1" { t.Errorf("base_url 未落库: %q", s.BaseURL) } } } if !found { t.Fatal("插件接入的源没有出现在内核源列表里") } // 平台声明的模型应可路由 models := c.Registry().ModelList() hasModel := false for _, id := range models { if id == "example-chat" { hasModel = true } } if !hasModel { t.Errorf("平台模型未进入可路由列表: %v", models) } t.Logf("已接入源 channels-example,模型可见: %v", models) } // TestChannelsPluginDeclaresItsNeeds 断言插件如实声明了它需要什么。 // // 声明是运维审批的依据:插件不声明,设置页就无法显示"申请中", // 运维只能等某个功能静默失效后才发现。 func TestChannelsPluginDeclaresItsNeeds(t *testing.T) { c := kernelTestCore(t) ps := c.Plugins() src, err := lua.ReadBundledPlugin("channels") if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatal(err) } // 内核动作声明 views := ps.KernelPermissions() var declared []string for _, v := range views { if v.Plugin == "channels" { declared = v.Declared } } if len(declared) == 0 { t.Fatal("channels 应声明它需要的内核动作") } for _, want := range []string{"add_source", "status"} { found := false for _, d := range declared { if d == want { found = true } } if !found { t.Errorf("应声明 %q, 实得 %v", want, declared) } } // 额外出站主机声明(非源 host,需单独批准) hosts := ps.PluginExtraHosts("channels") if len(hosts) == 0 { t.Fatal("channels 应声明额外出站主机") } t.Logf("声明内核动作=%v 额外主机=%v", declared, hosts) } // TestChannelsPluginHandlersRequireStatus 是一个契约回归:插件的每个 handler // 都必须返回 {status,json/body},否则内核会**静默丢弃**它(实测踩过: // `return {url=…}` 得到 200 空响应,没有任何错误信息)。 func TestChannelsPluginHandlersRequireStatus(t *testing.T) { c := kernelTestCore(t) ps := c.Plugins() src, err := lua.ReadBundledPlugin("channels") if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatal(err) } _ = ps.SetPluginKernelPermission("channels", []string{"add_source", "status", "list_sources"}, false) // 每个 handler 至少要在正常输入下给出非空响应 cases := []struct { fn string req *lua.HandlerRequest what string }{ {"list_platforms", &lua.HandlerRequest{Method: "GET"}, "平台列表"}, {"status_page", &lua.HandlerRequest{Method: "GET"}, "状态页"}, {"poll_login", &lua.HandlerRequest{Method: "GET", Query: map[string]string{"id": "nope"}}, "轮询(未知会话)"}, {"start_login", &lua.HandlerRequest{Method: "POST", Body: `{"platform":"example"}`}, "发起登录"}, {"start_login", &lua.HandlerRequest{Method: "POST", Body: `{"platform":"nope"}`}, "发起登录(未知平台)"}, {"complete_login", &lua.HandlerRequest{Method: "POST", Body: `{}`}, "完成登录(缺字段)"}, {"claim_daily", &lua.HandlerRequest{Method: "POST", Body: `{"platform":"example"}`}, "领额度(未登录)"}, } for _, tc := range cases { out, err := ps.InvokeHandler("channels", tc.fn, tc.req) if err != nil { t.Errorf("%s (%s) 返回错误: %v", tc.fn, tc.what, err) continue } if out.JSON == nil && out.Body == "" { t.Errorf("%s (%s) 返回空响应 —— 内核会静默丢弃,调用方看不到任何原因", tc.fn, tc.what) } } } // jsonMarshalForTest 序列化一个接口值,用于检查凭据是否出现在 state 里。 func jsonMarshalForTest(v interface{}) ([]byte, error) { return json.Marshal(v) } // stateHasSecret 检查凭据是否泄漏进 plugin.state。 func stateHasSecret(ps *lua.Plugins, plugin, secret string) bool { st := ps.State(plugin) if st == nil { return false } b, err := jsonMarshalForTest(st) if err != nil { return false } return contains(string(b), secret) } // examplePlatformFixture 是端到端测试用的占位平台(对应此前内联在生产源码里 // 的 "example" 平台)。它经 inject_platforms 注入,且**只在测试进程内存在** —— // 生产插件的 PLATFORMS 只含真实厂商。 const examplePlatformFixture = `[ { "id": "example", "name": "Example 平台(测试夹具)", "flow": "link-poll", "login_url": "https://auth.example.com/device", "base_url": "https://api.example.com/v1", "adapter": "openai", "models": ["example-chat", "example-reason"], "checkin": {"url": "https://api.example.com/v1/api/daily/claim", "method": "POST", "body": "{}"} } ]` // injectTestPlatforms 把测试夹具注入插件(运行时,不落盘)。 func injectTestPlatforms(t *testing.T, ps *lua.Plugins, fixtureJSON string) { t.Helper() out, err := ps.InvokeHandler("channels", "inject_platforms", &lua.HandlerRequest{ Method: "POST", Body: `{"platforms":` + fixtureJSON + `}`, Role: "admin", }) if err != nil { t.Fatalf("inject_platforms: %v", err) } m, _ := out.JSON.(map[string]interface{}) if m["ok"] != true { t.Fatalf("注入测试平台失败: %v", m) } } // TestInjectPlatformsRejectsBadInput 锁定注入机制的三条边界: // 1. 不得覆盖内建平台(注入不能劫持真实厂商) // 2. 重复 id 拒绝 // 3. 空数组 = 清空注入(夹具不跨测试残留) // // 这个机制存在的理由:夹具内联进生产源码会让它们出现在生产 UI 的平台列表里 // (真实踩过 —— 16 个平台里有 4 个是夹具)。 func TestInjectPlatformsRejectsBadInput(t *testing.T) { c := kernelTestCore(t) ps := c.Plugins() src, err := lua.ReadBundledPlugin("channels") if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatal(err) } call := func(body string) map[string]interface{} { out, err := ps.InvokeHandler("channels", "inject_platforms", &lua.HandlerRequest{ Method: "POST", Body: body, Role: "admin", }) if err != nil { t.Fatalf("inject_platforms(%s): %v", body, err) } return out.JSON.(map[string]interface{}) } platforms := func() []interface{} { out, _ := ps.InvokeHandler("channels", "list_platforms", &lua.HandlerRequest{Method: "GET", Role: "admin"}) return out.JSON.(map[string]interface{})["platforms"].([]interface{}) } base := len(platforms()) if base == 0 { t.Fatal("生产目录应有真实厂商平台") } // 注入一个合法夹具 → 列表 +1 if m := call(`{"platforms":[{"id":"fixture-a","name":"A","flow":"paste","base_url":"https://a.example/v1"}]}`); m["ok"] != true { t.Fatalf("合法注入应成功: %v", m) } if got := len(platforms()); got != base+1 { t.Fatalf("注入后平台数 = %d, 期望 %d", got, base+1) } // 1. 覆盖内建平台 → 拒绝 if m := call(`{"platforms":[{"id":"buddy","name":"劫持"}]}`); m["error"] == nil { t.Fatal("覆盖内建平台必须被拒绝") } // 2. 重复 id → 拒绝 if m := call(`{"platforms":[{"id":"fixture-b"},{"id":"fixture-b"}]}`); m["error"] == nil { t.Fatal("重复 id 必须被拒绝") } // 无 id → 拒绝 if m := call(`{"platforms":[{"name":"no-id"}]}`); m["error"] == nil { t.Fatal("缺 id 必须被拒绝") } // 3. 空数组 = 清空注入,回到基线 if m := call(`{"platforms":[]}`); m["ok"] != true { t.Fatalf("清空应成功: %v", m) } if got := len(platforms()); got != base { t.Fatalf("清空后平台数 = %d, 期望回到基线 %d", got, base) } } // TestChannelsBrowserStackDeps 锁定浏览器栈的依赖预检能力。 // // 这个能力此前**不存在**:代码里写着「运行 deploy 脚本一键安装」,但安装机制 // 根本没有(deploy.sh 里零浏览器栈内容),于是 VNC 兜底在缺依赖的机器上只会 // 报一句含糊的"进程启动失败"。现在如实报告缺什么、装哪个包。 func TestChannelsBrowserStackDeps(t *testing.T) { c := kernelTestCore(t) ps := c.Plugins() src, err := lua.ReadBundledPlugin("channels") if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatal(err) } out, err := ps.InvokeHandler("channels", "deps_status", &lua.HandlerRequest{ Method: "GET", Role: "admin", }) if err != nil { t.Fatalf("deps_status: %v", err) } m, ok := out.JSON.(map[string]interface{}) if !ok { t.Fatalf("deps_status 应返回对象: %#v", out.JSON) } if _, has := m["ok"]; !has { t.Fatal("deps_status 必须报告 ok") } if _, has := m["missing"]; !has { t.Fatal("deps_status 必须报告 missing 清单") } if m["ok"] == false { hint, _ := m["install_hint"].(string) if hint == "" { t.Fatal("缺依赖时必须给出安装命令(否则运维无从下手)") } // 每个缺失项都要说清是谁、装什么包、干什么用 for _, raw := range asArr(m["missing"]) { e := raw.(map[string]interface{}) for _, k := range []string{"bin", "pkg", "why"} { if s, _ := e[k].(string); s == "" { t.Fatalf("缺少字段 %s: %v", k, e) } } } t.Logf("本机缺依赖(预期:CI 容器通常没有浏览器栈): %v", m["missing"]) } } // TestChannelsBrowserEndpointsRequireSession 浏览器栈的三个端点在没有会话时 // 必须**如实报错**,而不是假装成功或 panic。 func TestChannelsBrowserEndpointsRequireSession(t *testing.T) { c := kernelTestCore(t) ps := c.Plugins() src, err := lua.ReadBundledPlugin("channels") if err != nil { t.Fatal(err) } if err := ps.LoadSource("channels", src); err != nil { t.Fatal(err) } // 无会话时 collect 必须报错 out, err := ps.InvokeHandler("channels", "collect_credentials", &lua.HandlerRequest{ Method: "POST", Body: `{"platform":"buddy"}`, Role: "admin", }) if err != nil { t.Fatalf("collect_credentials: %v", err) } if out.JSON.(map[string]interface{})["error"] == nil { t.Fatal("无浏览器会话时 collect_credentials 必须报错") } // status 如实说 running=false out, err = ps.InvokeHandler("channels", "browser_status", &lua.HandlerRequest{ Method: "GET", Role: "admin", }) if err != nil { t.Fatalf("browser_status: %v", err) } if out.JSON.(map[string]interface{})["running"] != false { t.Fatal("无会话时 running 应为 false") } // stop 幂等:无会话时也返回 ok out, err = ps.InvokeHandler("channels", "stop_browser", &lua.HandlerRequest{ Method: "POST", Role: "admin", }) if err != nil { t.Fatalf("stop_browser: %v", err) } if out.JSON.(map[string]interface{})["ok"] != true { t.Fatal("stop_browser 应幂等成功") } }