初始提交:边缘AI算力机统一AI通讯层
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
CI / security-scan (push) Has been cancelled

This commit is contained in:
freedakgmail
2026-08-03 07:44:05 +08:00
commit 93a469061d
51 changed files with 11565 additions and 0 deletions
+295
View File
@@ -0,0 +1,295 @@
package compatibility
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/edgeai/gateway/internal/config"
"github.com/edgeai/gateway/internal/observability"
"github.com/edgeai/gateway/internal/server"
"github.com/edgeai/gateway/pkg/api"
)
var compatServer *httptest.Server
func TestMain(m *testing.M) {
cfg := &config.Config{
Server: config.ServerConfig{Host: "127.0.0.1", Port: 0, MaxRequestBodyMB: 5},
Auth: config.AuthConfig{Enabled: true, Methods: []string{"api_key"}},
Scheduler: config.SchedulerConfig{
MaxRunningTasks: 2, MaxQueuedTasks: 10,
},
Timeouts: config.TimeoutConfig{
DefaultQueueMs: 2000, DefaultInferenceMs: 10000, DefaultTotalMs: 15000,
},
Context: config.ContextConfig{SafetyMarginRatio: 0.1, DefaultPolicy: "recent_only"},
Models: map[string]config.ModelConfig{
"general-chat": {
Provider: "ollama", ActualModel: "qwen2.5:0.5b",
Endpoint: "http://127.0.0.1:11434", ContextWindow: 4096,
MaxOutputTokens: 256, CancelSupported: true,
},
"fast-chat": {
Provider: "ollama", ActualModel: "qwen2.5:0.5b",
Endpoint: "http://127.0.0.1:11434", ContextWindow: 2048,
MaxOutputTokens: 128, CancelSupported: true,
},
},
Observability: config.ObservabilityConfig{MetricsPath: "/metrics", LogLevel: "error"},
Storage: config.StorageConfig{
SessionDB: "sqlite:///tmp/edgeai-compat/sessions.db",
TaskState: "sqlite:///tmp/edgeai-compat/tasks.db",
},
}
os.MkdirAll("/tmp/edgeai-compat", 0755)
defer os.RemoveAll("/tmp/edgeai-compat")
logger := observability.NewLogger(observability.LevelError, os.Stderr, "metadata_only")
srv, err := server.New(cfg, logger)
if err != nil {
panic("failed to create compat test server: " + err.Error())
}
compatServer = httptest.NewServer(srv.HTTPSrv.Handler)
defer compatServer.Close()
m.Run()
}
// COMPAT-001: GET /v1/models returns OpenAI-compatible format
func TestCompat001_ModelsFormat(t *testing.T) {
req, _ := http.NewRequest("GET", compatServer.URL+"/v1/models", nil)
req.Header.Set("Authorization", "Bearer test-key")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 401 {
// If auth passes (unlikely without real key), check format
var result api.ModelListResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("failed to decode models response: %v", err)
}
if result.Object != "list" {
t.Errorf("expected object 'list', got %s", result.Object)
}
for _, m := range result.Data {
if m.Object != "model" {
t.Errorf("expected object 'model', got %s", m.Object)
}
}
}
}
// COMPAT-002: Chat request format matches OpenAI API
func TestCompat002_ChatRequestFormat(t *testing.T) {
// Verify the request body structure is OpenAI-compatible
req := api.ChatRequest{
Model: "general-chat",
Messages: []api.Message{
{Role: "system", Content: "You are a helpful assistant."},
{Role: "user", Content: "Hello!"},
},
Stream: false,
}
data, err := json.Marshal(req)
if err != nil {
t.Fatal(err)
}
// Verify JSON structure
var raw map[string]any
json.Unmarshal(data, &raw)
requiredFields := []string{"model", "messages"}
for _, field := range requiredFields {
if _, ok := raw[field]; !ok {
t.Errorf("required field %q missing from chat request", field)
}
}
// Verify messages structure
msgs, ok := raw["messages"].([]any)
if !ok || len(msgs) != 2 {
t.Fatalf("expected 2 messages, got %v", raw["messages"])
}
firstMsg, ok := msgs[0].(map[string]any)
if !ok {
t.Fatal("expected message to be object")
}
if firstMsg["role"] != "system" {
t.Errorf("expected role 'system', got %v", firstMsg["role"])
}
if firstMsg["content"] != "You are a helpful assistant." {
t.Errorf("unexpected content: %v", firstMsg["content"])
}
}
// COMPAT-003: Chat response format matches OpenAI API
func TestCompat003_ChatResponseFormat(t *testing.T) {
resp := api.ChatResponse{
RequestID: "req-123",
TaskID: "task-456",
Status: "completed",
Model: "general-chat",
Choices: []api.Choice{
{
Index: 0,
Message: &api.Message{
Role: "assistant",
Content: "Hello! How can I help you?",
},
FinishReason: "stop",
},
},
Usage: &api.Usage{
InputTokens: 10,
OutputTokens: 8,
TotalTokens: 18,
},
}
data, err := json.Marshal(resp)
if err != nil {
t.Fatal(err)
}
var raw map[string]any
json.Unmarshal(data, &raw)
// Verify OpenAI-compatible fields
if _, ok := raw["choices"]; !ok {
t.Error("choices field missing from response")
}
if _, ok := raw["model"]; !ok {
t.Error("model field missing from response")
}
}
// COMPAT-004: SSE streaming format matches OpenAI API
func TestCompat004_SSEFormat(t *testing.T) {
// Verify SSE chunk format
chunk := map[string]any{
"id": "req-123",
"object": "chat.completion.chunk",
"model": "general-chat",
"choices": []map[string]any{
{
"index": 0,
"delta": map[string]any{
"content": "Hello",
},
"finish_reason": nil,
},
},
}
data, err := json.Marshal(chunk)
if err != nil {
t.Fatal(err)
}
var raw map[string]any
json.Unmarshal(data, &raw)
if raw["object"] != "chat.completion.chunk" {
t.Errorf("expected object 'chat.completion.chunk', got %v", raw["object"])
}
}
// COMPAT-005: Error response format matches OpenAI API
func TestCompat005_ErrorFormat(t *testing.T) {
req, _ := http.NewRequest("GET", compatServer.URL+"/v1/models", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
var buf bytes.Buffer
buf.ReadFrom(resp.Body)
var errResp map[string]any
json.Unmarshal(buf.Bytes(), &errResp)
// OpenAI format: {"error": {"code": ..., "message": ...}}
errBody, ok := errResp["error"].(map[string]any)
if !ok {
t.Fatal("expected 'error' object in response")
}
if _, ok := errBody["code"]; !ok {
t.Error("expected 'code' field in error")
}
if _, ok := errBody["message"]; !ok {
t.Error("expected 'message' field in error")
}
}
// COMPAT-006: Session API follows REST conventions
func TestCompat006_SessionREST(t *testing.T) {
// POST /v1/sessions creates a session
createReq, _ := http.NewRequest("POST", compatServer.URL+"/v1/sessions", nil)
createReq.Header.Set("Content-Type", "application/json")
createReq.Header.Set("Authorization", "Bearer test-key")
createReq.Body = io.NopCloser(bytes.NewReader([]byte(`{"application_id":"test-app"}`)))
createResp, err := http.DefaultClient.Do(createReq)
if err != nil {
t.Fatal(err)
}
createResp.Body.Close()
// Should be 201 (Created) or 401 (auth)
if createResp.StatusCode != 201 && createResp.StatusCode != 401 {
t.Errorf("expected 201 or 401 for POST /v1/sessions, got %d", createResp.StatusCode)
}
// DELETE /v1/sessions/:id deletes a session
deleteReq, _ := http.NewRequest("DELETE", compatServer.URL+"/v1/sessions/test-id", nil)
deleteReq.Header.Set("Authorization", "Bearer test-key")
deleteResp, err := http.DefaultClient.Do(deleteReq)
if err != nil {
t.Fatal(err)
}
deleteResp.Body.Close()
// Should be 200, 401, or 400 (not found)
if deleteResp.StatusCode == 500 {
t.Error("expected non-500 for DELETE session")
}
}
// COMPAT-007: Multiple models in config are all listed
func TestCompat007_MultipleModels(t *testing.T) {
// Verify config has multiple models
cfg := &config.Config{
Models: map[string]config.ModelConfig{
"general-chat": {Provider: "ollama", ActualModel: "a"},
"fast-chat": {Provider: "ollama", ActualModel: "b"},
},
}
if len(cfg.Models) != 2 {
t.Errorf("expected 2 models, got %d", len(cfg.Models))
}
}
// COMPAT-008: curl-compatible request (no extra headers needed)
func TestCompat008_CurlCompatible(t *testing.T) {
// Simulate a curl request with minimal headers
req, _ := http.NewRequest("GET", compatServer.URL+"/health", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("expected 200 for simple curl-like request, got %d", resp.StatusCode)
}
}