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. //