Files
ModelRouter/internal/lua/adapter_two_directions_test.go
JianFeeeee d6d41209d9 test(adapters): 逐适配器两向能力断言 + AdapterIngress 访问器
用户要求:16 个适配器逐个验证两个方向(出向序列化 / 入向解析)重构后都没坏。

- 新增 VM.AdapterIngress(name):暴露适配器认领的入站路径,供运维面与测试查看。
- 新增 TestEveryBundledAdapterTwoDirections:对每个内置适配器断言
  1) 上游向 transform_request 存在且可调用;
  2) 出向 — 声明了 serialize_response 的必须可调用且产出合法 JSON/SSE,
     未声明的确认走内核 OpenAI 兜底(两种都合法);
  3) 入向 — 声明了 adapter.ingress 的必须同时实现 deserialize_request
     (否则内核会让出给空解析器),实现了钩子却没声明 ingress 的也报错
     (内核永远不会调用它)。
  12 个内置适配器全部通过。

隔离实例实测(19 源 + 16 适配器):
- 启动无解密失败/崩溃,16 适配器全加载。
- 入向让出决定性实证:装入认领 /v1/messages 的 zzclaimer 并移除 anthropic 后,
  畸形 body 被适配器解析成 model=ZZ-CLAIMER-WAS-HERE,内核据此路由得
  404 model_not_found —— 证明内核确实让出(活实例复现,非仅单测)。
- 真实请求两向贯通:deepseek-v4-flash / sensenova-6.8-flash-lite 返回 200 真实响应;
  其余源返回上游 502(配额/渠道),说明请求正确到达了各适配器的上游。
2026-10-12 16:37:23 +08:00

95 lines
4.0 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"
"path/filepath"
"strings"
"testing"
)
// TestEveryBundledAdapterTwoDirections 逐个验证每个内置适配器在**两个方向**上
// 的声明与能力一致,且都能在真实 VM 上被调用而不报错。这是"重构后两个方向
// 都没坏"的批量断言(用户要求:16 个适配器逐个验证两个方向)。
//
// 两个方向:
//
// 出向(egress):serialize_* 钩子 —— 统一格式 → 客户端协议。
// 一个适配器要么声明了 serialize_*(接管),要么依赖内核 OpenAI 兜底。
// 两种都合法;不合法的是"声明了钩子但调用即报错"。
//
// 入向(ingress):adapter.ingress + deserialize_request —— 客户端协议 → 统一格式。
// 声明 ingress 的适配器必须同时实现 deserialize_request,否则内核会让出给
// 一个空解析器(核心里 parseViaIngress 已挡住,此处再在适配器数据面钉一次)。
//
// 上游向(upstream):transform_request —— 统一格式 → 上游协议。
// 这是所有适配器的本职,必须存在且可调用。
func TestEveryBundledAdapterTwoDirections(t *testing.T) {
vm := NewVM(filepath.Join(t.TempDir(), "adapters"))
if err := vm.Start(); err != nil {
t.Fatal(err)
}
defer vm.Stop()
adapters := vm.ListAdapters()
if len(adapters) == 0 {
t.Fatal("no adapters loaded")
}
// 一个最小、协议中立的统一请求,供上游向 transform_request 使用。
unifiedReq := `{"model":"m","messages":[{"role":"user","content":"hi"}],"max_tokens":16,"stream":false}`
for _, a := range adapters {
a := a
t.Run(a.Name, func(t *testing.T) {
// ---- 上游向:transform_request 必须存在且可调用 ----
if !vm.HasFn(a.Name, "transform_request") {
t.Errorf("%s: 缺 transform_request(上游向本职)", a.Name)
} else if _, err := vm.Transform(a.Name, "transform_request", unifiedReq); err != nil {
t.Errorf("%s: transform_request 调用失败: %v", a.Name, err)
}
// ---- 出向:serialize_* 声明与调用一致性 ----
declaredEgress := false
for _, fn := range []string{"serialize_response", "serialize_chunk", "serialize_open_preamble", "serialize_close", "serialize_done_marker"} {
if vm.HasFn(a.Name, fn) {
declaredEgress = true
}
}
if vm.HasFn(a.Name, "serialize_response") {
// 统一结果形状(见 types.UnifiedResult)
res := `{"model":"m","id":"x","content":"hello","finish_reason":"stop","token_usage":{"prompt":1,"completion":1,"total":2}}`
out, err := vm.Transform(a.Name, "serialize_response", res)
if err != nil {
t.Errorf("%s: serialize_response 调用失败: %v", a.Name, err)
} else if strings.TrimSpace(out) == "" {
t.Errorf("%s: serialize_response 返回空", a.Name)
} else if !json.Valid([]byte(out)) && !strings.HasPrefix(strings.TrimSpace(out), "event:") {
t.Errorf("%s: serialize_response 既非 JSON 也非 SSE: %q", a.Name, out)
}
}
if declaredEgress {
t.Logf("%s: 出向由适配器接管", a.Name)
} else {
t.Logf("%s: 出向走内核 OpenAI 兜底(未声明 serialize_*)", a.Name)
}
// ---- 入向:声明 ingress 必须配 deserialize_request ----
ingress := vm.AdapterIngress(a.Name)
hasDeser := vm.HasFn(a.Name, "deserialize_request")
if ingress != "" {
if !hasDeser {
t.Errorf("%s: 声明 ingress=%q 但没有 deserialize_request —— 内核会让出给空解析器", a.Name, ingress)
} else {
// 用该适配器自己服务的那个端点的典型 body 调一次,确认不报错。
if _, err := vm.Transform(a.Name, "deserialize_request", `{"model":"m","max_tokens":8,"messages":[{"role":"user","content":"hi"}]}`); err != nil {
t.Errorf("%s: deserialize_request 调用失败: %v", a.Name, err)
}
t.Logf("%s: 入向认领 %s", a.Name, ingress)
}
} else if hasDeser {
t.Errorf("%s: 实现了 deserialize_request 却没声明 ingress —— 内核永远不会调用它", a.Name)
}
})
}
}