Resample ASR audio to task sample rate

This commit is contained in:
2026-10-12 13:42:49 +08:00
parent 2a5d4f2365
commit 969e9639ec
17 changed files with 239 additions and 34 deletions
+8 -7
View File
@@ -1,13 +1,14 @@
# 当前待办
## ASR 对接边界音频转换(未实现)
## ASR 对接边界音频转换(本地已实现,待真实验收)
- [ ] 将内部媒体格式与 ASR 输入要求分离;内部可继续使用统一格式,不要求服务商支持内部固定的 16000 采样率。
- [ ] 使用获批任务现有参数确定 ASR 目标输入格式,在发送边界转换音频;实际格式已经匹配时不转换。请求中的采样率必须与实际发送音频一致。
- [ ] 不忽略或覆盖任务参数,不仅修改采样率声明,不补 SDK 默认值,不因失败换采样率或换服务商。输入要求不能明确、目标不支持或转换失败时明确报错,不发送格式不匹配的音频。
- [ ] 实现前核查现有音频依赖的转换能力;按 TDD 补充至少 16000→8000、8000→16000、同采样率直通、流式分块与失败测试,核验实际音频而非只检查参数。
- [ ] 真实供应商兼容性须在另获授权后验证;本地结构校验、模拟测试不代替真实签收。
- [x] 内部媒体继续使用单声道 16 kHz PCM16;绑定时读取并校验任务现有 `sample_rate`,不补默认值。
- [x] 在 ASR 发送边界按目标采样率转换实际音频;请求参数原样保留,发送的采样率与音频一致,同采样率时不重采样。
- [x] 缺失、非整数、超出 8–48 kHz 范围或转换后没有完整 PCM16 样本时明确失败;不改写参数、不换率、不换服务商。
- [x] 复用已有 PCM16 转换能力;TDD 覆盖 16→8 kHz ASR、8↔16 kHz 媒体转换、同率直通、分块收发和错误输入。
- [ ] 在获批的隔离 ASR-only 与 full-AI 任务上完成真实 ASR 验证;本地测试不能代替供应商和通话验收。
- [ ] 修正隔离 full-AI 测试任务的 TTS 输出格式与 Agent PCM16 要求不匹配问题,再验证 TTS、LLM、ASR 全链路。
唯一规则来源是共享合同的 [业务规范](../contracts/schema/saas-dispatcher.md) 与 [任务 Schema](../contracts/schema/http-task-detail.schema.json),这里仅记录未完成工作,不维护第二份字段定义。
**当前边界:** Agent 仍使用单声道 16 kHz PCM16,尚未实现上述 ASR 转换。Schema 允许 8000、16000 等参数,只表示配置结构有效,不表示当前程序或任意供应商已经支持。此次仅更新待办、合同、示例及测试,不改变运行行为、不部署、不拨号、不调用 AI。
**当前边界:** 实现位于本地分支 `fix/asr-sample-rate-20261012`,尚未部署。Dispatcher 当前只有 full-AI 任务,没有 ASR-only 测试任务。测试管理页通过 NatApp 隧道访问失败(目标 `192.168.0.3:8888` 不可用),因此尚未创建测试任务,也未对 `15003164745` 或 `15830461047` 发起新呼叫。
+47
View File
@@ -0,0 +1,47 @@
package ai
import (
"encoding/json"
"errors"
"math"
"git.ipao.vip/rogee/go-sip/internal/audio"
)
const (
internalPCM16SampleRate = 16000
minASRSampleRate = 8000
maxASRSampleRate = 48000
)
func parseASRSampleRate(params map[string]json.RawMessage) (int, error) {
raw, ok := params["sample_rate"]
if !ok {
return 0, errors.New("ASR params.sample_rate is required")
}
var rate float64
if err := json.Unmarshal(raw, &rate); err != nil {
return 0, errors.New("ASR params.sample_rate must be numeric")
}
if math.IsNaN(rate) || math.IsInf(rate, 0) || math.Trunc(rate) != rate || rate < minASRSampleRate || rate > maxASRSampleRate {
return 0, errors.New("ASR params.sample_rate must be an integer from 8000 to 48000 Hz")
}
return int(rate), nil
}
func prepareASRPCM16(pcm16 []byte, targetRate int) ([]byte, error) {
if len(pcm16) == 0 || len(pcm16)%2 != 0 {
return nil, errors.New("ASR input must contain nonempty signed PCM16 samples")
}
if targetRate < minASRSampleRate || targetRate > maxASRSampleRate {
return nil, errors.New("ASR target sample rate is outside the supported 8000–48000 Hz range")
}
// The media layer uses the same resampler when converting the supported
// 8-kHz PCMA wire audio to the agent's 16-kHz PCM16 format.
resampled := audio.ResamplePCM16(pcm16, internalPCM16SampleRate, targetRate)
if len(resampled) == 0 || len(resampled)%2 != 0 {
return nil, errors.New("ASR resampling produced no complete PCM16 samples")
}
return resampled, nil
}
+112
View File
@@ -0,0 +1,112 @@
package ai
import (
"bytes"
"strconv"
"testing"
)
func TestBindStoresConfiguredASRSampleRate(t *testing.T) {
for _, rate := range []int{8000, 16000, 24000, 48000} {
t.Run(strconv.Itoa(rate), func(t *testing.T) {
task, providers := currentFixture(t, "asr_only")
task = changeCurrentAgent(t, task, func(agent map[string]any) {
asr := agent["asr"].(map[string]any)
params := asr["params"].(map[string]any)
params["sample_rate"] = rate
})
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
if bound.ASR.SampleRate != rate {
t.Fatalf("sample rate = %d, want %d", bound.ASR.SampleRate, rate)
}
})
}
}
func TestBindRejectsMissingASRSampleRate(t *testing.T) {
task, providers := currentFixture(t, "asr_only")
task = changeCurrentAgent(t, task, func(agent map[string]any) {
asr := agent["asr"].(map[string]any)
params := asr["params"].(map[string]any)
delete(params, "sample_rate")
})
if _, err := Bind(task, providers); err == nil {
t.Fatal("Bind accepted ASR parameters without sample_rate")
}
}
func TestBindRejectsInvalidASRSampleRates(t *testing.T) {
for name, rate := range map[string]any{
"null": nil,
"string": "8000",
"fractional": 8000.5,
"boolean": true,
"zero": 0,
"below-min": 7999,
"above-max": 48001,
} {
t.Run(name, func(t *testing.T) {
task, providers := currentFixture(t, "asr_only")
task = changeCurrentAgent(t, task, func(agent map[string]any) {
asr := agent["asr"].(map[string]any)
params := asr["params"].(map[string]any)
params["sample_rate"] = rate
})
if _, err := Bind(task, providers); err == nil {
t.Fatalf("Bind accepted invalid sample_rate %v", rate)
}
})
}
}
func TestPrepareASRPCM16ResamplesToConfiguredRate(t *testing.T) {
input := []byte{1, 0, 2, 0, 3, 0, 4, 0}
got, err := prepareASRPCM16(input, 8000)
if err != nil {
t.Fatal(err)
}
if want := []byte{1, 0, 3, 0}; !bytes.Equal(got, want) {
t.Fatalf("resampled PCM16 = %v, want %v", got, want)
}
if want := []byte{1, 0, 2, 0, 3, 0, 4, 0}; !bytes.Equal(input, want) {
t.Fatalf("input PCM16 was modified: %v", input)
}
}
func TestPrepareASRPCM16CopiesAtInternalRate(t *testing.T) {
input := []byte{1, 0, 2, 0}
got, err := prepareASRPCM16(input, internalPCM16SampleRate)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, input) {
t.Fatalf("PCM16 = %v, want %v", got, input)
}
got[0] = 9
if input[0] != 1 {
t.Fatal("prepared ASR audio aliases the caller's PCM buffer")
}
}
func TestPrepareASRPCM16RejectsInvalidAudioOrRate(t *testing.T) {
for name, test := range map[string]struct {
pcm []byte
rate int
}{
"empty": {nil, 8000},
"odd-length": {[]byte{1}, 8000},
"too-short-after-downsample": {[]byte{1, 0}, 8000},
"below-min-rate": {[]byte{1, 0, 2, 0}, 7999},
"above-max-rate": {[]byte{1, 0, 2, 0}, 48001},
} {
t.Run(name, func(t *testing.T) {
if _, err := prepareASRPCM16(test.pcm, test.rate); err == nil {
t.Fatal("expected conversion to fail")
}
})
}
}
+3 -3
View File
@@ -42,7 +42,7 @@ func TestASRParametersReachDoubaoSDKWebSocket(t *testing.T) {
defer server.Close()
task, providers := currentFixture(t, tc.mode)
task = changeCurrentAgent(t, task, func(a map[string]any) {
a["asr"].(map[string]any)["params"] = map[string]any{"reqid": "ignored-params-reqid", "sequence": 99, "model_name": "ignored-params-model", "result_type": tc.resultType, "enable_nonstream": tc.nonstream, "unknown_flag": false, "nullable": nil, "opaque": "vendor-only", "large_number": json.Number("9007199254740993")}
a["asr"].(map[string]any)["params"] = map[string]any{"reqid": "ignored-params-reqid", "sequence": 99, "model_name": "ignored-params-model", "result_type": tc.resultType, "enable_nonstream": tc.nonstream, "sample_rate": 8000, "unknown_flag": false, "nullable": nil, "opaque": "vendor-only", "large_number": json.Number("9007199254740993")}
})
asrProvider := providers["volcengine"]
asrProvider.WSEndpoint = "ws" + strings.TrimPrefix(server.URL, "http")
@@ -53,7 +53,7 @@ func TestASRParametersReachDoubaoSDKWebSocket(t *testing.T) {
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if _, err := bound.Recognize(ctx, []byte{1, 0}); err == nil {
if _, err := bound.Recognize(ctx, []byte{1, 0, 2, 0}); err == nil {
t.Fatal("disconnected mock ASR cannot pretend recognition completed")
}
select {
@@ -82,7 +82,7 @@ func TestASRParametersReachDoubaoSDKWebSocket(t *testing.T) {
if string(bound.ASR.Params["model_name"]) != `"ignored-params-model"` || string(bound.ASR.Params["reqid"]) != `"ignored-params-reqid"` || string(bound.ASR.Params["sequence"]) != "99" {
t.Fatal("ASR request construction changed the original params")
}
if len(audio) != 4 || len(request) != 9 || audio["format"] != "pcm_s16le" || audio["sample_rate"] != float64(16000) || audio["channel"] != float64(1) || audio["bits"] != float64(16) || request["result_type"] != tc.resultType || request["enable_nonstream"] != tc.nonstream || request["unknown_flag"] != false || request["nullable"] != nil || request["opaque"] != "vendor-only" {
if len(audio) != 4 || len(request) != 10 || audio["format"] != "pcm_s16le" || audio["sample_rate"] != float64(8000) || audio["channel"] != float64(1) || audio["bits"] != float64(16) || request["sample_rate"] != float64(8000) || request["result_type"] != tc.resultType || request["enable_nonstream"] != tc.nonstream || request["unknown_flag"] != false || request["nullable"] != nil || request["opaque"] != "vendor-only" {
t.Fatalf("SDK ASR request omitted approved input/interim fields: audio=%v request=%v", audio, request)
}
case <-ctx.Done():
+1 -1
View File
@@ -12,7 +12,7 @@ func bailianFixture(t *testing.T) Binding {
a[role].(map[string]any)["provider_ref"] = "ali_bailian"
}
a["asr"].(map[string]any)["model"] = "fun-asr-flash-8k-realtime-2026-01-28"
a["asr"].(map[string]any)["params"] = map[string]any{"sample_rate": 16000, "format": "pcm", "language": "zh"}
a["asr"].(map[string]any)["params"] = map[string]any{"sample_rate": 8000, "format": "pcm", "language": "zh"}
a["llm"].(map[string]any)["model"] = "Qwen3.8-Flash"
a["tts"].(map[string]any)["model"] = "cosyvoice-v3-flash"
a["tts"].(map[string]any)["voice"] = "longanyang"
+2 -2
View File
@@ -98,7 +98,7 @@ func TestFunASRPreservesPCMAndReturnsOnlyConfirmedFinalSentences(t *testing.T) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
cmd := mockRead(t, c, ctx)
if cmd.Payload["model"] != "fun-asr-flash-8k-realtime-2026-01-28" || cmd.Payload["parameters"].(map[string]any)["sample_rate"] != float64(16000) {
if cmd.Payload["model"] != "fun-asr-flash-8k-realtime-2026-01-28" || cmd.Payload["parameters"].(map[string]any)["sample_rate"] != float64(8000) {
t.Error("ASR model/sample rate changed")
}
mockEvent(c, ctx, cmd.Header.TaskID, "task-started", map[string]any{})
@@ -129,7 +129,7 @@ func TestFunASRPreservesPCMAndReturnsOnlyConfirmedFinalSentences(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
text, err := bound.Recognize(ctx, make([]byte, 32000))
if err != nil || text != "第一句。第二句。" || bytesReceived.Load() != 32000 {
if err != nil || text != "第一句。第二句。" || bytesReceived.Load() != 16000 {
t.Fatalf("ASR: text=%q err=%v sent=%d", text, err, bytesReceived.Load())
}
}
+9 -4
View File
@@ -27,9 +27,10 @@ type Binding struct {
}
type ASRConfig struct {
Provider configread.Provider
Model string
Params map[string]json.RawMessage
Provider configread.Provider
Model string
SampleRate int
Params map[string]json.RawMessage
}
type LLMConfig struct {
Provider configread.Provider
@@ -97,11 +98,15 @@ func Bind(task configread.Task, providers map[string]configread.Provider) (Bindi
return Binding{}, errors.New("decode immutable AI settings")
}
b := Binding{Mode: s.Mode}
sampleRate, err := parseASRSampleRate(s.ASR.Params)
if err != nil {
return Binding{}, fmt.Errorf("ASR settings: %w", err)
}
p, err := currentProvider(providers, s.ASR.ProviderRef, "asr")
if err != nil {
return Binding{}, err
}
b.ASR = ASRConfig{Provider: p, Model: s.ASR.Model, Params: s.ASR.Params}
b.ASR = ASRConfig{Provider: p, Model: s.ASR.Model, SampleRate: sampleRate, Params: s.ASR.Params}
if s.Mode == "asr_only" {
return b, nil
}
+1 -1
View File
@@ -28,7 +28,7 @@ func currentFixture(t *testing.T, mode string) (configread.Task, map[string]conf
providers[code] = configread.Provider{ProviderRef: "catalog-" + code, Code: code, Name: "Example only", Endpoint: "https://example.invalid", WSEndpoint: "wss://example.invalid", Credential: "example-only-not-a-real-secret"}
}
task = changeCurrentAgent(t, task, func(a map[string]any) {
a["asr"] = map[string]any{"provider_ref": "volcengine", "model": "example-asr", "params": map[string]any{"result_type": "full", "enable_nonstream": false, "enable_itn": false}}
a["asr"] = map[string]any{"provider_ref": "volcengine", "model": "example-asr", "params": map[string]any{"result_type": "full", "enable_nonstream": false, "enable_itn": false, "sample_rate": 16000}}
if mode == "full_ai" {
a["llm"] = map[string]any{"provider_ref": "openai_compatible", "model": "example-chat", "params": map[string]any{"temperature": 0, "max_tokens": 256}}
a["tts"] = map[string]any{"provider_ref": "ali_bailian", "model": "qwen3-tts-flash", "voice": "Cherry", "params": map[string]any{"format": "pcm", "sample_rate": 16000, "rate": 1, "language": "Chinese"}}
+1 -1
View File
@@ -20,7 +20,7 @@ func TestTaskFieldsWinOverSameDifferentAndNullParamValues(t *testing.T) {
} else if name == "null" {
asrModel, llmModel, voice = nil, nil, nil
}
agent["asr"].(map[string]any)["params"] = map[string]any{"model_name": asrModel, "reqid": "ignored-id", "sequence": 99, "opaque": false}
agent["asr"].(map[string]any)["params"] = map[string]any{"sample_rate": 16000, "model_name": asrModel, "reqid": "ignored-id", "sequence": 99, "opaque": false}
agent["llm"].(map[string]any)["params"] = map[string]any{"model": llmModel, "messages": "ignored-messages", "opaque": false}
agent["tts"].(map[string]any)["params"] = map[string]any{"voice": voice, "opaque": false}
})
+10 -3
View File
@@ -138,8 +138,15 @@ func (b Binding) Recognize(ctx context.Context, pcm16 []byte) (string, error) {
if len(pcm16) == 0 || len(pcm16)%2 != 0 {
return "", errors.New("ASR requires nonempty signed 16-bit PCM")
}
asrPCM16, err := prepareASRPCM16(pcm16, b.ASR.SampleRate)
if err != nil {
return "", fmt.Errorf("prepare ASR audio: %w", err)
}
if b.ASR.SampleRate != internalPCM16SampleRate {
slog.Debug("resampled ASR input", "source_sample_rate", internalPCM16SampleRate, "target_sample_rate", b.ASR.SampleRate, "input_bytes", len(pcm16), "output_bytes", len(asrPCM16))
}
if b.ASR.Provider.Code == "ali_bailian" {
return recognizeBailian(ctx, b.ASR, pcm16)
return recognizeBailian(ctx, b.ASR, asrPCM16)
}
u, err := url.Parse(b.ASR.Provider.Endpoint)
if err != nil || u.Host == "" || (u.Scheme != "wss" && u.Scheme != "ws") {
@@ -157,7 +164,7 @@ func (b Binding) Recognize(ctx context.Context, pcm16 []byte) (string, error) {
delete(parameters, key)
}
request := doubaospeech.ASRV2Config{
Format: doubaospeech.FormatPCMS16LE, SampleRate: 16000, Channel: 1, Bits: 16,
Format: doubaospeech.FormatPCMS16LE, SampleRate: doubaospeech.SampleRate(b.ASR.SampleRate), Channel: 1, Bits: 16,
Request: &doubaospeech.ASRV2RequestConfig{ModelName: b.ASR.Model},
Parameters: parameters,
}
@@ -166,7 +173,7 @@ func (b Binding) Recognize(ctx context.Context, pcm16 []byte) (string, error) {
return "", err
}
defer session.Close()
if err := session.SendAudio(ctx, pcm16, true); err != nil {
if err := session.SendAudio(ctx, asrPCM16, true); err != nil {
return "", err
}
var final finalASRAccumulator
+8 -6
View File
@@ -64,14 +64,16 @@ func TestTaskTTSProtocolForwardsOpaqueModelVoiceAndParams(t *testing.T) {
}
}
func TestTaskASRForwardsParamsWithoutRewritingAudio(t *testing.T) {
func TestTaskASRResamplesToConfiguredRateAndForwardsParams(t *testing.T) {
task, ps := protocolTask(t, TTSProtocolDashScopeTask)
task = changeCurrentAgent(t, task, func(agent map[string]any) {
agent["asr"].(map[string]any)["params"] = map[string]any{"sample_rate": 8000, "opaque": "unchanged", "nullable": nil}
})
b, e := Bind(task, ps)
if e != nil {
t.Fatal(e)
}
b.ASR.Params = map[string]json.RawMessage{"sample_rate": json.RawMessage("24000"), "opaque": json.RawMessage(`"unchanged"`), "nullable": json.RawMessage("null")}
pcm := []byte{1, 0, 2, 0}
pcm := []byte{1, 0, 2, 0, 3, 0, 4, 0}
captured := make(chan []byte, 1)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, e := websocket.Accept(w, r, nil)
@@ -84,7 +86,7 @@ func TestTaskASRForwardsParamsWithoutRewritingAudio(t *testing.T) {
defer cancel()
cmd := mockRead(t, c, ctx)
p := cmd.Payload["parameters"].(map[string]any)
if len(p) != 3 || p["sample_rate"] != float64(24000) || p["opaque"] != "unchanged" || p["nullable"] != nil {
if len(p) != 3 || p["sample_rate"] != float64(8000) || p["opaque"] != "unchanged" || p["nullable"] != nil {
t.Error("ASR params rewritten")
}
mockEvent(c, ctx, cmd.Header.TaskID, "task-started", nil)
@@ -106,7 +108,7 @@ func TestTaskASRForwardsParamsWithoutRewritingAudio(t *testing.T) {
if e != nil || text != "Final." {
t.Fatal(e)
}
if !bytes.Equal(<-captured, pcm) {
t.Fatal("input media rewritten from opaque params")
if !bytes.Equal(<-captured, []byte{1, 0, 3, 0}) {
t.Fatal("ASR audio was not resampled to the configured sample rate")
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
func TestNewSchemaBindsCodeReferenceAndOpaqueParams(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
task = changeCurrentAgent(t, task, func(a map[string]any) {
a["asr"].(map[string]any)["params"] = map[string]any{"unknown_flag": false, "nullable": nil, "opaque": "vendor-value"}
a["asr"].(map[string]any)["params"] = map[string]any{"sample_rate": 16000, "unknown_flag": false, "nullable": nil, "opaque": "vendor-value"}
a["llm"].(map[string]any)["params"] = map[string]any{"temperature": 0, "unknown_limit": json.Number("9007199254740993")}
a["tts"].(map[string]any)["params"] = map[string]any{"rate": 9.5, "language": "vendor-only"}
a["prompt"].(map[string]any)["text"] = strings.Repeat("长", 40000)
+31
View File
@@ -0,0 +1,31 @@
package audio
import (
"bytes"
"testing"
)
func TestResamplePCM16BetweenTelephonyRates(t *testing.T) {
pcm8k := []byte{1, 0, 3, 0}
pcm16k := ResamplePCM16(pcm8k, 8000, 16000)
if want := []byte{1, 0, 2, 0, 3, 0, 3, 0}; !bytes.Equal(pcm16k, want) {
t.Fatalf("8 kHz to 16 kHz = %v, want %v", pcm16k, want)
}
downsampled := ResamplePCM16(pcm16k, 16000, 8000)
if !bytes.Equal(downsampled, pcm8k) {
t.Fatalf("16 kHz to 8 kHz = %v, want %v", downsampled, pcm8k)
}
}
func TestResamplePCM16AtSameRateReturnsIndependentCopy(t *testing.T) {
input := []byte{1, 0, 2, 0}
got := ResamplePCM16(input, 16000, 16000)
if !bytes.Equal(got, input) {
t.Fatalf("same-rate PCM16 = %v, want %v", got, input)
}
got[0] = 9
if input[0] != 1 {
t.Fatal("same-rate resampling returned the original buffer")
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ import (
func malformedASRTask(t *testing.T) []byte {
t.Helper()
original := configExample(t, "config-read-task-asr")
modified := bytes.Replace(original, []byte(`"params":{}`), []byte(`"params":{"nested":{"unexpected":true}}`), 1)
modified := bytes.Replace(original, []byte(`"params":{"sample_rate":16000}`), []byte(`"params":{"sample_rate":16000,"nested":{"unexpected":true}}`), 1)
if bytes.Equal(original, modified) {
t.Fatal("ASR fixture did not contain params")
}
+2 -2
View File
@@ -17,7 +17,7 @@ import (
func TestApprovedAgentReceivesFrozenASROnlySnapshot(t *testing.T) {
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
req := approvedTestRequest(t, now)
req.TaskConfigJson = bytes.Replace(req.TaskConfigJson, []byte(`"params":{}`), []byte(`"params":{"reqid":"ignored-id","sequence":99,"model_name":"ignored-model","opaque":false}`), 1)
req.TaskConfigJson = bytes.Replace(req.TaskConfigJson, []byte(`"params":{"sample_rate":16000}`), []byte(`"params":{"sample_rate":16000,"reqid":"ignored-id","sequence":99,"model_name":"ignored-model","opaque":false}`), 1)
hash, err := configread.ExecutionBindingSHA256(req.TaskConfigJson, req.ProvidersJson, req.SipRevision)
if err != nil {
t.Fatal(err)
@@ -50,7 +50,7 @@ func TestApprovedAgentReceivesFrozenASROnlySnapshot(t *testing.T) {
func TestApprovedAgentRejectsMalformedParamsWithoutPersistingAttempt(t *testing.T) {
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
req := approvedTestRequest(t, now)
req.TaskConfigJson = bytes.Replace(req.TaskConfigJson, []byte(`"params":{}`), []byte(`"params":{"nested":{"unexpected":true}}`), 1)
req.TaskConfigJson = bytes.Replace(req.TaskConfigJson, []byte(`"params":{"sample_rate":16000}`), []byte(`"params":{"sample_rate":16000,"nested":{"unexpected":true}}`), 1)
if !strings.Contains(string(req.TaskConfigJson), `"nested":{"unexpected":true}`) {
t.Fatal("malformed params were not inserted into fixture")
}
+1 -1
View File
@@ -28,7 +28,7 @@ func TestStoreSnapshotPreservesAgentAndAllowsNewSIPRevision(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if loaded.SIP.Revision != 8 || loaded.Quota.MaxConcurrentCalls != 3 || loaded.Providers["volcengine"].Credential != "sk-******" || !strings.Contains(string(loaded.Task.Agent.Raw), `"params":{}`) {
if loaded.SIP.Revision != 8 || loaded.Quota.MaxConcurrentCalls != 3 || loaded.Providers["volcengine"].Credential != "sk-******" || !strings.Contains(string(loaded.Task.Agent.Raw), `"params":{"sample_rate":16000}`) {
t.Fatalf("binding was lost across restart: SIP=%d quota=%d agent=%s", loaded.SIP.Revision, loaded.Quota.MaxConcurrentCalls, loaded.Task.Agent.Raw)
}
snapshot.SIP.Revision = 9