上一版把 r1 的三条 finding 静默漏了(我在交付前没读 inbox),本提交逐条补齐:
1. 🔴 隐私:upstreamStatus 不再读也不记响应体
FastAPI 的 422 会把请求体里的 input 整段回显,原来「错误体前 512 字节进 WARN」
等于把她的原话写进日志。现在只记 status / content_type / response_bytes
(字节数优先取 Content-Length,缺失时有界排空取上界,读到即丢)。
新用例 TestSpeechRejectLogOmitsRequestText 用**回显正文**的假上游抓它;
变异(把 body 塞回 WARN)实测打红。
2. format 键彻底删除(假开关)
上游只拿 response_format 改自报 media_type,产物恒 .wav 无转码,我方只按字节
嗅探裁决 ⇒ wav/mp3/ogg 交付字节与响应头完全相同。删 tts.Config.Format 与
VoiceTTSTuning.Format,encodeSpeech 硬编码 requestFormat="wav"(不可配置)。
用例钉住请求体恒为 wav;变异(改成 mp3)打红。旧文件里残留的 format 键被忽略。
3. Stats 接进 degrade 的 WARN(attempts/failures)
否则统计在生产日志里没有消费者。TestServiceDegradeLogsCounters 钉住;
变异(去掉两个字段)打红。
4. redactURL 兜底含 "@" 整体打码
解析失败/无 host 时原来原样返回,user:pass@ 会漏凭据。TestRedactURLCredentials
覆盖合法 URL 去 userinfo、非法含 @ 打码;变异(退回原样返回)打红。
另:关闭态零行为变化的**当前 tip 重跑**——
- handleSpeech 的 persona 解析改为只在语音服务实现 personaVoice 且 PrimaryActive
时才查库;未接 emotionTTS(既有链/stub)时零额外 I/O、ctx 不改、音频字节相同。
用例 TestSpeechEndpointDisabledVoiceDoesNotReadPersona(含对照组证明闸不是恒关);
变异(去掉闸)打红。
- voice.go 在 dev 基线里本就有 env.PersonaID 解析(无人格解析新增 I/O),本次未改。
Config.Normalized 去掉未使用的 logger 参数。go build/vet/test(含 -race)全绿;
真机 live 复测:cyrene 264104 B、furina 221194 B。
108 lines
4.2 KiB
Go
108 lines
4.2 KiB
Go
package runtimeconfig
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// TestLoadVoiceTTSTuning 锁定 voice-tts.json 的四条读取口径:
|
|
// 缺失 = 没配过(不是错误)、全空白 = 没配过、合法 = 逐键读出、坏文件 = 可分类错误。
|
|
func TestLoadVoiceTTSTuning(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "voice-tts.json")
|
|
|
|
// ① 缺失
|
|
if tuning, err := LoadVoiceTTSTuning(path); err != nil || tuning.EnabledOn() {
|
|
t.Fatalf("缺失: tuning=%+v err=%v, want 零值 + nil", tuning, err)
|
|
}
|
|
|
|
// ② 全空白
|
|
if err := os.WriteFile(path, []byte(" \n\t "), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if tuning, err := LoadVoiceTTSTuning(path); err != nil || tuning.EnabledOn() {
|
|
t.Fatalf("空白: tuning=%+v err=%v, want 零值 + nil", tuning, err)
|
|
}
|
|
|
|
// ③ 合法(新形状:personas 映射 + default;旧的全局 character 不再参与)
|
|
body := `{"enabled":true,"baseUrl":"http://127.0.0.1:9880",
|
|
"personas":{"cyrene":"char_84fb88a3","furina":"char_5314489e"},
|
|
"default":"char_84fb88a3","timeoutSec":45}`
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
tuning, err := LoadVoiceTTSTuning(path)
|
|
if err != nil {
|
|
t.Fatalf("合法文件: %v", err)
|
|
}
|
|
if !tuning.EnabledOn() || tuning.TimeoutSec != 45 {
|
|
t.Fatalf("读取结果不对: %+v", tuning)
|
|
}
|
|
if len(tuning.Personas) != 2 || tuning.Personas["furina"] != "char_5314489e" {
|
|
t.Fatalf("personas 没有被读出: %+v", tuning.Personas)
|
|
}
|
|
if tuning.Default != "char_84fb88a3" {
|
|
t.Fatalf("default = %q", tuning.Default)
|
|
}
|
|
|
|
// ③c 旧文件里若还留着 `format`,它**不再有任何效果**(结构体里没有这个键,
|
|
// json 当作未知字段忽略):加载仍成功,且交付格式只由字节嗅探决定。
|
|
if err := os.WriteFile(path, []byte(`{"enabled":true,"format":"mp3","personas":{"cyrene":"char_84fb88a3"}}`), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if tuning, err = LoadVoiceTTSTuning(path); err != nil {
|
|
t.Fatalf("带废弃 format 键的文件不该解不开: %v", err)
|
|
}
|
|
if !tuning.EnabledOn() || tuning.Personas["cyrene"] != "char_84fb88a3" {
|
|
t.Fatalf("废弃键影响了其余字段: %+v", tuning)
|
|
}
|
|
|
|
// ③b 旧的全局 character 仍能读出来(供启动日志提示"已废弃"),但它**不是**
|
|
// personas/default 的一部分 —— 组合根绝不把它当发音人。
|
|
if err := os.WriteFile(path, []byte(`{"enabled":true,"character":"char_old"}`), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if tuning, err = LoadVoiceTTSTuning(path); err != nil {
|
|
t.Fatalf("旧键: %v", err)
|
|
}
|
|
if tuning.LegacyGlobalCharacter() != "char_old" {
|
|
t.Fatalf("LegacyGlobalCharacter = %q", tuning.LegacyGlobalCharacter())
|
|
}
|
|
if len(tuning.Personas) != 0 || tuning.Default != "" {
|
|
t.Fatalf("旧全局 character 混进了 personas/default: %+v", tuning)
|
|
}
|
|
|
|
// ④ 显式 false 与"没配"同效,但指针让它在读取侧可区分。
|
|
if err := os.WriteFile(path, []byte(`{"enabled":false}`), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if tuning, err = LoadVoiceTTSTuning(path); err != nil || tuning.EnabledOn() || tuning.Enabled == nil {
|
|
t.Fatalf("显式 false: tuning=%+v err=%v", tuning, err)
|
|
}
|
|
|
|
// ⑤ 坏文件 ⇒ 可分类错误(调用方据此回落"不启用",且绝不覆盖原文件)
|
|
if err := os.WriteFile(path, []byte(`{"enabled":`), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if _, err := LoadVoiceTTSTuning(path); !errors.Is(err, ErrInvalidVoiceTTS) {
|
|
t.Fatalf("坏文件 err = %v, want ErrInvalidVoiceTTS", err)
|
|
}
|
|
if raw, err := os.ReadFile(path); err != nil || string(raw) != `{"enabled":` {
|
|
t.Fatalf("坏文件被覆盖了: %q err=%v", string(raw), err)
|
|
}
|
|
}
|
|
|
|
// TestDefaultVoiceTTSPath 钉住落点:`$DATA_DIR/config/voice-tts.json`(零新 .env 键)。
|
|
func TestDefaultVoiceTTSPath(t *testing.T) {
|
|
got := DefaultVoiceTTSPath(filepath.Join("data", "root"))
|
|
want := filepath.Join("data", "root", "config", "voice-tts.json")
|
|
if got != want {
|
|
t.Fatalf("path = %q, want %q", got, want)
|
|
}
|
|
if VoiceTTSConfigRelPath != "config/voice-tts.json" {
|
|
t.Fatalf("RelPath = %q", VoiceTTSConfigRelPath)
|
|
}
|
|
}
|