From d6d41209d9c9dd1d259b6dcceff0bf010587d606 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 12 Oct 2026 16:37:23 +0800 Subject: [PATCH] =?UTF-8?q?test(adapters):=20=E9=80=90=E9=80=82=E9=85=8D?= =?UTF-8?q?=E5=99=A8=E4=B8=A4=E5=90=91=E8=83=BD=E5=8A=9B=E6=96=AD=E8=A8=80?= =?UTF-8?q?=20+=20AdapterIngress=20=E8=AE=BF=E9=97=AE=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户要求: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(配额/渠道),说明请求正确到达了各适配器的上游。 --- internal/lua/adapter_two_directions_test.go | 94 +++++++++++++++++++++ internal/lua/vm.go | 14 +++ 2 files changed, 108 insertions(+) create mode 100644 internal/lua/adapter_two_directions_test.go diff --git a/internal/lua/adapter_two_directions_test.go b/internal/lua/adapter_two_directions_test.go new file mode 100644 index 0000000..bcd6609 --- /dev/null +++ b/internal/lua/adapter_two_directions_test.go @@ -0,0 +1,94 @@ +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) + } + }) + } +} diff --git a/internal/lua/vm.go b/internal/lua/vm.go index 6c53462..1b43ec7 100644 --- a/internal/lua/vm.go +++ b/internal/lua/vm.go @@ -1621,6 +1621,20 @@ func (v *VM) AdapterVersion(name string) string { return p.static.version } +// AdapterIngress returns the client-facing endpoint path an adapter CLAIMS +// (its static.ingress), or "" when it claims none. Exported so operator-facing +// surfaces (and tests) can see which adapter, if any, has taken over parsing of +// an inbound endpoint — the same declaration AdapterForIngress consults. +func (v *VM) AdapterIngress(name string) string { + p := v.pool(name) + if p == nil { + return "" + } + p.mu.Lock() + defer p.mu.Unlock() + return p.static.ingress +} + // EffectiveAdapterName resolves the name an adapter is actually registered // under, given the key it was loaded with. //