// Deterministic coverage for the flyto.TranscriptionProvider implementation: // a httptest server stands in for the OpenAI-compatible ASR endpoint and // asserts the wire contract (path, auth, multipart fields, streamed audio // bytes), while fixtures exercise the response parsing branches. // // flyto.TranscriptionProvider 实现的确定性覆盖: httptest 假服务端扮演 OpenAI // 兼容 ASR 端点, 断言线上契约 (路径 / 鉴权 / multipart 字段 / 流式音频字节), // fixture 覆盖响应解析分支. package openai import ( "context" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "git.flytoex.net/yuanwei/flyto-agent/core/pkg/flyto" ) func TestTranscribe_WireContractAndParse(t *testing.T) { var gotPath, gotAuth string var gotFields map[string]string var gotAudio string var gotFilename string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotAuth = r.Header.Get("Authorization") if err := r.ParseMultipartForm(1 << 20); err != nil { t.Errorf("parse multipart: %v", err) http.Error(w, "bad multipart", http.StatusBadRequest) return } gotFields = map[string]string{} for k, v := range r.MultipartForm.Value { gotFields[k] = v[0] } f, hdr, err := r.FormFile("file") if err != nil { t.Errorf("form file: %v", err) http.Error(w, "no file", http.StatusBadRequest) return } defer f.Close() b, _ := io.ReadAll(f) gotAudio = string(b) gotFilename = hdr.Filename w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"text":"你好 世界","language":"zh","duration":6.25,` + `"segments":[{"start":0,"end":6.25,"text":"你好 世界","speaker":"销售",` + `"words":[{"word":"你好","start":0.2,"end":0.4}]}]}`)) })) defer srv.Close() p := New(Config{APIKey: "sk-test", BaseURL: srv.URL}) resp, err := p.Transcribe(context.Background(), &flyto.TranscriptionRequest{ Audio: strings.NewReader("RIFF-fake-wav-bytes"), Filename: "call.wav", Model: "qwen3-asr-1.7b-audio8-text4", Language: "zh", ResponseFormat: "verbose_json", WordTimestamps: boolPtr(true), DiarizeBackend: "energy_tripass", LeftSpeaker: "销售", RightSpeaker: "客户", ChunkMinutes: 12.5, Extra: map[string]string{"temperature": "0", "model": "must-not-override"}, }) if err != nil { t.Fatalf("Transcribe: %v", err) } if gotPath != "/v1/audio/transcriptions" { t.Errorf("path = %q", gotPath) } if gotAuth != "Bearer sk-test" { t.Errorf("auth = %q", gotAuth) } if gotFilename != "call.wav" { t.Errorf("filename = %q", gotFilename) } if gotAudio != "RIFF-fake-wav-bytes" { t.Errorf("audio bytes = %q (must stream verbatim)", gotAudio) } want := map[string]string{ "model": "qwen3-asr-1.7b-audio8-text4", // typed field wins over Extra "language": "zh", "response_format": "verbose_json", "word_timestamps": "true", "diarize_backend": "energy_tripass", "left_speaker": "销售", "right_speaker": "客户", "chunk_minutes": "12.5", "temperature": "0", // Extra passthrough } for k, v := range want { if gotFields[k] != v { t.Errorf("field %s = %q, want %q", k, gotFields[k], v) } } if _, present := gotFields["long_audio"]; present { t.Error("zero-valued LongAudio must not be sent (server default applies)") } if resp.Text != "你好 世界" || resp.Language != "zh" || resp.Duration != 6.25 { t.Errorf("parsed response = %+v", resp) } if len(resp.Segments) != 1 || resp.Segments[0].Speaker != "销售" || len(resp.Segments[0].Words) != 1 || resp.Segments[0].Words[0].Word != "你好" { t.Errorf("segments = %+v", resp.Segments) } } func TestTranscribe_TextFormatRawBody(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("plain transcript body")) })) defer srv.Close() p := New(Config{APIKey: "sk-test", BaseURL: srv.URL}) resp, err := p.Transcribe(context.Background(), &flyto.TranscriptionRequest{ Audio: strings.NewReader("x"), Model: "m", ResponseFormat: "text", }) if err != nil { t.Fatalf("Transcribe: %v", err) } if resp.Text != "plain transcript body" { t.Errorf("text = %q", resp.Text) } } func TestTranscribe_HTTPErrorSurfacesBody(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) })) defer srv.Close() p := New(Config{APIKey: "bad", BaseURL: srv.URL}) _, err := p.Transcribe(context.Background(), &flyto.TranscriptionRequest{ Audio: strings.NewReader("x"), Model: "m", }) if err == nil || !strings.Contains(err.Error(), "401") || !strings.Contains(err.Error(), "unauthorized") { t.Fatalf("want loud 401 with body snippet, got %v", err) } } func TestTranscribe_InputValidation(t *testing.T) { p := New(Config{APIKey: "k", BaseURL: "http://unused"}) if _, err := p.Transcribe(context.Background(), nil); err == nil { t.Error("nil request must error") } if _, err := p.Transcribe(context.Background(), &flyto.TranscriptionRequest{Model: "m"}); err == nil { t.Error("nil audio must error") } if _, err := p.Transcribe(context.Background(), &flyto.TranscriptionRequest{Audio: strings.NewReader("x")}); err == nil { t.Error("empty model must error") } noKey := New(Config{BaseURL: "http://unused"}) if _, err := noKey.Transcribe(context.Background(), &flyto.TranscriptionRequest{Audio: strings.NewReader("x"), Model: "m"}); err == nil { t.Error("missing APIKey must error") } } // boolPtr builds a *bool literal for the tristate WordTimestamps field. // boolPtr 构造三态 WordTimestamps 的 *bool 字面量. func boolPtr(b bool) *bool { return &b } // TestTranscribeWordTimestampsTristate -- nil omits the field (server default // applies), explicit false hits the wire (the long-audio aligner escape // hatch), and an Extra key must not be shadowed by an UNSET typed field. // // TestTranscribeWordTimestampsTristate -- nil 不上线 (服务端默认), 显式 false // 必须上线 (长音频对齐器出路), Extra 键不得被未赋值的类型化字段遮蔽. func TestTranscribeWordTimestampsTristate(t *testing.T) { for _, tc := range []struct { name string wt *bool extra map[string]string want string // expected word_timestamps form value; "" = absent }{ {name: "nil omits", wt: nil, want: ""}, {name: "explicit false sent", wt: boolPtr(false), want: "false"}, {name: "explicit true sent", wt: boolPtr(true), want: "true"}, {name: "extra passes through unset typed name", wt: nil, extra: map[string]string{"word_timestamps": "false"}, want: "false"}, } { t.Run(tc.name, func(t *testing.T) { var got url.Values srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(1 << 20); err != nil { t.Errorf("parse form: %v", err) } got = url.Values(r.MultipartForm.Value) _, _ = w.Write([]byte(`{"text":"ok"}`)) })) defer srv.Close() p := New(Config{APIKey: "sk", BaseURL: srv.URL}) _, err := p.Transcribe(context.Background(), &flyto.TranscriptionRequest{ Audio: strings.NewReader("x"), Model: "m", WordTimestamps: tc.wt, Extra: tc.extra, }) if err != nil { t.Fatalf("Transcribe: %v", err) } if tc.want == "" { if _, present := got["word_timestamps"]; present { t.Errorf("word_timestamps must be absent, got %v", got["word_timestamps"]) } } else if got.Get("word_timestamps") != tc.want { t.Errorf("word_timestamps = %q, want %q", got.Get("word_timestamps"), tc.want) } }) } } // TestTranscribeExtraOnAlignerOverflow -- an unknown extension param // (on_aligner_overflow) flows to the wire verbatim via Extra. // // TestTranscribeExtraOnAlignerOverflow -- 未知扩展参数经 Extra 逐字上线. func TestTranscribeExtraOnAlignerOverflow(t *testing.T) { var got url.Values srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = r.ParseMultipartForm(1 << 20) got = url.Values(r.MultipartForm.Value) _, _ = w.Write([]byte(`{"text":"ok"}`)) })) defer srv.Close() p := New(Config{APIKey: "sk", BaseURL: srv.URL}) _, err := p.Transcribe(context.Background(), &flyto.TranscriptionRequest{ Audio: strings.NewReader("x"), Model: "m", Extra: map[string]string{"on_aligner_overflow": "chunk"}, }) if err != nil { t.Fatalf("Transcribe: %v", err) } if got.Get("on_aligner_overflow") != "chunk" { t.Errorf("on_aligner_overflow = %q, want chunk", got.Get("on_aligner_overflow")) } }