package yandex import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" stt "github.com/yandex-cloud/go-genproto/yandex/cloud/ai/stt/v3" "google.golang.org/protobuf/proto" ) // Сохранённый ответ провайдера — единственное, из чего пересчитывается архив: // результат операции у SpeechKit не переспрашивается, и повторное распознавание // стоит денег. Поэтому проверки ниже судят не «разбор чего-то вернул», а // сохранность самого ответа. // response собирает ответ потока с одной репликой. func response(text string, start, end int64) *stt.StreamingResponse { return &stt.StreamingResponse{ Event: &stt.StreamingResponse_FinalRefinement{ FinalRefinement: &stt.FinalRefinement{ Type: &stt.FinalRefinement_NormalizedText{ NormalizedText: &stt.AlternativeUpdate{ Alternatives: []*stt.Alternative{ {Text: text, StartTimeMs: start, EndTimeMs: end}, }, }, }, }, }, } } // Сохранённое читается обратно тем же: реплики со временем и плоский текст. func TestPayloadSurvivesRoundTrip(t *testing.T) { responses := []*stt.StreamingResponse{ response("Первая реплика.", 0, 900), response("Вторая реплика.", 900, 1800), } raw, err := encodeResponses(responses) require.NoError(t, err) require.NotEmpty(t, raw) decoded, err := decodeResponses(raw) require.NoError(t, err) require.Len(t, decoded, 2) outcome := outcomeFromResponses(decoded) require.Len(t, outcome.Replicas, 2) assert.Equal(t, "Первая реплика.", outcome.Replicas[0].Text) assert.Equal(t, int64(0), outcome.Replicas[0].StartMs) assert.Equal(t, int64(900), outcome.Replicas[0].EndMs) assert.Equal(t, "Вторая реплика.", outcome.Replicas[1].Text) assert.Equal(t, "Первая реплика. Вторая реплика.", outcome.PlainText) } // Ради этого свойства сохранение и сделано двоичным. Провайдер добавляет поля // без предупреждения, и текстовое представление, собранное по нашей // скомпилированной схеме, выбросило бы их молча — а пересчитать архив было бы // уже не из чего: операция не переспрашивается. func TestUnknownProviderFieldSurvivesStorage(t *testing.T) { original := response("Реплика.", 0, 500) // Так выглядит поле, которого наша схема не знает: провайдер прислал его, // разбор положил в неизвестные. unknown := protoimplUnknown(t) original.ProtoReflect().SetUnknown(unknown) require.NotEmpty(t, original.ProtoReflect().GetUnknown(), "неизвестное поле поставлено") raw, err := encodeResponses([]*stt.StreamingResponse{original}) require.NoError(t, err) decoded, err := decodeResponses(raw) require.NoError(t, err) require.Len(t, decoded, 1) assert.Equal(t, []byte(unknown), []byte(decoded[0].ProtoReflect().GetUnknown()), "неизвестное провайдерское поле пережило запись и чтение") // И известное при этом на месте. outcome := outcomeFromResponses(decoded) require.Len(t, outcome.Replicas, 1) assert.Equal(t, "Реплика.", outcome.Replicas[0].Text) } // Обрезанное вложение узнаётся отказом, а не половиной расшифровки: половина // текста, выданная за целую, тише и хуже отказа. func TestTruncatedPayloadIsRefused(t *testing.T) { raw, err := encodeResponses([]*stt.StreamingResponse{response("Реплика.", 0, 500)}) require.NoError(t, err) require.Greater(t, len(raw), 2) _, err = decodeResponses(raw[:len(raw)-2]) assert.Error(t, err, "обрезанное вложение не разбирается молча") } // Пустой поток даёт пустой результат, а не отказ: «на записи нет текста» — // законный исход распознавания. func TestEmptyStreamGivesEmptyOutcome(t *testing.T) { raw, err := encodeResponses(nil) require.NoError(t, err) decoded, err := decodeResponses(raw) require.NoError(t, err) assert.Empty(t, decoded) outcome := outcomeFromResponses(decoded) assert.Empty(t, outcome.Replicas) assert.Empty(t, outcome.PlainText) } // Ответ без разбора текста реплик не даёт и разбор не роняет: провайдер шлёт по // потоку и служебные события. func TestResponseWithoutTextIsSkipped(t *testing.T) { responses := []*stt.StreamingResponse{ {Event: &stt.StreamingResponse_FinalRefinement{FinalRefinement: &stt.FinalRefinement{}}}, response("Реплика.", 0, 500), response("", 500, 600), } outcome := outcomeFromResponses(responses) require.Len(t, outcome.Replicas, 1, "пустые и служебные события репликами не становятся") assert.Equal(t, "Реплика.", outcome.PlainText) } // protoimplUnknown собирает байты неизвестного поля: номер поля, которого в // нашей схеме нет, с целочисленным значением. func protoimplUnknown(t *testing.T) []byte { t.Helper() // Поле 4095, тип varint, значение 7 — заведомо за пределами схемы ответа. raw, err := proto.Marshal(&stt.StreamingResponse{}) require.NoError(t, err) require.Empty(t, raw) return []byte{0xF8, 0xFF, 0x3F, 0x07} }