package mediafetch // 本文件锁定**入站语音字节内联**的口径(切片 IM-ASR):音频白名单命中/未命中、平台 // 已给文字就不取字节、占位标记不算文字、体积与条数上限、失败不炸,以及**音频白名单 // 绝不污染文档白名单**(后者是 Web 文档上传的口径)。 import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/documents" ) // voiceMessage 构造一条「无正文的纯语音」入站消息(telegram/QQ 的形状)。 func voiceMessage(url string, m adapter.Media) *adapter.UnifiedMessage { return &adapter.UnifiedMessage{ Platform: "test", ContentType: adapter.ContentVoice, Media: []adapter.Media{m}, } } // TestInlineVoiceFetchesAllowedAudio 是正向用例:白名单格式的语音被内联进 Data。 func TestInlineVoiceFetchesAllowedAudio(t *testing.T) { t.Parallel() f := &countingFetcher{data: []byte("audio")} msg := voiceMessage("https://cdn.example.com/a.mp3", adapter.Media{ Kind: adapter.MediaVoice, URL: "https://cdn.example.com/a.mp3", Name: "a.mp3", MIMEType: "audio/mpeg", }) Inline(context.Background(), testLogger(t), msg, f) if f.calls() != 1 { t.Fatalf("取回次数 = %d, want 1", f.calls()) } if string(msg.Media[0].Data) != "audio" { t.Fatalf("Data = %q, want 取回的字节", msg.Media[0].Data) } if format, ok := AudioFormatOf(msg.Media[0]); !ok || format != "mp3" { t.Fatalf("AudioFormatOf = %q/%v, want mp3/true", format, ok) } } // TestInlineVoiceAcceptsMIMEOnlyReference 钉住 telegram 的形状:语音只有 `file_id` 与 // 平台声明的 `audio/ogg`,**连文件名都没有** —— 只看扩展名会把 telegram 语音全数拒掉。 func TestInlineVoiceAcceptsMIMEOnlyReference(t *testing.T) { t.Parallel() f := &countingFetcher{data: []byte("ogg")} msg := voiceMessage("AwACAgIAAxkBAAI", adapter.Media{ Kind: adapter.MediaVoice, URL: "AwACAgIAAxkBAAI", MIMEType: "audio/ogg", }) Inline(context.Background(), testLogger(t), msg, f) if f.calls() != 1 { t.Fatalf("取回次数 = %d, want 1(MIME 兜底必须生效)", f.calls()) } format, ok := AudioFormatOf(msg.Media[0]) if !ok || format != "ogg" { t.Fatalf("AudioFormatOf = %q/%v, want ogg/true", format, ok) } } // TestInlineVoiceSkipsUnsupportedAudioFormat 钉住预筛:不在音频白名单内的**一个字节都不 // 发出去**(SILK 需要转码、可执行文件更不该取)。 func TestInlineVoiceSkipsUnsupportedAudioFormat(t *testing.T) { t.Parallel() for _, name := range []string{"v.silk", "v.exe", "voice", ""} { t.Run(name, func(t *testing.T) { t.Parallel() f := &countingFetcher{data: []byte("x")} msg := voiceMessage("https://cdn.example.com/whatever", adapter.Media{ Kind: adapter.MediaVoice, URL: "https://cdn.example.com/whatever", Name: name, }) Inline(context.Background(), testLogger(t), msg, f) if f.calls() != 0 { t.Fatalf("格式 %q 不在音频白名单里却发起了取回", name) } if len(msg.Media[0].Data) != 0 { t.Fatalf("格式 %q 不该被内联", name) } }) } } // TestAudioWhitelistDoesNotPolluteDocumentWhitelist 钉住切片的硬红线:音频白名单是 // **独立**的一份,`documents.ExtAllowed`(Web 文档上传口径)绝不能被音频格式污染。 func TestAudioWhitelistDoesNotPolluteDocumentWhitelist(t *testing.T) { t.Parallel() for _, ext := range []string{".wav", ".mp3", ".ogg", ".oga", ".opus", ".m4a", ".aac", ".amr", ".flac", ".webm"} { if !audioExtAllowed(ext) { t.Fatalf("audioExtAllowed(%q) = false, want true", ext) } if documents.ExtAllowed(ext) { t.Fatalf("documents.ExtAllowed(%q) = true —— 音频格式污染了文档白名单", ext) } } } // TestInlineVoiceSkipsWhenChatTextPresent 钉住「平台自带转写优先、不重复付费」: // `Content` 已有真实文本(微信的 voice_item.text)⇒ **连字节都不取**。 // // 同一用例里钉住反面:QQ 的 `[语音]` 是**占位标记**、不是文字 ⇒ 仍然要取字节与转写, // 否则纯语音会被当成「已有正文」而整条丢掉。 func TestInlineVoiceSkipsWhenChatTextPresent(t *testing.T) { t.Parallel() cases := []struct { name string content string want int }{ {"平台自带转写", "你好呀", 0}, {"用户正文", "看看这条语音", 0}, {"QQ 占位标记", "[语音]", 1}, {"QQ 占位标记+空白", " [语音] ", 1}, {"空正文", "", 1}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() f := &countingFetcher{data: []byte("x")} msg := voiceMessage("https://cdn.example.com/a.ogg", adapter.Media{ Kind: adapter.MediaVoice, URL: "https://cdn.example.com/a.ogg", Name: "a.ogg", }) msg.Content = tc.content Inline(context.Background(), testLogger(t), msg, f) if f.calls() != tc.want { t.Fatalf("取回次数 = %d, want %d(正文 %q)", f.calls(), tc.want, tc.content) } }) } } // TestChatTextStripsMediaPlaceholders 钉住「有没有真实文本」的判定只有一份。 func TestChatTextStripsMediaPlaceholders(t *testing.T) { t.Parallel() cases := []struct { content string want string }{ {"[语音]", ""}, {"[图片][语音]", ""}, {" [文件] ", ""}, {"@300 look [图片][语音]", "@300 look"}, {"你好", "你好"}, {"", ""}, } for _, tc := range cases { if got := ChatText(tc.content); got != tc.want { t.Fatalf("ChatText(%q) = %q, want %q", tc.content, got, tc.want) } } } // TestInlineVoiceSkipsExistingBytesAndOversize 钉住两条省流:已有字节不重复下载; // 平台声明的体积超语音上限时连请求都不发。 func TestInlineVoiceSkipsExistingBytesAndOversize(t *testing.T) { t.Parallel() t.Run("已有字节", func(t *testing.T) { t.Parallel() f := &countingFetcher{data: []byte("new")} msg := voiceMessage("https://cdn.example.com/a.ogg", adapter.Media{ Kind: adapter.MediaVoice, URL: "https://cdn.example.com/a.ogg", Name: "a.ogg", Data: []byte("local"), }) Inline(context.Background(), testLogger(t), msg, f) if f.calls() != 0 || string(msg.Media[0].Data) != "local" { t.Fatalf("已有字节的语音被重复下载或覆盖:calls=%d data=%q", f.calls(), msg.Media[0].Data) } }) t.Run("声明超限", func(t *testing.T) { t.Parallel() f := &countingFetcher{data: []byte("x")} msg := voiceMessage("https://cdn.example.com/big.ogg", adapter.Media{ Kind: adapter.MediaVoice, URL: "https://cdn.example.com/big.ogg", Name: "big.ogg", Size: MaxVoiceBytes + 1, }) Inline(context.Background(), testLogger(t), msg, f) if f.calls() != 0 { t.Fatalf("声明超限的语音仍被取回 %d 次, want 0", f.calls()) } }) } // TestInlineVoiceCapsPerMessage 钉住条数上限(与文件附件同源的常量)。 func TestInlineVoiceCapsPerMessage(t *testing.T) { t.Parallel() f := &countingFetcher{data: []byte("x")} msg := voiceMessage("https://cdn.example.com/0.ogg", adapter.Media{ Kind: adapter.MediaVoice, URL: "https://cdn.example.com/0.ogg", Name: "0.ogg", }) for i := 1; i <= documents.MaxPerMessage+2; i++ { msg.Media = append(msg.Media, adapter.Media{ Kind: adapter.MediaVoice, URL: "https://cdn.example.com/x.ogg", Name: "x.ogg", }) } Inline(context.Background(), testLogger(t), msg, f) if f.calls() != documents.MaxPerMessage { t.Fatalf("取回次数 = %d, want %d", f.calls(), documents.MaxPerMessage) } } // TestInlineVoiceFailureKeepsMessageAndContinues 钉住「失败不炸」:取不到字节时消息照旧、 // 后续语音继续尝试,且**绝不**改动正文(下游据此说「没拿到音频字节」)。 func TestInlineVoiceFailureKeepsMessageAndContinues(t *testing.T) { t.Parallel() f := &countingFetcher{err: ErrBadStatus} msg := voiceMessage("https://cdn.example.com/a.ogg", adapter.Media{ Kind: adapter.MediaVoice, URL: "https://cdn.example.com/a.ogg", Name: "a.ogg", }) msg.Media = append(msg.Media, adapter.Media{ Kind: adapter.MediaVoice, URL: "https://cdn.example.com/b.ogg", Name: "b.ogg", }) Inline(context.Background(), testLogger(t), msg, f) if f.calls() != 2 { t.Fatalf("取回次数 = %d, want 2(一次失败不阻断第二次)", f.calls()) } for i, m := range msg.Media { if len(m.Data) != 0 { t.Fatalf("Media[%d] 在失败时被写入了字节", i) } } if msg.Content != "" { t.Fatalf("失败路径改动了正文:%q(诚实提示由桥接器负责,取回层不写正文)", msg.Content) } }