test(channelbridge): 握手回放也断言出站 voice(M58)
- dialOwnerWSHistory 交回 history 帧,replayVoiceOfID 按 id 取 voice 原文 - 成功与降级两条整链用例都补「重连 WS 回放」断言,与实时帧逐字相等
This commit is contained in:
1 parent
462f007ec1
commit
a1d8c463e1
1 file changed
+56
-5
@@ -11,9 +11,10 @@ package channelbridge
|
||||
// 而本切片要钉的正是「结论已经在手里之后」那一段。
|
||||
//
|
||||
// 钉住四件事:
|
||||
// 1. 合成成功 ⇒ 助手回复的**第一帧**就带 `voice`(source=tts/status=transcribed + 副本四键);
|
||||
// 2. 合成降级 ⇒ `voice.status=failed` + `fallbackReason` 在帧上**与**历史回看(REST)上都可见,
|
||||
// 且正文照发原文(降级不是「少说一句话」);
|
||||
// 1. 合成成功 ⇒ 助手回复的**第一帧**就带 `voice`(source=tts/status=transcribed + 副本四键),
|
||||
// 重新连 WS 的握手回放与 REST 读面逐字相同;
|
||||
// 2. 合成降级 ⇒ `voice.status=failed` + `fallbackReason` 在帧上**与**历史回看(REST、握手回放)
|
||||
// 上都可见,且正文照发原文(降级不是「少说一句话」);
|
||||
// 3. 音频只挂在**第一个**气泡上(多段回复里重复挂会让对方收到 N 份同样的语音);
|
||||
// 4. 出站 Media 的字节就是盘上那份副本(读不回来时宁可不挂,见 outbound_voice.go)。
|
||||
|
||||
@@ -118,6 +119,14 @@ func newOutboundVoiceE2E(t *testing.T, sess session.Session) *outboundVoiceE2E {
|
||||
// dialOwnerWS 连上真网关的 `/ws/chat`(owner token),并吃掉握手回放的 `history` 帧,
|
||||
// 使后续读取从「本回合的第一帧」开始(与入站整链同一手法)。
|
||||
func (e *outboundVoiceE2E) dialOwnerWS(t *testing.T) *websocket.Conn {
|
||||
t.Helper()
|
||||
conn, _ := e.dialOwnerWSHistory(t)
|
||||
return conn
|
||||
}
|
||||
|
||||
// dialOwnerWSHistory 同 dialOwnerWS,但把握手回放的 `history` 帧一并交回 —— 出站语音
|
||||
// 也得在「隔天重新打开客户端」的那条路上看得见(列有值即带),这条读面因此要能被断言。
|
||||
func (e *outboundVoiceE2E) dialOwnerWSHistory(t *testing.T) (*websocket.Conn, wsFrame) {
|
||||
t.Helper()
|
||||
wsURL := "ws" + strings.TrimPrefix(e.ts.URL, "http") + "/ws/chat?token=" + e.tok
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
@@ -129,7 +138,30 @@ func (e *outboundVoiceE2E) dialOwnerWS(t *testing.T) *websocket.Conn {
|
||||
if ev.Type != "history" {
|
||||
t.Fatalf("第一个帧 = %q, want history(握手回放)", ev.Type)
|
||||
}
|
||||
return conn
|
||||
return conn, ev
|
||||
}
|
||||
|
||||
// replayVoiceOfID 从握手回放帧的 `data.messages` 里按 id 找到那一行,返回它的 `voice`
|
||||
// 原始 JSON(键的在场性因此可判);该行没有 `voice` 键时返回 nil 与 false。
|
||||
func replayVoiceOfID(t *testing.T, ev wsFrame, id string) (json.RawMessage, bool) {
|
||||
t.Helper()
|
||||
var data struct {
|
||||
Messages []map[string]json.RawMessage `json:"messages"`
|
||||
}
|
||||
if err := json.Unmarshal(ev.Data, &data); err != nil {
|
||||
t.Fatalf("解码握手回放帧 %s: %v", ev.Data, err)
|
||||
}
|
||||
for _, row := range data.Messages {
|
||||
var gotID string
|
||||
_ = json.Unmarshal(row["id"], &gotID)
|
||||
if gotID != id {
|
||||
continue
|
||||
}
|
||||
raw, ok := row["voice"]
|
||||
return raw, ok
|
||||
}
|
||||
t.Fatalf("握手回放里没有 id=%q 的那一行(回放=%s)", id, ev.Data)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// storeOutboundCopy 用**真**音频落盘分支(`documents.Store.SaveAudio`)把一份合成产物
|
||||
@@ -218,6 +250,15 @@ func TestIMOutboundVoiceFirstFrameCarriesVoice(t *testing.T) {
|
||||
if got, want := canonicalVoice(t, raw), canonicalVoice(t, restVoice); got != want {
|
||||
t.Fatalf("帧里的 voice = %s, REST 读面同一行 = %s", got, want)
|
||||
}
|
||||
// 握手回放(隔天重新打开客户端):同一条消息在 `history` 帧里也带着这份出站语音。
|
||||
_, replay := env.dialOwnerWSHistory(t)
|
||||
replayRaw, ok := replayVoiceOfID(t, replay, id)
|
||||
if !ok {
|
||||
t.Fatalf("🔴 握手回放里这条出站语音缺 voice 键: %s", replay.Data)
|
||||
}
|
||||
if got, want := canonicalVoice(t, replayRaw), canonicalVoice(t, raw); got != want {
|
||||
t.Fatalf("握手回放里的 voice = %s, 实时帧 = %s", got, want)
|
||||
}
|
||||
// 正文一字未改:语音是这条回复的另一种呈现,不是它的替代品。
|
||||
if got := frameContent(t, msg); got != outboundVoiceStoredText {
|
||||
t.Fatalf("帧里的正文 = %q, want %q", got, outboundVoiceStoredText)
|
||||
@@ -258,10 +299,20 @@ func TestIMOutboundVoiceFallbackIsVisibleAsFailed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
// 历史回看(REST 读面)与帧上是同一份事实 —— 隔天再打开也看得到这条降级。
|
||||
restVoice := voiceOfRowByID(t, fetchMessages(t, env.ts.URL, history.MainSessionID, env.tok), messageIDOf(t, msg))
|
||||
id := messageIDOf(t, msg)
|
||||
restVoice := voiceOfRowByID(t, fetchMessages(t, env.ts.URL, history.MainSessionID, env.tok), id)
|
||||
if got, want := canonicalVoice(t, raw), canonicalVoice(t, restVoice); got != want {
|
||||
t.Fatalf("帧里的 voice = %s, REST 读面同一行 = %s", got, want)
|
||||
}
|
||||
// 握手回放(重新连 WS)同样带着这条降级:客户端的「本来想用语音」小标不只在实时帧上有。
|
||||
_, replay := env.dialOwnerWSHistory(t)
|
||||
replayRaw, ok := replayVoiceOfID(t, replay, id)
|
||||
if !ok {
|
||||
t.Fatalf("🔴 握手回放里这条降级回复缺 voice 键: %s", replay.Data)
|
||||
}
|
||||
if got, want := canonicalVoice(t, replayRaw), canonicalVoice(t, raw); got != want {
|
||||
t.Fatalf("握手回放里的 voice = %s, 实时帧 = %s", got, want)
|
||||
}
|
||||
// 正文原样:降级只影响「有没有音频」,绝不影响说什么。
|
||||
if got := frameContent(t, msg); got != outboundVoiceStoredText {
|
||||
t.Fatalf("降级回复的正文 = %q, want %q(正文不该被降级改写)", got, outboundVoiceStoredText)
|
||||
|
||||
Reference in new issue
Block a user