package app import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" ) type timeoutNetError struct{} func (timeoutNetError) Error() string { return "timeout" } func (timeoutNetError) Timeout() bool { return true } func (timeoutNetError) Temporary() bool { return true } func TestNormalizeCardRecognitionCandidates(t *testing.T) { got := normalizeCardRecognitionCandidates([]string{ "ICCID: 8986-0401-7854-7796-048", "8986040178547796048", "89860401785477960480", "7986040178547796048", "898604017854779604", "898604017854779604800", }) want := []string{ "8986040178547796048", "89860401785477960480", } if len(got) != len(want) { t.Fatalf("len(got) = %d, want %d, got=%v", len(got), len(want), got) } for index := range want { if got[index] != want[index] { t.Fatalf("got[%d] = %s, want %s", index, got[index], want[index]) } } } func TestRequestCardRecognitionModelUsesEinoMultimodalMessage(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if request.URL.Path != "/v1/chat/completions" { t.Errorf("path = %s, want /v1/chat/completions", request.URL.Path) } var body struct { Messages []struct { Content []struct { Type string `json:"type"` ImageURL struct { URL string `json:"url"` } `json:"image_url"` } `json:"content"` } `json:"messages"` } if err := json.NewDecoder(request.Body).Decode(&body); err != nil { t.Errorf("decode request: %v", err) } if len(body.Messages) != 1 || len(body.Messages[0].Content) != 2 { t.Errorf("unexpected messages: %+v", body.Messages) } else if !strings.HasPrefix(body.Messages[0].Content[1].ImageURL.URL, "data:image/png;base64,") { t.Errorf("image url = %s", body.Messages[0].Content[1].ImageURL.URL) } writer.Header().Set("Content-Type", "application/json") _, _ = writer.Write([]byte(`{"id":"chatcmpl-test","object":"chat.completion","created":1,"model":"test-model","choices":[{"index":0,"message":{"role":"assistant","content":"{\"code\":\"CARD_RECOGNIZED\",\"iccid\":\"8986040178547796048\"}"},"finish_reason":"stop"}]}`)) })) defer server.Close() content, err := requestCardRecognitionModel(context.Background(), cardRecognitionConfig{ APIKey: "test-key", BaseURL: server.URL + "/v1", Model: "test-model", Timeout: 3 * time.Second, MaxTokens: 64, }, []byte("image"), "image/png") if err != nil { t.Fatalf("requestCardRecognitionModel() error = %v", err) } if !strings.Contains(content, "8986040178547796048") { t.Fatalf("content = %s", content) } } func TestParseCardRecognitionResultMarkdownJSON(t *testing.T) { got := parseCardRecognitionResult("```json\n{\"code\":\"CARD_RECOGNIZED\",\"cardType\":\"iccid\",\"iccid\":\"8986040178547796048\",\"candidates\":[\"8986040178547796048\"],\"confidence\":1.2}\n```") if got.Code != cardRecognitionCodeRecognized { t.Fatalf("Code = %s, want %s", got.Code, cardRecognitionCodeRecognized) } if got.Confidence != 1 { t.Fatalf("Confidence = %v, want 1", got.Confidence) } if len(got.Candidates) != 1 || got.Candidates[0] != "8986040178547796048" { t.Fatalf("Candidates = %v", got.Candidates) } } func TestParseCardRecognitionResultAmbiguous(t *testing.T) { got := parseCardRecognitionResult(`{ "code": "CARD_AMBIGUOUS", "cardType": "iccid", "candidates": ["8986040178547796048", "89860401785477960480"], "confidence": 0.84 }`) if got.Code != cardRecognitionCodeAmbiguous { t.Fatalf("Code = %s, want %s", got.Code, cardRecognitionCodeAmbiguous) } if len(got.Candidates) != 2 { t.Fatalf("Candidates = %v", got.Candidates) } } func TestParseCardRecognitionResultFiltersInvalidCandidates(t *testing.T) { got := parseCardRecognitionResult(`{ "code": "CARD_RECOGNIZED", "iccid": "13800008000", "candidates": ["898604017854779604", "898604017854779604800", "7986040178547796048"], "confidence": -0.4 }`) if got.Confidence != 0 { t.Fatalf("Confidence = %v, want 0", got.Confidence) } if len(got.Candidates) != 0 { t.Fatalf("Candidates = %v, want empty", got.Candidates) } } func TestParseCardRecognitionResultFallbackFromText(t *testing.T) { got := parseCardRecognitionResult("识别到 ICCID 8986040178547796048,另一个候选是 89860401785477960480。") if got.Code != cardRecognitionCodeAmbiguous { t.Fatalf("Code = %s, want %s", got.Code, cardRecognitionCodeAmbiguous) } if len(got.Candidates) != 2 { t.Fatalf("Candidates = %v", got.Candidates) } } func TestClampCardRecognitionConfidence(t *testing.T) { if got := clampCardRecognitionConfidence(-0.1); got != 0 { t.Fatalf("negative clamp = %v, want 0", got) } if got := clampCardRecognitionConfidence(1.1); got != 1 { t.Fatalf("over one clamp = %v, want 1", got) } if got := clampCardRecognitionConfidence(0.5); got != 0.5 { t.Fatalf("normal clamp = %v, want 0.5", got) } } func TestIsCardRecognitionTimeoutError(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) defer cancel() <-ctx.Done() if !isCardRecognitionTimeoutError(ctx, errors.New("other")) { t.Fatal("deadline exceeded context should be treated as timeout") } if !isCardRecognitionTimeoutError(context.Background(), timeoutNetError{}) { t.Fatal("net timeout error should be treated as timeout") } if isCardRecognitionTimeoutError(context.Background(), errors.New("temporary unavailable")) { t.Fatal("non-timeout error should not be treated as timeout") } }