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:
AskaEth committed 2026-10-06 15:59:14 +08:00
1 parent a30f4a49ff
commit ddd3d00d09
3 files changed
+196 -42

No files matched your search

+6
View File
@@ -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)
+68
View File
@@ -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")
}