1
0

feat: 系统性改进后端测试体系

- 新增 6 个测试场景 (config load pipe, handler errors, service aggregation, engine degradation, openai decoder edges, negative tests)
- 更新测试工具规格 (mockgen, in-memory SQLite)
- 覆盖率目标从 >80% 提升至 >85%
- 新增 test-unit 和 test-integration Makefile 命令
- 新增死代码清理和 mockgen 需求
- 归档变更至 openspec/changes/archive/2026-04-22-improve-backend-testing/
This commit is contained in:
2026-04-22 13:18:51 +08:00
parent 59179094ed
commit 4e86adffb7
32 changed files with 3374 additions and 729 deletions

View File

@@ -321,3 +321,58 @@ func (m *testMiddleware) InterceptStreamEvent(event *canonical.CanonicalStreamEv
}
var _ = json.Marshal
func TestConvertEmbeddingBody_DecodeError(t *testing.T) {
registry := NewMemoryRegistry()
engine := NewConversionEngine(registry, nil)
clientAdapter := newMockAdapter("client", false)
clientAdapter.decodeEmbeddingReqFn = func(raw []byte) (*canonical.CanonicalEmbeddingRequest, error) {
return nil, errors.New("decode embedding failed")
}
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeEmbeddings: true}
providerAdapter := newMockAdapter("provider", false)
providerAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeEmbeddings: true}
_ = engine.RegisterAdapter(clientAdapter)
_ = engine.RegisterAdapter(providerAdapter)
body := []byte(`{"model":"text-embedding","input":"hello"}`)
result, err := engine.convertEmbeddingBody(clientAdapter, providerAdapter, NewTargetProvider("", "", ""), body)
require.NoError(t, err)
assert.Equal(t, body, result)
}
func TestConvertRerankBody_DecodeError(t *testing.T) {
registry := NewMemoryRegistry()
engine := NewConversionEngine(registry, nil)
clientAdapter := newMockAdapter("client", false)
clientAdapter.decodeRerankReqFn = func(raw []byte) (*canonical.CanonicalRerankRequest, error) {
return nil, errors.New("decode rerank failed")
}
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeRerank: true}
providerAdapter := newMockAdapter("provider", false)
providerAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeRerank: true}
_ = engine.RegisterAdapter(clientAdapter)
_ = engine.RegisterAdapter(providerAdapter)
body := []byte(`{"model":"rerank","query":"test","documents":["a"]}`)
result, err := engine.convertRerankBody(clientAdapter, providerAdapter, NewTargetProvider("", "", ""), body)
require.NoError(t, err)
assert.Equal(t, body, result)
}
func TestConvertBody_UnknownInterfaceType(t *testing.T) {
registry := NewMemoryRegistry()
engine := NewConversionEngine(registry, nil)
clientAdapter := newMockAdapter("client", false)
providerAdapter := newMockAdapter("provider", false)
_ = engine.RegisterAdapter(clientAdapter)
_ = engine.RegisterAdapter(providerAdapter)
body := []byte(`{"test":"data"}`)
result, err := engine.convertBody(InterfaceType("UNKNOWN"), clientAdapter, providerAdapter, NewTargetProvider("", "", ""), body)
require.NoError(t, err)
assert.Equal(t, body, result)
}

View File

@@ -13,18 +13,20 @@ import (
// mockProtocolAdapter 模拟协议适配器
type mockProtocolAdapter struct {
protocolName string
passthrough bool
ifaceType InterfaceType
supportsIface map[InterfaceType]bool
decodeReqFn func([]byte) (*canonical.CanonicalRequest, error)
encodeReqFn func(*canonical.CanonicalRequest, *TargetProvider) ([]byte, error)
decodeRespFn func([]byte) (*canonical.CanonicalResponse, error)
encodeRespFn func(*canonical.CanonicalResponse) ([]byte, error)
streamDecoderFn func() StreamDecoder
streamEncoderFn func() StreamEncoder
rewriteReqFn func([]byte, string, InterfaceType) ([]byte, error)
rewriteRespFn func([]byte, string, InterfaceType) ([]byte, error)
protocolName string
passthrough bool
ifaceType InterfaceType
supportsIface map[InterfaceType]bool
decodeReqFn func([]byte) (*canonical.CanonicalRequest, error)
encodeReqFn func(*canonical.CanonicalRequest, *TargetProvider) ([]byte, error)
decodeRespFn func([]byte) (*canonical.CanonicalResponse, error)
encodeRespFn func(*canonical.CanonicalResponse) ([]byte, error)
streamDecoderFn func() StreamDecoder
streamEncoderFn func() StreamEncoder
rewriteReqFn func([]byte, string, InterfaceType) ([]byte, error)
rewriteRespFn func([]byte, string, InterfaceType) ([]byte, error)
decodeEmbeddingReqFn func([]byte) (*canonical.CanonicalEmbeddingRequest, error)
decodeRerankReqFn func([]byte) (*canonical.CanonicalRerankRequest, error)
}
func newMockAdapter(name string, passthrough bool) *mockProtocolAdapter {
@@ -126,6 +128,9 @@ func (m *mockProtocolAdapter) EncodeModelInfoResponse(info *canonical.CanonicalM
}
func (m *mockProtocolAdapter) DecodeEmbeddingRequest(raw []byte) (*canonical.CanonicalEmbeddingRequest, error) {
if m.decodeEmbeddingReqFn != nil {
return m.decodeEmbeddingReqFn(raw)
}
return &canonical.CanonicalEmbeddingRequest{}, nil
}
@@ -142,6 +147,9 @@ func (m *mockProtocolAdapter) EncodeEmbeddingResponse(resp *canonical.CanonicalE
}
func (m *mockProtocolAdapter) DecodeRerankRequest(raw []byte) (*canonical.CanonicalRerankRequest, error) {
if m.decodeRerankReqFn != nil {
return m.decodeRerankReqFn(raw)
}
return &canonical.CanonicalRerankRequest{}, nil
}

View File

@@ -409,3 +409,25 @@ func TestDecodeResponse_Refusal(t *testing.T) {
}
assert.True(t, found)
}
func TestDecodeRequest_AssistantContentArray(t *testing.T) {
body := []byte(`{
"model": "gpt-4",
"messages": [
{"role": "user", "content": "hello"},
{
"role": "assistant",
"content": [{"type": "text", "text": "hello back"}]
}
]
}`)
req, err := decodeRequest(body)
require.NoError(t, err)
assert.Len(t, req.Messages, 2)
assistantMsg := req.Messages[1]
assert.Equal(t, canonical.RoleAssistant, assistantMsg.Role)
assert.Len(t, assistantMsg.Content, 1)
assert.Equal(t, "text", assistantMsg.Content[0].Type)
assert.Equal(t, "hello back", assistantMsg.Content[0].Text)
}