Files
Cyrene_v2/cmd/cyrene/wire_voice_tts_test.go
AskaEth b90811fae4 docs(voice): 收口「没会话/没绑定就走 default」的镜像注释(r4)
唯一 finding:audio.go 的 speechRequest.SessionID 注释原本写「省略它时本口没有会话
概念 ⇒ 走 emotionTTS 的 default 分支」——**反了**。真实链路是
effectivePersona(ctx,"") → sessionPersona("") 为空**不是终点** → Effective =
defaultPersonaID() → 常规安装必有全局默认人格 ⇒ personaId 非空 ⇒ 走 personas 映射。
照那句话改的人会写出 `if req.SessionID == "" { personaID = "" }`,正是 r2 修掉的病。
改为:省略时按**全局默认人格**选发音人(会话绑定 → 全局默认人格 → 两者都没有才落到
default 字符),并显式写了"不要写成 if SessionID == \"\" { personaID = \"\" }"。

顺手同根三处(不留第四轮):
- tts/config.go 的 Config.Default 注释:成因表「会话无绑定 / 读取失败 / 该出口本身
  没有会话」已过时 ⇒ 改为「空串的唯一合法成因是连全局默认人格都没有」,并写明不是
  "会话未绑定"、也不是"出站口没有会话"。
- runtimeconfig/voicetts.go 的 Default 字段注释与文件头同族措辞同步;
- selection.go 的 WithPersonaID / Resolve 分支表、service.go 的分支小标把
  「解析不到」统一改成「连全局默认人格都没有」;
- voice_emotiontts_test.go 的 TestSpeechEndpointNoSessionUsesDefault 注释同步(该用例
  没接人格库,所以确实落到 default —— 但成因写对了);
- wire_voice_tts_test.go nit:改成「已废弃的全局 `character` 不生效」(墓碑键仍在)。

纯注释:`git diff` 里非注释改动行为空。go build/vet 通过;四包 go test -count=1 全绿。
2026-10-26 13:40:00 +08:00

128 lines
4.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// 本文件锁定切片 M35 在**组合根**上的三条口径(都是"改坏就静默不工作/静默开错"的那种):
//
// 1. 面板文件缺失 ⇒ **这一级不启用**,返回的仍是既有链本身(行为逐字不变);
// 2. 文件里 `enabled:true` ⇒ 返回 emotionTTS 门面,`personas`/`default`/`timeoutSec`
// 按文件来(发音人只能来自 persona 映射;已废弃的全局 `character` 不生效);
// 3. 文件坏掉 ⇒ **不启用**(宁可没有这一级,也不拿读不懂的配置去驱动外部服务)。
//
// 它用 `DefaultVoiceTTSPath` 的真实落点($DATA_DIR/config/voice-tts.json)跑,
// 因此也钉住"零新 .env 键":没有任何一处读环境变量。
import (
"os"
"path/filepath"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
mvoice "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/multimodal/voice"
voicetts "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/voice/tts"
)
// voiceTTSApp 造一个只够本题用的 app:数据目录 + 一条既有链。
func voiceTTSApp(t *testing.T) *app {
t.Helper()
dir := t.TempDir()
return &app{
cfg: &config.Config{DataDir: dir},
log: tidyTestLog(),
voice: mvoice.New(config.VoiceConfig{}),
}
}
// TestVoiceForGateway_DisabledByDefault 钉住"没配过 ⇒ 原样用既有链"。
func TestVoiceForGateway_DisabledByDefault(t *testing.T) {
a := voiceTTSApp(t)
got := a.voiceForGateway()
if got != any(a.voice) {
t.Fatalf("没配过时不该包装既有链: %T", got)
}
}
// TestVoiceForGateway_WrapsWhenEnabled 钉住"文件启用 ⇒ 门面生效 + 配置逐项读对"。
//
// 🔴 它同时钉住主人 2026-10-26 那条硬要求在**组合根**上的形态:配置里读出来的是
// `personas` 映射与 `default`,**没有**任何"全局一个 character"。
func TestVoiceForGateway_WrapsWhenEnabled(t *testing.T) {
a := voiceTTSApp(t)
writeVoiceTTSPanel(t, a, `{"enabled":true,"baseUrl":"http://127.0.0.1:9880",
"personas":{"cyrene":"char_84fb88a3","furina":"char_5314489e"},
"default":"char_84fb88a3","format":"wav","timeoutSec":45}`)
cfg, enabled := a.voiceTTSConfig()
if !enabled {
t.Fatal("面板写了 enabled:true,但组合根报未启用")
}
if len(cfg.Personas) != 2 || cfg.Personas["furina"] != "char_5314489e" {
t.Fatalf("personas 没有逐项读出: %+v", cfg.Personas)
}
if cfg.Default != "char_84fb88a3" {
t.Fatalf("default = %q, want char_84fb88a3", cfg.Default)
}
got := a.voiceForGateway()
if _, ok := got.(*voicetts.Service); !ok {
t.Fatalf("启用后应返回 *tts.Service, got %T", got)
}
}
// TestVoiceForGateway_LegacyCharacterIgnored 钉住已废弃的全局 character **不生效**:
// 只写旧键时,映射与 default 都是空的 ⇒ 任何请求都选不到发音人(不会悄悄回退到
// 那个全局角色,因为那正是主人担心的病害)。
func TestVoiceForGateway_LegacyCharacterIgnored(t *testing.T) {
a := voiceTTSApp(t)
writeVoiceTTSPanel(t, a, `{"enabled":true,"character":"char_84fb88a3"}`)
cfg, enabled := a.voiceTTSConfig()
if !enabled {
t.Fatal("enabled:true 应让这一级启用")
}
if len(cfg.Personas) != 0 || cfg.Default != "" {
t.Fatalf("废弃的全局 character 被当成了发音人: personas=%v default=%q", cfg.Personas, cfg.Default)
}
}
// TestVoiceForGateway_BadFileStaysDisabled 钉住"坏文件 ⇒ 不启用"(绝不半开)。
func TestVoiceForGateway_BadFileStaysDisabled(t *testing.T) {
a := voiceTTSApp(t)
writeVoiceTTSPanel(t, a, `{"enabled":`)
got := a.voiceForGateway()
if _, ok := got.(*voicetts.Service); ok {
t.Fatal("坏文件却启用了 emotionTTS(会拿读不懂的配置去调外部服务)")
}
if got != any(a.voice) {
t.Fatalf("坏文件时应原样用既有链, got %T", got)
}
}
// TestVoiceTTSConfig_NoEnvKeys 钉住"零新 .env 键":把相关环境变量设成垃圾值,
// 配置解析的结果只由面板文件决定(证明它根本没读环境)。
func TestVoiceTTSConfig_NoEnvKeys(t *testing.T) {
a := voiceTTSApp(t)
t.Setenv("VOICE_TTS_PROVIDER", "emotiontts")
t.Setenv("EMOTIONTTS_BASE_URL", "http://evil.invalid")
writeVoiceTTSPanel(t, a, `{"enabled":true}`)
cfg, enabled := a.voiceTTSConfig()
if !enabled {
t.Fatal("面板启用却没启用")
}
if cfg.BaseURL != "" {
t.Errorf("baseURL = %q,want 空(默认值由 Normalized 补,不读环境)", cfg.BaseURL)
}
}
// writeVoiceTTSPanel 把内容写进**真实落点**($DATA_DIR/config/voice-tts.json)。
func writeVoiceTTSPanel(t *testing.T, a *app, body string) {
t.Helper()
path := filepath.Join(a.cfg.DataDir, "config", "voice-tts.json")
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("write panel: %v", err)
}
}