refactor(ai): name approved bindings by domain role

This commit is contained in:
2026-09-30 17:02:22 +08:00
parent a6fbe2b129
commit 91d23a15d3
21 changed files with 174 additions and 129 deletions
@@ -140,6 +140,8 @@
- 当前文档单入口:`AGENTS.md`、根 `README.md` 和 `docs/README.md` 不再把旧 MQ-only、F 工作包或 v0.4 发现写作当前执行依据,改指唯一项目内通信说明、机器合同与新的 `docs/plan-saas-dispatcher.md` 状态入口;物理部署说明标明当前制品只支持隔离 Mock、不批准真实安装。原 AGENTS 原字节归档到 `docs/archive/AGENTS-before-current-contract.md`(SHA-256 `8996ce2db909508172f59f39e780bdab2f48b2eba1acc3590dcffc50f8b2f6f0`)。版本化计划 `docs/plan-saas-dispatcher-v05-v0.1.md` 已被当前 `contracts/local/manifest.json` 固定原路径及 SHA-256 `666f39e56ea9f4b55661efcac82edd6f9729848e2d60e5f24cdf5aa3ac97ee87`,因此保留原字节作为合同**历史来源例外**,另建不带代次的当前计划索引,而不伪造来源或另立运行合同。四份当前根文档的相对链接、两份原字节 SHA-256、`go test ./contracts ./internal/config`、`make check`(含 race/vet/build 与三项真实隔离 MQ PASS)及 `make release-check-local` 均已核对通过;当前待办仍有 AI 自有名称及历史夹具残留分类,本批不宣布 P07 完成。真实 SaaS、Asterisk/ECS、OSS、AI 与非生产主机诊断均未运行。
- AI 自有代次命名清理:新增 `TestAIBindingsUseDomainNames`,先在旧 `CurrentBound`/`BindCurrent` 和 `current*.go` 上得到预期失败;将获批配置绑定、ASR/LLM/TTS/对话参数和单通话控制统一改为业务名 `Binding`、`ASRConfig`/`LLMConfig`/`TTSConfig`/`ConversationConfig`、`Bind`、`Call`/`NewCall`,对应 `internal/ai/` 的源码和测试文件不再带 `current` 前缀,调用方与测试同步改名,业务行为未增减。命名测试、四个受影响模块的 `go test -race ... -count=1`、全仓 `make check`(含三项真实隔离 MQ PASS)及 `make release-check-local` 通过,旧 AI 标识和文件名在活动 Go 源中已无匹配。旧 `contracts/local/v0.1–v0.4` 文件与未使用的 `internal/testfixture` 仍需按真实来源及调用情况分类,不能凭命名删除合同来源或现存数据;此批不宣称 P07 完成。
## 验收台账
P01–P06 的项目内隔离证据见上;P07 全仓命名与唯一入口尚在清理,A01–A12 和 K01–K16 的最终对照仍待 P08。不得用本地 Mock 冒充外部签收。
+2 -2
View File
@@ -23,7 +23,7 @@ type ApprovedMockTurn struct {
}
// ApprovedMockPipeline is private to one approved Mock call. The signed AI
// parameters remain owned by CurrentBound; this adapter neither contacts a
// parameters remain owned by Binding; this adapter neither contacts a
// provider nor silently manufactures absent script turns or audio.
type ApprovedMockPipeline struct {
mu sync.Mutex
@@ -36,7 +36,7 @@ type ApprovedMockPipeline struct {
next int
}
func NewApprovedMockPipeline(bound CurrentBound, script ApprovedMockScript, hangup func(context.Context) error) (*ApprovedMockPipeline, error) {
func NewApprovedMockPipeline(bound Binding, script ApprovedMockScript, hangup func(context.Context) error) (*ApprovedMockPipeline, error) {
keyword, err := NewKeywordHangup(bound.HangupKeywords, hangup)
if err != nil {
return nil, err
+12 -12
View File
@@ -8,7 +8,7 @@ import (
func TestApprovedMockASROnlyDoesNotInventRefusalOrAssistantAudio(t *testing.T) {
var hangups int
pipeline, err := NewApprovedMockPipeline(CurrentBound{Mode: "asr_only", Conversation: CurrentConversation{MaxTurns: 1}}, ApprovedMockScript{
pipeline, err := NewApprovedMockPipeline(Binding{Mode: "asr_only", Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{
Turns: []ApprovedMockTurn{{Transcript: "我拒绝但是没有配置关键词"}},
}, func(context.Context) error { hangups++; return nil })
if err != nil {
@@ -27,7 +27,7 @@ func TestApprovedMockASROnlyDoesNotInventRefusalOrAssistantAudio(t *testing.T) {
}
func TestApprovedMockAbsentOptionalTurnLimitUsesSharedOneTurnBoundary(t *testing.T) {
pipeline, err := NewApprovedMockPipeline(CurrentBound{Mode: "asr_only"}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "隔离识别"}}}, func(context.Context) error { return nil })
pipeline, err := NewApprovedMockPipeline(Binding{Mode: "asr_only"}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "隔离识别"}}}, func(context.Context) error { return nil })
if err != nil {
t.Fatal(err)
}
@@ -44,7 +44,7 @@ func TestApprovedMockFullAIFreezesScriptAndStopsBeforeKeywordReply(t *testing.T)
{Transcript: "请不要联系", Reply: "不得播报", ReplyPCM16: reply},
}}
var hangups int
bound := CurrentBound{Mode: "full_ai", Opening: "批准的开场", HangupKeywords: []string{"不要联系"}, LLM: &CurrentLLM{}, TTS: &CurrentTTS{}, Conversation: CurrentConversation{MaxTurns: 2}}
bound := Binding{Mode: "full_ai", Opening: "批准的开场", HangupKeywords: []string{"不要联系"}, LLM: &LLMConfig{}, TTS: &TTSConfig{}, Conversation: ConversationConfig{MaxTurns: 2}}
pipeline, err := NewApprovedMockPipeline(bound, script, func(context.Context) error { hangups++; return nil })
if err != nil {
t.Fatal(err)
@@ -70,16 +70,16 @@ func TestApprovedMockFullAIFreezesScriptAndStopsBeforeKeywordReply(t *testing.T)
func TestApprovedMockRejectsMissingAIOrUnscriptedMedia(t *testing.T) {
for _, tc := range []struct {
name string
bound CurrentBound
bound Binding
script ApprovedMockScript
}{
{"missing_script", CurrentBound{Mode: "asr_only", Conversation: CurrentConversation{MaxTurns: 1}}, ApprovedMockScript{}},
{"asr_with_llm", CurrentBound{Mode: "asr_only", LLM: &CurrentLLM{}, Conversation: CurrentConversation{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}},
{"full_without_llm", CurrentBound{Mode: "full_ai", TTS: &CurrentTTS{}, Conversation: CurrentConversation{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}},
{"unavailable_opening_audio", CurrentBound{Mode: "full_ai", LLM: &CurrentLLM{}, TTS: &CurrentTTS{}, Opening: "批准开场", Conversation: CurrentConversation{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试", Reply: "模拟答复", ReplyPCM16: []byte{1, 0}}}}},
{"unsolicited_odd_opening_audio", CurrentBound{Mode: "full_ai", LLM: &CurrentLLM{}, TTS: &CurrentTTS{}, Conversation: CurrentConversation{MaxTurns: 1}}, ApprovedMockScript{OpeningPCM16: []byte{1}, Turns: []ApprovedMockTurn{{Transcript: "测试", Reply: "模拟答复", ReplyPCM16: []byte{1, 0}}}}},
{"extra_unapproved_turn", CurrentBound{Mode: "asr_only", Conversation: CurrentConversation{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "第一轮"}, {Transcript: "第二轮"}}}},
{"negative_turn_limit", CurrentBound{Mode: "asr_only", Conversation: CurrentConversation{MaxTurns: -1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}},
{"missing_script", Binding{Mode: "asr_only", Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{}},
{"asr_with_llm", Binding{Mode: "asr_only", LLM: &LLMConfig{}, Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}},
{"full_without_llm", Binding{Mode: "full_ai", TTS: &TTSConfig{}, Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}},
{"unavailable_opening_audio", Binding{Mode: "full_ai", LLM: &LLMConfig{}, TTS: &TTSConfig{}, Opening: "批准开场", Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试", Reply: "模拟答复", ReplyPCM16: []byte{1, 0}}}}},
{"unsolicited_odd_opening_audio", Binding{Mode: "full_ai", LLM: &LLMConfig{}, TTS: &TTSConfig{}, Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{OpeningPCM16: []byte{1}, Turns: []ApprovedMockTurn{{Transcript: "测试", Reply: "模拟答复", ReplyPCM16: []byte{1, 0}}}}},
{"extra_unapproved_turn", Binding{Mode: "asr_only", Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "第一轮"}, {Transcript: "第二轮"}}}},
{"negative_turn_limit", Binding{Mode: "asr_only", Conversation: ConversationConfig{MaxTurns: -1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}},
} {
t.Run(tc.name, func(t *testing.T) {
if _, err := NewApprovedMockPipeline(tc.bound, tc.script, func(context.Context) error { return nil }); err == nil {
@@ -87,7 +87,7 @@ func TestApprovedMockRejectsMissingAIOrUnscriptedMedia(t *testing.T) {
}
})
}
if _, err := NewApprovedMockPipeline(CurrentBound{Mode: "asr_only"}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}, nil); err == nil {
if _, err := NewApprovedMockPipeline(Binding{Mode: "asr_only"}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}, nil); err == nil {
t.Fatal("mock without a real hangup callback was admitted")
}
}
@@ -18,7 +18,7 @@ type currentASRWireCapture struct {
err error
}
func TestCurrentASRParametersReachDoubaoSDKWebSocket(t *testing.T) {
func TestASRParametersReachDoubaoSDKWebSocket(t *testing.T) {
for _, tc := range []struct {
mode, resultType string
nonstream bool
@@ -43,7 +43,7 @@ func TestCurrentASRParametersReachDoubaoSDKWebSocket(t *testing.T) {
asrProvider := providers["asr-example"]
asrProvider.Endpoint = server.URL
providers[asrProvider.ProviderRef] = asrProvider
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
@@ -13,29 +13,29 @@ import (
doubaospeech "github.com/GizClaw/doubao-speech-go"
)
// CurrentBound is a per-call, immutable binding of the approved task and
// Binding is a per-call, immutable binding of the approved task and
// provider settings. It must be built before dispatching any side effects.
// Credential values are never included in errors or logs.
type CurrentBound struct {
type Binding struct {
Mode string
ASR CurrentASR
LLM *CurrentLLM
TTS *CurrentTTS
ASR ASRConfig
LLM *LLMConfig
TTS *TTSConfig
Prompt string
PromptMaxBytes int
AllowedVariables []string
Opening string
HangupKeywords []string
Conversation CurrentConversation
Conversation ConversationConfig
}
type CurrentASR struct {
type ASRConfig struct {
Provider configread.Provider
Request doubaospeech.ASRV2Config
Timeout time.Duration
}
type CurrentLLM struct {
type LLMConfig struct {
Provider configread.Provider
Model string
Temperature *float64
@@ -43,15 +43,15 @@ type CurrentLLM struct {
Timeout time.Duration
}
type CurrentTTS struct {
type TTSConfig struct {
Provider configread.Provider
Request doubaospeech.TTSV2Request
Timeout time.Duration
}
// CurrentConversation preserves the task's explicit dialogue limits. An
// ConversationConfig preserves the task's explicit dialogue limits. An
// absent positive limit remains zero; an explicit false remains non-nil.
type CurrentConversation struct {
type ConversationConfig struct {
AllowInterrupt *bool
SilenceTimeout time.Duration
MaxDuration time.Duration
@@ -111,51 +111,51 @@ type currentAgentSettings struct {
} `json:"conversation"`
}
// BindCurrent rejects schema-valid settings which the selected SDK cannot
// Bind rejects schema-valid settings which the selected SDK cannot
// express. In particular, the published TTS schema is intentionally not
// silently narrowed to the SDK's speed/format capabilities.
func BindCurrent(task configread.Task, providers map[string]configread.Provider) (CurrentBound, error) {
func Bind(task configread.Task, providers map[string]configread.Provider) (Binding, error) {
if len(task.Raw) == 0 {
return CurrentBound{}, errors.New("approved task snapshot is missing")
return Binding{}, errors.New("approved task snapshot is missing")
}
if err := contract.ValidateCurrent("config-read", task.Raw); err != nil {
return CurrentBound{}, fmt.Errorf("task snapshot: %w", err)
return Binding{}, fmt.Errorf("task snapshot: %w", err)
}
var frozen configread.Task
if err := json.Unmarshal(task.Raw, &frozen); err != nil {
return CurrentBound{}, fmt.Errorf("decode task snapshot: %w", err)
return Binding{}, fmt.Errorf("decode task snapshot: %w", err)
}
if frozen.Resource != "task_config" || frozen.TaskID != task.TaskID || frozen.TenantID != task.TenantID || frozen.DispatcherID != task.DispatcherID {
return CurrentBound{}, errors.New("approved task snapshot identity mismatch")
return Binding{}, errors.New("approved task snapshot identity mismatch")
}
var settings currentAgentSettings
if err := json.Unmarshal(frozen.Agent.Raw, &settings); err != nil {
return CurrentBound{}, fmt.Errorf("decode immutable AI settings: %w", err)
return Binding{}, fmt.Errorf("decode immutable AI settings: %w", err)
}
bound := CurrentBound{Mode: settings.Mode}
bound := Binding{Mode: settings.Mode}
asrProvider, err := currentProvider(providers, settings.ASR.ProviderRef, "asr", "volcengine_asr")
if err != nil {
return CurrentBound{}, err
return Binding{}, err
}
if settings.ASR.Input.Encoding != "pcm_s16le" || settings.ASR.Input.Channels != 1 || settings.ASR.Input.SampleWidthBytes != 2 {
return CurrentBound{}, errors.New("ASR input format is unsupported by selected SDK")
return Binding{}, errors.New("ASR input format is unsupported by selected SDK")
}
sampleRate, err := currentSampleRate(settings.ASR.Input.SampleRateHz)
if err != nil {
return CurrentBound{}, fmt.Errorf("ASR input: %w", err)
return Binding{}, fmt.Errorf("ASR input: %w", err)
}
if sampleRate != doubaospeech.SampleRate(16000) {
return CurrentBound{}, errors.New("ASR media requires 16000 Hz PCM16")
return Binding{}, errors.New("ASR media requires 16000 Hz PCM16")
}
lang := doubaospeech.Language(settings.ASR.Language)
switch lang {
case doubaospeech.LanguageZhCN, doubaospeech.LanguageEnUS, doubaospeech.LanguageJaJP, doubaospeech.LanguageKoKR:
default:
return CurrentBound{}, errors.New("ASR language is unsupported by selected SDK")
return Binding{}, errors.New("ASR language is unsupported by selected SDK")
}
asrTimeout, err := currentTimeout(settings.ASR.TimeoutMS)
if err != nil {
return CurrentBound{}, fmt.Errorf("ASR timeout: %w", err)
return Binding{}, fmt.Errorf("ASR timeout: %w", err)
}
asrRequest := doubaospeech.ASRV2Config{
Format: doubaospeech.FormatPCMS16LE, SampleRate: sampleRate,
@@ -172,54 +172,54 @@ func BindCurrent(task configread.Task, providers map[string]configread.Provider)
}
asrRequest.Request.ResultType = asrRequest.ResultType
}
bound.ASR = CurrentASR{Provider: asrProvider, Request: asrRequest, Timeout: asrTimeout}
bound.ASR = ASRConfig{Provider: asrProvider, Request: asrRequest, Timeout: asrTimeout}
switch settings.Mode {
case "asr_only":
return bound, nil
case "full_ai":
if settings.LLM == nil || settings.TTS == nil || settings.Prompt == nil || settings.Conversation == nil {
return CurrentBound{}, errors.New("full AI settings are incomplete")
return Binding{}, errors.New("full AI settings are incomplete")
}
default:
return CurrentBound{}, errors.New("AI mode is unsupported")
return Binding{}, errors.New("AI mode is unsupported")
}
llmProvider, err := currentProvider(providers, settings.LLM.ProviderRef, "llm", "openai_compatible")
if err != nil {
return CurrentBound{}, err
return Binding{}, err
}
llmTimeout, err := currentTimeout(settings.LLM.TimeoutMS)
if err != nil {
return CurrentBound{}, fmt.Errorf("LLM timeout: %w", err)
return Binding{}, fmt.Errorf("LLM timeout: %w", err)
}
bound.LLM = &CurrentLLM{Provider: llmProvider, Model: settings.LLM.Model, Temperature: settings.LLM.Temperature, MaxTokens: settings.LLM.MaxTokens, Timeout: llmTimeout}
bound.LLM = &LLMConfig{Provider: llmProvider, Model: settings.LLM.Model, Temperature: settings.LLM.Temperature, MaxTokens: settings.LLM.MaxTokens, Timeout: llmTimeout}
ttsProvider, err := currentProvider(providers, settings.TTS.ProviderRef, "tts", "volcengine_tts")
if err != nil {
return CurrentBound{}, err
return Binding{}, err
}
if settings.TTS.Format.Encoding != "pcm_s16le" || settings.TTS.Format.Channels != 1 {
return CurrentBound{}, errors.New("TTS output encoding/channel is unsupported by selected SDK")
return Binding{}, errors.New("TTS output encoding/channel is unsupported by selected SDK")
}
ttsSampleRate, err := currentSampleRate(settings.TTS.Format.SampleRateHz)
if err != nil {
return CurrentBound{}, fmt.Errorf("TTS output: %w", err)
return Binding{}, fmt.Errorf("TTS output: %w", err)
}
if ttsSampleRate != doubaospeech.SampleRate(16000) {
return CurrentBound{}, errors.New("TTS media requires 16000 Hz PCM16")
return Binding{}, errors.New("TTS media requires 16000 Hz PCM16")
}
ttsRate := 0
if settings.TTS.Speed != nil {
floatRate := (*settings.TTS.Speed - 1) * 100
if floatRate < -50 || floatRate > 100 || math.Abs(floatRate-math.Round(floatRate)) > 1e-9 {
return CurrentBound{}, errors.New("TTS speed is unsupported by selected SDK")
return Binding{}, errors.New("TTS speed is unsupported by selected SDK")
}
ttsRate = int(math.Round(floatRate))
}
ttsTimeout, err := currentTimeout(settings.TTS.TimeoutMS)
if err != nil {
return CurrentBound{}, fmt.Errorf("TTS timeout: %w", err)
return Binding{}, fmt.Errorf("TTS timeout: %w", err)
}
bound.TTS = &CurrentTTS{Provider: ttsProvider, Request: doubaospeech.TTSV2Request{
bound.TTS = &TTSConfig{Provider: ttsProvider, Request: doubaospeech.TTSV2Request{
Speaker: settings.TTS.Voice, ResourceID: settings.TTS.Model,
Format: doubaospeech.FormatPCMS16LE, SampleRate: ttsSampleRate, SpeechRate: ttsRate,
}, Timeout: ttsTimeout}
@@ -228,21 +228,21 @@ func BindCurrent(task configread.Task, providers map[string]configread.Provider)
if settings.Prompt.MaxBytes != nil {
bound.PromptMaxBytes = *settings.Prompt.MaxBytes
if len(bound.Prompt) > bound.PromptMaxBytes {
return CurrentBound{}, errors.New("prompt exceeds its approved byte limit")
return Binding{}, errors.New("prompt exceeds its approved byte limit")
}
}
if settings.Conversation.AllowInterrupt != nil && *settings.Conversation.AllowInterrupt {
return CurrentBound{}, errors.New("conversation interrupt is unsupported by current media controller")
return Binding{}, errors.New("conversation interrupt is unsupported by current media controller")
}
silence, err := currentTimeout(settings.Conversation.SilenceTimeoutMS)
if err != nil {
return CurrentBound{}, fmt.Errorf("conversation silence timeout: %w", err)
return Binding{}, fmt.Errorf("conversation silence timeout: %w", err)
}
maxDuration, err := currentTimeout(settings.Conversation.MaxDurationMS)
if err != nil {
return CurrentBound{}, fmt.Errorf("conversation duration: %w", err)
return Binding{}, fmt.Errorf("conversation duration: %w", err)
}
bound.Conversation = CurrentConversation{
bound.Conversation = ConversationConfig{
AllowInterrupt: settings.Conversation.AllowInterrupt,
SilenceTimeout: silence, MaxDuration: maxDuration,
MaxTurns: settings.Conversation.MaxTurns,
@@ -63,7 +63,7 @@ func changeCurrentAgent(t *testing.T, task configread.Task, change func(map[stri
func TestBindCurrentFullAIUsesApprovedSDKFields(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
@@ -88,7 +88,7 @@ func TestBindCurrentASROnlyDoesNotBindOtherProviders(t *testing.T) {
task, providers := currentFixture(t, "asr_only")
delete(providers, "llm-example")
delete(providers, "tts-example")
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
@@ -111,7 +111,7 @@ func TestBindCurrentRejectsSDKUnsupportedTTSWithoutChangingSchema(t *testing.T)
t.Run(tc.name, func(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
task = changeCurrentAgent(t, task, func(agent map[string]any) { tc.edit(agent["tts"].(map[string]any)) })
_, err := BindCurrent(task, providers)
_, err := Bind(task, providers)
if err == nil || !strings.Contains(err.Error(), "TTS") || strings.Contains(err.Error(), providers["tts-example"].Credential) {
t.Fatalf("expected explicit non-secret TTS capability error, got %v", err)
}
@@ -149,7 +149,7 @@ func TestBindCurrentRejectsUnauthorizedProviderBeforeCall(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
tc.mutate(providers)
if _, err := BindCurrent(task, providers); err == nil {
if _, err := Bind(task, providers); err == nil {
t.Fatal("unavailable AI provider cannot authorize execution")
}
})
@@ -8,7 +8,7 @@ import (
func TestBindCurrentPreservesPromptAndConversationControls(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
@@ -26,7 +26,7 @@ func TestBindCurrentRejectsPromptOverConfiguredByteLimit(t *testing.T) {
task = changeCurrentAgent(t, task, func(agent map[string]any) {
agent["prompt"].(map[string]any)["max_bytes"] = 3
})
_, err := BindCurrent(task, providers)
_, err := Bind(task, providers)
if err == nil || !strings.Contains(err.Error(), "prompt") {
t.Fatalf("prompt limit was ignored: %v", err)
}
@@ -37,7 +37,7 @@ func TestBindCurrentRejectsUnsupportedBargeInBeforeCall(t *testing.T) {
task = changeCurrentAgent(t, task, func(agent map[string]any) {
agent["conversation"].(map[string]any)["allow_interrupt"] = true
})
_, err := BindCurrent(task, providers)
_, err := Bind(task, providers)
if err == nil || !strings.Contains(err.Error(), "interrupt") {
t.Fatalf("unsupported interruption cannot silently become false: %v", err)
}
@@ -12,7 +12,7 @@ import (
"testing"
)
func TestCurrentTTSRejectsExcessPendingAudioWithoutRetry(t *testing.T) {
func TestTTSRejectsExcessPendingAudioWithoutRetry(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
task = changeCurrentAgent(t, task, func(agent map[string]any) {
agent["conversation"].(map[string]any)["max_pending_audio_chunks"] = 1
@@ -29,7 +29,7 @@ func TestCurrentTTSRejectsExcessPendingAudioWithoutRetry(t *testing.T) {
p := providers["tts-example"]
p.Endpoint = server.URL
providers[p.ProviderRef] = p
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
@@ -41,7 +41,7 @@ func TestCurrentTTSRejectsExcessPendingAudioWithoutRetry(t *testing.T) {
}
}
func TestCurrentReplyChunksUnicodeByApprovedSentenceLimit(t *testing.T) {
func TestReplyChunksUnicodeByApprovedSentenceLimit(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
task = changeCurrentAgent(t, task, func(agent map[string]any) {
conversation := agent["conversation"].(map[string]any)
@@ -67,7 +67,7 @@ func TestCurrentReplyChunksUnicodeByApprovedSentenceLimit(t *testing.T) {
p := providers["tts-example"]
p.Endpoint = server.URL
providers[p.ProviderRef] = p
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
@@ -5,7 +5,7 @@ import (
"testing"
)
func TestCurrentBindRejectsSampleRatesUnsupportedByAgentMedia(t *testing.T) {
func TestBindRejectsSampleRatesUnsupportedByAgentMedia(t *testing.T) {
for _, tc := range []struct {
name, mode, endpoint string
}{
@@ -22,7 +22,7 @@ func TestCurrentBindRejectsSampleRatesUnsupportedByAgentMedia(t *testing.T) {
agent["tts"].(map[string]any)["format"].(map[string]any)["sample_rate_hz"] = 24000
}
})
if _, err := BindCurrent(task, providers); err == nil || !strings.Contains(err.Error(), "media") {
if _, err := Bind(task, providers); err == nil || !strings.Contains(err.Error(), "media") {
t.Fatalf("SDK-supported sample rate cannot silently mismatch 16 kHz Agent media: %v", err)
}
})
+43
View File
@@ -0,0 +1,43 @@
package ai
import (
"go/ast"
"go/parser"
"go/token"
"os"
"strings"
"testing"
)
func TestAIBindingsUseDomainNames(t *testing.T) {
files, err := os.ReadDir(".")
if err != nil {
t.Fatal(err)
}
for _, file := range files {
name := file.Name()
if file.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
continue
}
if strings.HasPrefix(name, "current") {
t.Errorf("%s retains an implementation-generation filename", name)
}
parsed, err := parser.ParseFile(token.NewFileSet(), name, nil, 0)
if err != nil {
t.Fatal(err)
}
ast.Inspect(parsed, func(node ast.Node) bool {
var name string
switch declaration := node.(type) {
case *ast.TypeSpec:
name = declaration.Name.Name
case *ast.FuncDecl:
name = declaration.Name.Name
}
if strings.Contains(name, "Current") {
t.Errorf("%s retains an implementation-generation declaration %s", file.Name(), name)
}
return true
})
}
}
@@ -12,7 +12,7 @@ import (
"testing"
)
func TestCurrentCallOpeningUsesApprovedTTSOnce(t *testing.T) {
func TestCallOpeningUsesApprovedTTSOnce(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
var calls atomic.Int32
text := make(chan string, 1)
@@ -35,11 +35,11 @@ func TestCurrentCallOpeningUsesApprovedTTSOnce(t *testing.T) {
p := providers["tts-example"]
p.Endpoint = server.URL
providers[p.ProviderRef] = p
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
call, err := NewCurrentCall(bound, func(context.Context) error { return nil })
call, err := NewCall(bound, func(context.Context) error { return nil })
if err != nil {
t.Fatal(err)
}
@@ -52,7 +52,7 @@ func TestCurrentCallOpeningUsesApprovedTTSOnce(t *testing.T) {
}
}
func TestCurrentCallOpeningFailureIsVisibleAndNeverRetried(t *testing.T) {
func TestCallOpeningFailureIsVisibleAndNeverRetried(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -63,11 +63,11 @@ func TestCurrentCallOpeningFailureIsVisibleAndNeverRetried(t *testing.T) {
p := providers["tts-example"]
p.Endpoint = server.URL
providers[p.ProviderRef] = p
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
call, err := NewCurrentCall(bound, func(context.Context) error { return nil })
call, err := NewCall(bound, func(context.Context) error { return nil })
if err != nil {
t.Fatal(err)
}
@@ -84,13 +84,13 @@ func TestCurrentCallOpeningFailureIsVisibleAndNeverRetried(t *testing.T) {
}
}
func TestCurrentCallASROnlyNeverRequestsOpeningTTS(t *testing.T) {
func TestCallASROnlyNeverRequestsOpeningTTS(t *testing.T) {
task, providers := currentFixture(t, "asr_only")
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
call, err := NewCurrentCall(bound, func(context.Context) error { return nil })
call, err := NewCall(bound, func(context.Context) error { return nil })
if err != nil {
t.Fatal(err)
}
@@ -99,16 +99,16 @@ func TestCurrentCallASROnlyNeverRequestsOpeningTTS(t *testing.T) {
}
}
func TestCurrentCallOptionalEmptyOpeningMakesNoTTSRequest(t *testing.T) {
func TestCallOptionalEmptyOpeningMakesNoTTSRequest(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
task = changeCurrentAgent(t, task, func(agent map[string]any) {
agent["conversation"].(map[string]any)["opening"] = ""
})
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
call, err := NewCurrentCall(bound, func(context.Context) error { return nil })
call, err := NewCall(bound, func(context.Context) error { return nil })
if err != nil {
t.Fatal(err)
}
@@ -120,13 +120,13 @@ func TestCurrentCallOptionalEmptyOpeningMakesNoTTSRequest(t *testing.T) {
}
}
func TestCurrentCallRequiresApprovedOpeningBeforeFullAITurn(t *testing.T) {
func TestCallRequiresApprovedOpeningBeforeFullAITurn(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
call, err := NewCurrentCall(bound, func(context.Context) error { return nil })
call, err := NewCall(bound, func(context.Context) error { return nil })
if err != nil {
t.Fatal(err)
}
@@ -16,27 +16,27 @@ import (
"github.com/openai/openai-go/v3/shared"
)
// CurrentCall owns the keyword action for exactly one call. A failed or
// Call owns the keyword action for exactly one call. A failed or
// uncertain hangup is never attempted again for that call.
type CurrentCall struct {
bound CurrentBound
type Call struct {
bound Binding
keyword *KeywordHangup
mu sync.Mutex
openingRequested bool
openingReady bool
}
func NewCurrentCall(bound CurrentBound, hangup func(context.Context) error) (*CurrentCall, error) {
func NewCall(bound Binding, hangup func(context.Context) error) (*Call, error) {
keyword, err := NewKeywordHangup(bound.HangupKeywords, hangup)
if err != nil {
return nil, err
}
return &CurrentCall{bound: bound, keyword: keyword}, nil
return &Call{bound: bound, keyword: keyword}, nil
}
// Open synthesizes the approved opening at most once per call. An ambiguous
// synthesis result is visible to the caller and never replayed automatically.
func (c *CurrentCall) Open(ctx context.Context) ([]byte, error) {
func (c *Call) Open(ctx context.Context) ([]byte, error) {
if c == nil || c.bound.Mode != "full_ai" {
return nil, errors.New("ASR-only call has no opening TTS")
}
@@ -63,7 +63,7 @@ func (c *CurrentCall) Open(ctx context.Context) ([]byte, error) {
return audio, nil
}
func (c *CurrentCall) HandleFinalASR(ctx context.Context, segment ASRSegment) (bool, error) {
func (c *Call) HandleFinalASR(ctx context.Context, segment ASRSegment) (bool, error) {
if c == nil {
return false, errors.New("AI call is unavailable")
}
@@ -72,7 +72,7 @@ func (c *CurrentCall) HandleFinalASR(ctx context.Context, segment ASRSegment) (b
// RunTurn performs one utterance. No LLM or TTS request is sent after a
// keyword hangup, and only a final user recognition may enter the LLM.
func (c *CurrentCall) RunTurn(ctx context.Context, pcm16 []byte) (TurnResult, error) {
func (c *Call) RunTurn(ctx context.Context, pcm16 []byte) (TurnResult, error) {
if c == nil {
return TurnResult{}, errors.New("AI call is unavailable")
}
@@ -109,7 +109,7 @@ func (c *CurrentCall) RunTurn(ctx context.Context, pcm16 []byte) (TurnResult, er
return TurnResult{Transcript: text, Reply: reply, AudioPCM16: audio}, nil
}
func (b CurrentBound) Recognize(ctx context.Context, pcm16 []byte) (string, error) {
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")
}
@@ -153,7 +153,7 @@ func (b CurrentBound) Recognize(ctx context.Context, pcm16 []byte) (string, erro
return final.Result()
}
func (b CurrentBound) Complete(ctx context.Context, finalUserText string) (string, error) {
func (b Binding) Complete(ctx context.Context, finalUserText string) (string, error) {
if b.Mode != "full_ai" || b.LLM == nil {
return "", errors.New("ASR-only mode does not call LLM")
}
@@ -185,14 +185,14 @@ func (b CurrentBound) Complete(ctx context.Context, finalUserText string) (strin
return result.Choices[0].Message.Content, nil
}
func (b CurrentBound) Synthesize(ctx context.Context, text string) ([]byte, error) {
func (b Binding) Synthesize(ctx context.Context, text string) ([]byte, error) {
audio, _, err := b.synthesize(ctx, text, 0)
return audio, err
}
// SynthesizeReply splits a reply at the approved Unicode character limit,
// retaining every character and the same pending-audio bound across requests.
func (b CurrentBound) SynthesizeReply(ctx context.Context, text string) ([]byte, error) {
func (b Binding) SynthesizeReply(ctx context.Context, text string) ([]byte, error) {
if strings.TrimSpace(text) == "" {
return nil, errors.New("TTS requires nonempty reply")
}
@@ -215,7 +215,7 @@ func (b CurrentBound) SynthesizeReply(ctx context.Context, text string) ([]byte,
return audio, nil
}
func (b CurrentBound) synthesize(ctx context.Context, text string, alreadyPending int) ([]byte, int, error) {
func (b Binding) synthesize(ctx context.Context, text string, alreadyPending int) ([]byte, int, error) {
if b.Mode != "full_ai" || b.TTS == nil {
return nil, 0, errors.New("ASR-only mode does not call TTS")
}
@@ -11,7 +11,7 @@ import (
"testing"
)
func TestCurrentTTSPassesApprovedParametersToSDK(t *testing.T) {
func TestTTSPassesApprovedParametersToSDK(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
task = changeCurrentAgent(t, task, func(agent map[string]any) { agent["tts"].(map[string]any)["speed"] = 1.3 })
var captured map[string]any
@@ -33,7 +33,7 @@ func TestCurrentTTSPassesApprovedParametersToSDK(t *testing.T) {
p := providers["tts-example"]
p.Endpoint = server.URL
providers[p.ProviderRef] = p
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
@@ -51,7 +51,7 @@ func TestCurrentTTSPassesApprovedParametersToSDK(t *testing.T) {
}
}
func TestCurrentLLMPassesExplicitZeroAndDoesNotRetry(t *testing.T) {
func TestLLMPassesExplicitZeroAndDoesNotRetry(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
calls := 0
var captured map[string]any
@@ -74,7 +74,7 @@ func TestCurrentLLMPassesExplicitZeroAndDoesNotRetry(t *testing.T) {
p := providers["llm-example"]
p.Endpoint = server.URL + "/v1"
providers[p.ProviderRef] = p
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
@@ -91,7 +91,7 @@ func TestCurrentLLMPassesExplicitZeroAndDoesNotRetry(t *testing.T) {
}
}
func TestCurrentLLMFailureAndEmptyChoicesAreNotRetried(t *testing.T) {
func TestLLMFailureAndEmptyChoicesAreNotRetried(t *testing.T) {
for _, tc := range []struct {
name string
code int
@@ -113,7 +113,7 @@ func TestCurrentLLMFailureAndEmptyChoicesAreNotRetried(t *testing.T) {
provider := providers["llm-example"]
provider.Endpoint = server.URL + "/v1"
providers[provider.ProviderRef] = provider
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
@@ -124,14 +124,14 @@ func TestCurrentLLMFailureAndEmptyChoicesAreNotRetried(t *testing.T) {
}
}
func TestCurrentKeywordHangupUsesFinalUserTextOnly(t *testing.T) {
func TestKeywordHangupUsesFinalUserTextOnly(t *testing.T) {
task, providers := currentFixture(t, "full_ai")
bound, err := BindCurrent(task, providers)
bound, err := Bind(task, providers)
if err != nil {
t.Fatal(err)
}
calls := 0
call, err := NewCurrentCall(bound, func(context.Context) error { calls++; return nil })
call, err := NewCall(bound, func(context.Context) error { calls++; return nil })
if err != nil {
t.Fatal(err)
}
@@ -153,7 +153,7 @@ func TestCurrentKeywordHangupUsesFinalUserTextOnly(t *testing.T) {
}
}
func TestCurrentASRRequiresFinalResult(t *testing.T) {
func TestASRRequiresFinalResult(t *testing.T) {
var text finalASRAccumulator
text.Add("不用了", false)
if _, err := text.Result(); err == nil {
+1 -1
View File
@@ -28,7 +28,7 @@ func ExecuteApproved(ctx context.Context, session MediaSession, mode ai.Mode, pi
// ApprovedCapture hands explicit task conversation limits to the shared media
// controller. A zero field denotes an absent optional business value.
func ApprovedCapture(bound ai.CurrentBound) CaptureConfig {
func ApprovedCapture(bound ai.Binding) CaptureConfig {
return CaptureConfig{
FirstSpeechTimeout: bound.Conversation.SilenceTimeout,
CallDuration: bound.Conversation.MaxDuration,
+3 -3
View File
@@ -160,7 +160,7 @@ func TestApprovedASROnlySkipsOpeningAndReplyEvenIfPipelineHasAudio(t *testing.T)
}
func TestApprovedCapturePreservesConversationControls(t *testing.T) {
bound := ai.CurrentBound{Conversation: ai.CurrentConversation{SilenceTimeout: 3 * time.Second, MaxDuration: 2 * time.Minute, MaxTurns: 20, SentenceMaxChars: 80, MaxPendingAudioChunks: 32}}
bound := ai.Binding{Conversation: ai.ConversationConfig{SilenceTimeout: 3 * time.Second, MaxDuration: 2 * time.Minute, MaxTurns: 20, SentenceMaxChars: 80, MaxPendingAudioChunks: 32}}
capture := ApprovedCapture(bound)
if capture.FirstSpeechTimeout != 3*time.Second || capture.EndSilence != 3*time.Second || capture.CallDuration != 2*time.Minute || capture.MaxDuration != 0 || capture.MaxTurns != 20 || capture.VoiceThreshold <= 0 || capture.MaxPendingAudioChunks != 32 {
t.Fatalf("approved conversation limits were not handed to media controller: %+v", capture)
@@ -179,7 +179,7 @@ func (p *approvedDeadlineProbe) RunTurn(ctx context.Context, _ []byte) (ai.TurnR
func TestApprovedConversationDurationBoundsWholeCall(t *testing.T) {
pipeline := &approvedDeadlineProbe{}
session := &scriptedTurnSession{turns: [][]byte{bytes.Repeat([]byte{1, 0}, 3200)}}
capture := ApprovedCapture(ai.CurrentBound{Conversation: ai.CurrentConversation{
capture := ApprovedCapture(ai.Binding{Conversation: ai.ConversationConfig{
SilenceTimeout: 2 * time.Millisecond, MaxDuration: time.Second, MaxTurns: 1,
}})
if _, err := ExecuteApproved(context.Background(), session, ai.ModeASROnly, pipeline, capture); err != nil || !pipeline.deadlinePresent {
@@ -190,7 +190,7 @@ func TestApprovedConversationDurationBoundsWholeCall(t *testing.T) {
func TestApprovedSilenceTimeoutRejectsMissingSpeech(t *testing.T) {
pipeline := &approvedFlowPipeline{turn: ai.TurnResult{Transcript: "不应执行"}}
session := &scriptedTurnSession{turns: [][]byte{make([]byte, 6400)}}
capture := ApprovedCapture(ai.CurrentBound{Conversation: ai.CurrentConversation{
capture := ApprovedCapture(ai.Binding{Conversation: ai.ConversationConfig{
SilenceTimeout: 10 * time.Millisecond, MaxDuration: 100 * time.Millisecond, MaxTurns: 1,
}})
_, err := ExecuteApproved(context.Background(), session, ai.ModeASROnly, pipeline, capture)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
// approved task configuration. SDK-inexpressible settings must fail before a
// call reserves capacity or reaches the Agent; no defaults replace them.
func validateAISnapshot(snapshot configread.Snapshot) error {
if _, err := ai.BindCurrent(snapshot.Task, snapshot.Providers); err != nil {
if _, err := ai.Bind(snapshot.Task, snapshot.Providers); err != nil {
return fmt.Errorf("task %q approved AI snapshot: %w", snapshot.Task.TaskID, err)
}
return nil
+2 -2
View File
@@ -38,7 +38,7 @@ type ApprovedExecution struct {
RingTimeout time.Duration
MaxCallDuration time.Duration
DialBefore time.Time
AI ai.CurrentBound
AI ai.Binding
}
// GetLoadedSIP reports the adapter's observed state, not the desired static
@@ -112,7 +112,7 @@ func (s *Server) ExecuteApproved(ctx context.Context, req *agentpb.ExecuteApprov
if err := json.Unmarshal(req.ProvidersJson, &providers); err != nil {
return nil, status.Error(codes.InvalidArgument, "approved provider JSON is invalid")
}
bound, err := ai.BindCurrent(task, providers)
bound, err := ai.Bind(task, providers)
if err != nil {
// Errors from externally supplied snapshots must not echo a prompt,
// conversation, provider credential, or vendor endpoint into logs.
@@ -85,7 +85,7 @@ func TestApprovedFullAIUsesBoundProviderSDKsAndOnlyFinalUserKeyword(t *testing.T
if approved.AI.Mode != "full_ai" || approved.AI.LLM == nil || approved.AI.TTS == nil || approved.AI.Conversation.MaxTurns != 20 || approved.AI.LLM.Temperature == nil || *approved.AI.LLM.Temperature != 0 {
t.Fatal("approved full-AI settings were not delivered to the Agent")
}
call, err := ai.NewCurrentCall(approved.AI, func(context.Context) error { hangups++; return nil })
call, err := ai.NewCall(approved.AI, func(context.Context) error { hangups++; return nil })
if err != nil {
return err
}
+2 -2
View File
@@ -81,8 +81,8 @@ func recordedMockFixture(t *testing.T, pcm []byte, maxDuration time.Duration) (*
DispatcherID: "c046b893-8628-4589-ae50-619d049248a6", TenantID: 42,
TaskID: "task-asr", CallerProfileID: "caller-mock", SourceEventID: "event-fixture", CallID: "event-fixture",
Callee: "15003164745", SelectedTrunkID: "trunk-mock", MaxCallDuration: maxDuration,
DialBefore: time.Now().Add(time.Minute), AI: ai.CurrentBound{
Mode: "asr_only", Conversation: ai.CurrentConversation{SilenceTimeout: 100 * time.Millisecond, MaxDuration: maxDuration},
DialBefore: time.Now().Add(time.Minute), AI: ai.Binding{
Mode: "asr_only", Conversation: ai.ConversationConfig{SilenceTimeout: 100 * time.Millisecond, MaxDuration: maxDuration},
},
}
delivery := &agent.RecordingDelivery{
+8 -8
View File
@@ -49,13 +49,13 @@ func TestApprovedCallRunnerASROnlyHonorsSignedTimeout(t *testing.T) {
if err := json.Unmarshal(req.ProvidersJson, &providers); err != nil {
t.Fatal(err)
}
bound, err := ai.BindCurrent(task, providers)
bound, err := ai.Bind(task, providers)
if err != nil {
t.Fatal(err)
}
mediaSession := &blockedApprovedMedia{}
hangup := func(context.Context) error { return nil }
call, err := ai.NewCurrentCall(bound, hangup)
call, err := ai.NewCall(bound, hangup)
if err != nil {
t.Fatal(err)
}
@@ -98,13 +98,13 @@ func TestApprovedCallRunnerSynthesizesOpeningBeforeMediaCapture(t *testing.T) {
tts := providers["tts-example"]
tts.Endpoint = server.URL
providers[tts.ProviderRef] = tts
bound, err := ai.BindCurrent(task, providers)
bound, err := ai.Bind(task, providers)
if err != nil {
t.Fatal(err)
}
mediaSession := &blockedApprovedMedia{}
hangup := func(context.Context) error { return nil }
call, err := ai.NewCurrentCall(bound, hangup)
call, err := ai.NewCall(bound, hangup)
if err != nil {
t.Fatal(err)
}
@@ -119,7 +119,7 @@ func TestApprovedCallRunnerUsesExplicitIsolatedPipelineAndObservedMedia(t *testi
if err != nil {
t.Fatal(err)
}
bound := ai.CurrentBound{Mode: "asr_only", Conversation: ai.CurrentConversation{SilenceTimeout: 100 * time.Millisecond, MaxDuration: time.Second, MaxTurns: 1}}
bound := ai.Binding{Mode: "asr_only", Conversation: ai.ConversationConfig{SilenceTimeout: 100 * time.Millisecond, MaxDuration: time.Second, MaxTurns: 1}}
hangup := func(context.Context) error { return nil }
mock, err := ai.NewApprovedMockPipeline(bound, ai.ApprovedMockScript{Turns: []ai.ApprovedMockTurn{{Transcript: "隔离 Mock 最终识别文本"}}}, hangup)
if err != nil {
@@ -136,14 +136,14 @@ func TestApprovedCallRunnerUsesExplicitIsolatedPipelineAndObservedMedia(t *testi
}
func TestApprovedCallRunnerRejectsTypedNilPipelineBeforeMedia(t *testing.T) {
var missing *ai.CurrentCall
var missing *ai.Call
session := callflow.NewMemorySession(bytes.Repeat([]byte{1, 0}, 1600))
_, err := RunApprovedCall(context.Background(), ApprovedExecution{AI: ai.CurrentBound{Mode: "asr_only"}, MaxCallDuration: time.Second}, session, func(context.Context) error { return nil }, missing)
_, err := RunApprovedCall(context.Background(), ApprovedExecution{AI: ai.Binding{Mode: "asr_only"}, MaxCallDuration: time.Second}, session, func(context.Context) error { return nil }, missing)
if err == nil || session.Stats().ReceivedPackets != 0 {
t.Fatalf("missing pipeline was allowed to consume call media: err=%v stats=%+v", err, session.Stats())
}
var missingMedia *callflow.MemorySession
_, err = RunApprovedCall(context.Background(), ApprovedExecution{AI: ai.CurrentBound{Mode: "asr_only"}, MaxCallDuration: time.Second}, missingMedia, func(context.Context) error { return nil }, &ai.CurrentCall{})
_, err = RunApprovedCall(context.Background(), ApprovedExecution{AI: ai.Binding{Mode: "asr_only"}, MaxCallDuration: time.Second}, missingMedia, func(context.Context) error { return nil }, &ai.Call{})
if err == nil {
t.Fatal("typed-nil media session was admitted")
}
@@ -31,7 +31,7 @@ func TestRecordingDeliveryRealMutualTLSAndLocalOSSCommitsOneSQLiteResult(t *test
if err != nil {
t.Fatal(err)
}
bound := ai.CurrentBound{Mode: "asr_only", Conversation: ai.CurrentConversation{SilenceTimeout: 100 * time.Millisecond, MaxDuration: time.Second}}
bound := ai.Binding{Mode: "asr_only", Conversation: ai.ConversationConfig{SilenceTimeout: 100 * time.Millisecond, MaxDuration: time.Second}}
hangup := func(context.Context) error { return nil }
mock, err := ai.NewApprovedMockPipeline(bound, ai.ApprovedMockScript{Turns: []ai.ApprovedMockTurn{{Transcript: "隔离 Mock 最终识别文本"}}}, hangup)
if err != nil {