复审 p2-2items(P2-1 跨进程去重按裁决只报不修):
(a) **关闭期 ctx 取消 ≠ 没起**:后端关闭时 ctx 被取消,探活返回
`context.Canceled`;原实现把它判成"没起" ⇒ **在关闭过程中拉起 TTS**。
现在按 `errors.Is(err, context.Canceled)` 判为跳过:不记 markAttempt(不消耗
冷却)、不拉、也不 WARN 成故障。探活**自身超时**(DeadlineExceeded)仍是"没起",
照常拉 —— 两者用 errors.Is 区分,不按文案。新增 OutcomeCanceled。
(b) **探活地址必须合法**:写成 `127.0.0.1:9880`(漏 scheme)会让 http.NewRequest
每次都失败 ⇒「没起」永远成立 ⇒ 每次启动都拉一次进程。新增
`ttslaunch.ValidateProbeURL`(url.Parse + scheme ∈ {http,https} + host 非空):
- `New` 里校验 ⇒ 不合法即 WARN + 不启用(兜底闸,覆盖任何调用方);
- 组合根 `validateResolvedProbeURL` 里同一把闸再校验一次,WARN 带
`probe_url_source` 指明该改哪个文件 —— **baseUrl 兜底那条路也被覆盖**。
顺带按复审记录项 1 修正 `Proxy: nil` 的注释:net/http 本来就不给回环 IP 走代理,
这行真正保护的是 probeUrl 被配成主机名的情况(原注释高估了它)。
用例(均变异验证打红):
- TestCheck_ParentCanceled_SkipsLaunch:取消 ⇒ OutcomeCanceled + 0 次拉起 + 不进冷却
(变异:跳过改回"没起" ⇒ Outcome=launched、marker 落地)
- TestCheck_ProbeOwnTimeout_StillLaunches:探活自身超时仍照常拉
- TestNew_InvalidProbeURL_Disables:7 种非法形状 ⇒ 不接单、0 次拉起、有 WARN
- TestStartVoiceTTSLaunch_{ShutdownCancelDoesNotLaunch,InvalidProbeURLStaysOff,
InvalidBaseURLFallbackStaysOff}:组合根三条,均用真 .bat marker 观察"有没有拉起"
151 lines
5.8 KiB
Go
151 lines
5.8 KiB
Go
package main
|
|
|
|
// 本文件锁定复审 P2-2 在**组合根**上的两道闸:
|
|
//
|
|
// 1. **关闭期取消 ⇒ 0 次拉起**(P2-2a):后端关闭时 ctx 被取消,探活随之返回
|
|
// `context.Canceled`,这必须是"跳过",绝不能在关闭过程中拉起 TTS;
|
|
// 2. **探活地址非法 ⇒ 不启用、一次都不拉**(P2-2b):`probeUrl` 与 `voice-tts.json`
|
|
// 的 `baseUrl` 兜底**两个来源**都要被挡(漏一个就等于绕过闸门,变成"每次启动
|
|
// 都拉一次进程")。
|
|
//
|
|
// 用例除了看返回值,还会真等一会儿确认没有进程被拉起(拉起是异步的,脚本一跑
|
|
// 就会落 marker 文件);探活用黑洞监听(只接连接不回话),保证取消一定落在探活
|
|
// 进行中。
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/voice/ttslaunch"
|
|
)
|
|
|
|
// TestStartVoiceTTSLaunch_ShutdownCancelDoesNotLaunch 钉住"关闭期取消 ⇒ 0 次拉起"。
|
|
//
|
|
// 变异点:去掉 ttslaunch 里 `errors.Is(probeErr, context.Canceled)` 那个跳过分支 ⇒
|
|
// 取消被当成"没起" ⇒ 脚本被拉起(marker 出现),本用例红。
|
|
func TestStartVoiceTTSLaunch_ShutdownCancelDoesNotLaunch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
blackhole, addr := blackholeListener(t)
|
|
defer blackhole.Close()
|
|
|
|
a, buf := launchTestApp(t, dir)
|
|
script, marker := markerBat(t, dir)
|
|
writeLaunchPanel(t, dir, fmt.Sprintf(
|
|
`{"enabled":true,"probeUrl":%q,"probeTimeoutMs":5000,"scriptPath":%q}`,
|
|
"http://"+addr+"/", script))
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
launcher := a.startVoiceTTSLaunch(ctx)
|
|
if launcher == nil {
|
|
t.Fatal("启用后应拿到 launcher")
|
|
}
|
|
// 模拟后端关闭:探活还挂在黑洞上时取消。
|
|
cancel()
|
|
launcher.Wait()
|
|
|
|
assertNotLaunched(t, marker)
|
|
if out := buf.String(); strings.Contains(out, "emotionTTS launch failed") {
|
|
t.Fatalf("关闭期取消被记成了拉起失败: %s", out)
|
|
}
|
|
}
|
|
|
|
// TestStartVoiceTTSLaunch_InvalidProbeURLStaysOff 钉住第一道闸:launch 文件里写了
|
|
// 非法 `probeUrl` ⇒ 返回 nil(不构造 launcher)、WARN 标出源、**一次都不拉**。
|
|
//
|
|
// 变异点:去掉 `validateResolvedProbeURL` ⇒ 返回非 nil,且探活每轮必败 ⇒ 脚本被
|
|
// 拉起(marker 出现),两条断言都会红。
|
|
func TestStartVoiceTTSLaunch_InvalidProbeURLStaysOff(t *testing.T) {
|
|
dir := t.TempDir()
|
|
a, buf := launchTestApp(t, dir)
|
|
script, marker := markerBat(t, dir)
|
|
writeLaunchPanel(t, dir, fmt.Sprintf(`{"enabled":true,"probeUrl":"127.0.0.1:9880","scriptPath":%q}`, script))
|
|
|
|
launcher := a.startVoiceTTSLaunch(context.Background())
|
|
if launcher != nil {
|
|
t.Fatal("非法 probeUrl 仍启用了自启(会每次启动都拉一次进程)")
|
|
}
|
|
assertNotLaunched(t, marker)
|
|
out := buf.String()
|
|
if !strings.Contains(out, "invalid probe url") {
|
|
t.Fatalf("非法 probeUrl 没记 WARN: %s", out)
|
|
}
|
|
if !strings.Contains(out, "probe_url_source=voice-tts-launch.json") {
|
|
t.Fatalf("WARN 没标出该改哪个文件: %s", out)
|
|
}
|
|
}
|
|
|
|
// TestStartVoiceTTSLaunch_InvalidBaseURLFallbackStaysOff 钉住第二道闸:launch 文件
|
|
// 没写 probeUrl、而 `voice-tts.json` 的 baseUrl 非法 ⇒ 同样不启用、一次都不拉。
|
|
func TestStartVoiceTTSLaunch_InvalidBaseURLFallbackStaysOff(t *testing.T) {
|
|
dir := t.TempDir()
|
|
a, buf := launchTestApp(t, dir)
|
|
script, marker := markerBat(t, dir)
|
|
writeVoiceTTSPanel(t, a, `{"enabled":true,"baseUrl":"127.0.0.1:9880"}`)
|
|
writeLaunchPanel(t, dir, fmt.Sprintf(`{"enabled":true,"scriptPath":%q}`, script))
|
|
|
|
launcher := a.startVoiceTTSLaunch(context.Background())
|
|
if launcher != nil {
|
|
t.Fatal("baseUrl 兜底来的非法地址也没被挡住(每次启动都会拉一次进程)")
|
|
}
|
|
assertNotLaunched(t, marker)
|
|
out := buf.String()
|
|
if !strings.Contains(out, "invalid probe url") {
|
|
t.Fatalf("非法 baseUrl 没记 WARN: %s", out)
|
|
}
|
|
if !strings.Contains(out, "probe_url_source=voice-tts.json") {
|
|
t.Fatalf("WARN 没标出该改哪个文件: %s", out)
|
|
}
|
|
}
|
|
|
|
// TestValidateResolvedProbeURL_EmptyMeansDefault 钉住"空串不在这里判死":两个文件
|
|
// 都没给地址时交给 Normalized 补 9880(默认值本身合法)。
|
|
func TestValidateResolvedProbeURL_EmptyMeansDefault(t *testing.T) {
|
|
if err := validateResolvedProbeURL(probeResolution{url: "", probeSource: probeSourceDefault}); err != nil {
|
|
t.Fatalf("空地址应交给默认值: %v", err)
|
|
}
|
|
if err := validateResolvedProbeURL(probeResolution{url: ttslaunch.DefaultProbeURL}); err != nil {
|
|
t.Fatalf("默认地址必须合法: %v", err)
|
|
}
|
|
if err := validateResolvedProbeURL(probeResolution{url: "localhost:9880"}); err == nil {
|
|
t.Fatal("漏 scheme 的地址被放过了")
|
|
}
|
|
}
|
|
|
|
// markerBat 造一个"跑起来就落 marker"的 .bat,用于观察"到底有没有进程被拉起"。
|
|
// 非 Windows 返回空串(那台机器上这个脚本本来也拉不起来),调用方据此跳过观察。
|
|
func markerBat(t *testing.T, dir string) (script, marker string) {
|
|
t.Helper()
|
|
if runtime.GOOS != "windows" {
|
|
return "", ""
|
|
}
|
|
script = filepath.Join(dir, "一键启动.bat")
|
|
marker = filepath.Join(dir, "launched.txt")
|
|
body := fmt.Sprintf("@echo off\r\necho launched> \"%s\"\r\n", marker)
|
|
if err := os.WriteFile(script, []byte(body), 0o600); err != nil {
|
|
t.Fatalf("write script: %v", err)
|
|
}
|
|
return script, marker
|
|
}
|
|
|
|
// assertNotLaunched 等一小会儿确认脚本**没有**被拉起(拉起是异步的,所以不能只看
|
|
// 返回值;空 marker = 非 Windows,跳过观察)。
|
|
func assertNotLaunched(t *testing.T, marker string) {
|
|
t.Helper()
|
|
if marker == "" {
|
|
return
|
|
}
|
|
deadline := time.Now().Add(600 * time.Millisecond)
|
|
for time.Now().Before(deadline) {
|
|
if _, err := os.Stat(marker); err == nil {
|
|
t.Fatalf("脚本被拉起了: %s", marker)
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
}
|