- history_toolcalls_gap_test.go 改成正向断言:单路回合经 StartRoundWithTurnID 带权威 turnId ⇒ 落库行按该标识查得到 ⇒ HTTP 出口 toolCalls 在场;并保留「无权威标识 ⇒ 整体缺席」边界用例 - 组合根补 VoiceTranscoder: a.transcoder ⇒ 微信/QQ 的 AMR 语音在落盘前先转 wav 再落副本 - 4 个变异自检(删注入/摘 ctx 标识/清 turn_id/调换 wiring 顺序,均红)
69 lines
3.6 KiB
Go
69 lines
3.6 KiB
Go
package main
|
||
|
||
import (
|
||
"os"
|
||
"reflect"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/transcode"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/channelbridge"
|
||
)
|
||
|
||
// TestWireChannels_InjectsVoiceTranscoder 是一道「接线存在性」防回归检查
|
||
// (裁决 11;与同目录 `TestWireChannels_WiresInboundDocumentStore` 同一手法)。
|
||
//
|
||
// 钉住的事实:组合根把 `wireTranscode` 装配的本地转码引擎交给渠道 bridge,于是**不可播
|
||
// 格式的入站语音(微信 / QQ 的 AMR)在落副本之前先被转成可播副本**
|
||
// (design/25 §7.1 第 ④ 步)。少了这一行,bridge 按 nil 走它的诚实路径 —— 那句
|
||
// `category=transcode_not_wired` 的 WARN 照记、消息一个字不丢,**但那条语音不落副本**
|
||
// ⇒ 客户端拿不到可回放的字节,而**没有任何既有用例会变红**。所以必须显式钉住。
|
||
//
|
||
// 为什么是源码级检查而不是端到端:组合根本身无法在单元测试里实例化(开库、迁移、绑端口,
|
||
// 而后端还在运行);退化的行为侧(「转码不可用 ⇒ 不落副本」)已由
|
||
// `internal/services/channelbridge/inbound_voice_copy_test.go` 覆盖,这里要钉的只是
|
||
// **那一行注入**(以及它的**时机**,见下)。
|
||
func TestWireChannels_InjectsVoiceTranscoder(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
src := readSource(t, "wire_channels.go")
|
||
|
||
// 断言的是**一整行字段赋值**(行首到行尾),因此被注释掉的那一行
|
||
// (`\t\t// VoiceTranscoder: …`)或写在别处的同名字符串都不会让它通过。
|
||
const assignment = "\n\t\tVoiceTranscoder: a.transcoder,"
|
||
if !strings.Contains(src, assignment) {
|
||
t.Fatalf("wire_channels.go 里找不到 %q —— 入站语音的 AMR 不会被转成可播副本,"+
|
||
"微信语音因此不落副本(design/25 §7.1 第 ④ 步 / 裁决 11)", strings.TrimSpace(assignment))
|
||
}
|
||
|
||
// 注入面必须真装得下我们交进去的那个值:字段类型与 `a.transcoder` 的静态类型同源,
|
||
// 否则「接线成功」这句话就只是字面巧合(例如字段被改成一条自定义窄接口)。
|
||
field, ok := reflect.TypeOf(channelbridge.Options{}).FieldByName("VoiceTranscoder")
|
||
if !ok {
|
||
t.Fatal("channelbridge.Options 没有 VoiceTranscoder 字段:注入面无处可放(设计改了?)")
|
||
}
|
||
if want := reflect.TypeOf((*transcode.Transcoder)(nil)).Elem(); field.Type != want {
|
||
t.Fatalf("channelbridge.Options.VoiceTranscoder 的静态类型 = %v,want %v", field.Type, want)
|
||
}
|
||
|
||
// 注入的**时机**同样要钉住:`a.transcoder` 由 `wireTranscode()`(在 `wireRuntime` 里)
|
||
// 赋值,所以 `wireRuntime` 必须先于 `wireChannels` 执行。反过来的话上面那行注入的是
|
||
// **nil** —— 一切照旧、一条日志都不差,只有「AMR 语音落副本」这条能力静默消失。
|
||
app := readSource(t, "wire.go")
|
||
idxRuntime, idxChannels := strings.Index(app, "a.wireRuntime()"), strings.Index(app, "a.wireChannels()")
|
||
if idxRuntime < 0 || idxChannels < 0 || idxRuntime > idxChannels {
|
||
t.Fatalf("wire.go 里 wireRuntime / wireChannels 的先后不对(偏移 %d vs %d):转码引擎必须"+
|
||
"在渠道桥被构造**之前**装配好,否则 VoiceTranscoder 注入的是 nil", idxRuntime, idxChannels)
|
||
}
|
||
}
|
||
|
||
// readSource 读一个组合根源文件并把行尾归一成 LF(断言里的 `\n` 因此与平台无关)。
|
||
func readSource(t *testing.T, name string) string {
|
||
t.Helper()
|
||
raw, err := os.ReadFile(name)
|
||
if err != nil {
|
||
t.Fatalf("read %s: %v", name, err)
|
||
}
|
||
return strings.ReplaceAll(string(raw), "\r\n", "\n")
|
||
}
|