Files
AskaEth 6906d274fc feat(voice): M47 后端启动后探活 emotionTTS,未起则异步拉起一键启动脚本
主人 2026-10-24:「tts启动脚本在这 ...\一键启动.bat,以后启动后端可以顺便检测
一下如果TTS没起就顺带启一下」。本片只做「启动时顺带探一下」,**默认关**(主人
早前说过「开机自启先不急 后面做」),未开开关时连探活都不发。

- 新包 internal/runtime/voice/ttslaunch:探 http://127.0.0.1:9880(毫秒级超时),
  已在跑什么都不做;没起则异步起脚本(cmd.exe /c 传参,不拼命令行字符串)
- 防重复:in-flight 单飞 + 一次尝试后的冷却(失败也算一次,重启风暴不会变成重试风暴)
- 失败只 WARN:探活超时/脚本不存在/起进程失败都只记带 source 的日志,
  组合根那条路径**没有 error 通道**
- 配置走面板文件 $DATA_DIR/config/voice-tts-launch.json(零新 .env 键)
- 接线在 cmd/cyrene 的网关 Listen 之后,整段在后台 goroutine 里,不占启动时长
  (用例以「挂载耗时 < 150ms 而探活超时 800ms」证明)
- 不新开端点、不加帧族;不碰 gateway 包
2026-10-26 18:47:36 +08:00

85 lines
3.3 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 runtimeconfig
import (
"errors"
"os"
"path/filepath"
"testing"
)
// TestLoadVoiceTTSLaunchTuning 锁定 voice-tts-launch.json 的四条读取口径:
// 缺失 = 没配过(不是错误、**不启用**)、全空白 = 没配过、合法 = 逐键读出、
// 坏文件 = 可分类错误(调用方据此回落"不启用")。
func TestLoadVoiceTTSLaunchTuning(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "voice-tts-launch.json")
// ① 缺失 ⇒ 零值 + nil(默认关:这把开关会起进程,不能靠"猜"打开)
if tuning, err := LoadVoiceTTSLaunchTuning(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 := LoadVoiceTTSLaunchTuning(path); err != nil || tuning.EnabledOn() {
t.Fatalf("空白: tuning=%+v err=%v, want 零值 + nil", tuning, err)
}
// ③ 合法 ⇒ 逐键读出(脚本路径含中文,正是主人那台机器的形状)
body := `{"enabled":true,"scriptPath":"D:\\Project\\Service\\emotionTTS_v5.1.0\\一键启动.bat",
"probeUrl":"http://127.0.0.1:9880","probeTimeoutMs":250,"cooldownSec":90}`
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
tuning, err := LoadVoiceTTSLaunchTuning(path)
if err != nil {
t.Fatalf("合法文件: %v", err)
}
if !tuning.EnabledOn() {
t.Fatal("enabled:true 没被读出")
}
if tuning.ProbeTimeoutMs != 250 || tuning.CooldownSec != 90 {
t.Fatalf("超时/冷却没读出: %+v", tuning)
}
if tuning.ProbeURL != "http://127.0.0.1:9880" {
t.Fatalf("probeUrl = %q", tuning.ProbeURL)
}
if filepath.Base(tuning.ScriptPath) != "一键启动.bat" {
t.Fatalf("scriptPath 没读出: %q", tuning.ScriptPath)
}
// ④ 显式 false ⇒ 与"没配过"同效(EnabledOn 为 false)
if err := os.WriteFile(path, []byte(`{"enabled":false}`), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
if tuning, err = LoadVoiceTTSLaunchTuning(path); err != nil || tuning.EnabledOn() {
t.Fatalf("显式 false: tuning=%+v err=%v, want 不启用", tuning, err)
}
// ⑤ 坏文件 ⇒ 分类错误(调用方据此 WARN + 不启用),且绝不覆盖原文件
if err := os.WriteFile(path, []byte(`{"enabled":`), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
if _, err = LoadVoiceTTSLaunchTuning(path); !errors.Is(err, ErrInvalidVoiceTTSLaunch) {
t.Fatalf("坏文件: err=%v, want ErrInvalidVoiceTTSLaunch", err)
}
if raw, rerr := os.ReadFile(path); rerr != nil || string(raw) != `{"enabled":` {
t.Fatalf("坏文件被改写了: %q err=%v", raw, rerr)
}
}
// TestDefaultVoiceTTSLaunchPath 钉住落点:`$DATA_DIR/config/voice-tts-launch.json`
// —— 它是"零新 .env 键"这一条的物理形态(开关只可能来自面板文件)。
func TestDefaultVoiceTTSLaunchPath(t *testing.T) {
got := DefaultVoiceTTSLaunchPath("/data")
want := filepath.Join("/data", "config", "voice-tts-launch.json")
if got != want {
t.Fatalf("落点 = %q, want %q", got, want)
}
if VoiceTTSLaunchConfigRelPath != "config/voice-tts-launch.json" {
t.Fatalf("相对路径 = %q", VoiceTTSLaunchConfigRelPath)
}
}