fix: 修「假话但绿」的用例 + 补 VoiceTranscoder 注入(W4)
- history_toolcalls_gap_test.go 改成正向断言:单路回合经 StartRoundWithTurnID 带权威 turnId ⇒ 落库行按该标识查得到 ⇒ HTTP 出口 toolCalls 在场;并保留「无权威标识 ⇒ 整体缺席」边界用例 - 组合根补 VoiceTranscoder: a.transcoder ⇒ 微信/QQ 的 AMR 语音在落盘前先转 wav 再落副本 - 4 个变异自检(删注入/摘 ctx 标识/清 turn_id/调换 wiring 顺序,均红)
This commit is contained in:
1 parent
a30f4a49ff
commit
ddd3d00d09
3 files changed
+196
-42
No files matched your search
@@ -133,6 +133,12 @@ func (a *app) wireChannels() error {
|
||||
// a.voice 同时服务 Web 侧 WS 的 voice_input(同一条降级链,不另建一条)。
|
||||
VoiceSTT: a.voice,
|
||||
VoiceSTTTimeout: time.Duration(a.cfg.Voice.VoiceSTTTimeoutSec) * time.Second,
|
||||
// VoiceTranscoder 把本地转码引擎交给入站语音副本链(design/25 §7.1 第 ④ 步;
|
||||
// 裁决 11):**不可播格式(微信/QQ 的 AMR)必须先转成可播副本再落盘**,否则
|
||||
// 那条语音只能不落副本(`category=transcode_not_wired` 的诚实 WARN),客户端
|
||||
// 便拿不到可回放的字节。注入的是 `wireTranscode` 装配的**同一个**引擎实例
|
||||
// (与工具面的 `transcode` 工具同源),不是第二条降级链。
|
||||
VoiceTranscoder: a.transcoder,
|
||||
})
|
||||
if berr != nil {
|
||||
return fmt.Errorf("channel bridge: %w", berr)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
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")
|
||||
}
|
||||
Reference in new issue
Block a user