Files
iotlcm/server/internal/logic/app/app_card_recognition_test.go
T
MiFaZhan 2ed5dfa6a1 chore: 初始开源发布
iotlcm-app:小程序端 + 用户端服务。

- app/:uni-app 小程序前端(基于 unibest,MIT)
  - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID
  - 内网后端地址 → admin.iotlcm.example.com
  - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明)
- server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0)
  - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件)
  - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化
  - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位)
  - 依赖号码尾部的断言同步更新
  - 补 server/LICENSE(Apache-2.0)
- 移除内部资产:CI 触发器
- 补根 README,LF 行尾 + .gitattributes

已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
2026-10-26 14:01:25 +08:00

165 lines
5.4 KiB
Go

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")
}
}