主人 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 包
85 lines
3.3 KiB
Go
85 lines
3.3 KiB
Go
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)
|
||
}
|
||
}
|