117 lines
5.0 KiB
Go
117 lines
5.0 KiB
Go
package callflow
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/ai"
|
|
"git.ipao.vip/rogee/go-sip/internal/contract"
|
|
)
|
|
|
|
func approvedResultFixture() (FinalCallFacts, Result) {
|
|
start := time.Date(2026, 9, 21, 1, 0, 0, 0, time.UTC)
|
|
facts := FinalCallFacts{
|
|
TaskID: "task-asr", CallerProfileID: "caller-profile-mock", Callee: "15003164745", TrunkID: "trunk-mock",
|
|
StartedAt: start, EndedAt: start.Add(3 * time.Second), Outcome: "answered", ReasonMessage: "completed in isolated mock",
|
|
}
|
|
result := Result{
|
|
Turns: []ai.TurnResult{
|
|
{Transcript: "用户真实识别文本", Reply: "未实际播放的助手候选内容"},
|
|
{Transcript: "请不要联系", EndedByKeyword: true},
|
|
},
|
|
CaptureWindows: []CaptureWindow{
|
|
{StartedAt: start.Add(100 * time.Millisecond), EndedAt: start.Add(1200 * time.Millisecond)},
|
|
{StartedAt: start.Add(1600 * time.Millisecond), EndedAt: start.Add(2250 * time.Millisecond)},
|
|
},
|
|
}
|
|
return facts, result
|
|
}
|
|
|
|
func TestFinalResultPayloadUsesOnlyObservedUserASRAndMediaTimes(t *testing.T) {
|
|
facts, result := approvedResultFixture()
|
|
payload, err := FinalResultPayload(facts, result)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
message, err := json.Marshal(map[string]any{
|
|
"event_id": "call-result-test", "event_type": "call.execute.result",
|
|
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", "tenant_id": 1001,
|
|
"issued_at": "2026-09-21T01:00:04Z", "payload": json.RawMessage(payload),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := contract.ValidateCurrent("mq", message); err != nil {
|
|
t.Fatalf("locally produced result violates the sole current MQ schema: %v", err)
|
|
}
|
|
var parsed struct {
|
|
TaskID string `json:"task_id"`
|
|
CallerProfileID string `json:"caller_profile_id"`
|
|
DurationMS int64 `json:"duration_ms"`
|
|
Outcome string `json:"outcome"`
|
|
ReasonCode *int `json:"reason_code"`
|
|
OptOut bool `json:"opt_out"`
|
|
Recording map[string]any `json:"recording"`
|
|
Transcript []struct {
|
|
SegmentID string `json:"segment_id"`
|
|
TurnID string `json:"turn_id"`
|
|
Role string `json:"role"`
|
|
StartMS int64 `json:"start_ms"`
|
|
EndMS int64 `json:"end_ms"`
|
|
Text string `json:"text"`
|
|
} `json:"transcript"`
|
|
}
|
|
if err := json.Unmarshal(payload, &parsed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if parsed.TaskID != facts.TaskID || parsed.CallerProfileID != facts.CallerProfileID || parsed.DurationMS != 3000 || parsed.Outcome != "answered" || parsed.ReasonCode != nil || !parsed.OptOut || len(parsed.Recording) != 0 || len(parsed.Transcript) != 2 {
|
|
t.Fatalf("result changed approved facts or invented a recording: %+v", parsed)
|
|
}
|
|
if first, second := parsed.Transcript[0], parsed.Transcript[1]; first.SegmentID != "segment-1" || first.TurnID != "turn-1" || first.Role != "user" || first.StartMS != 100 || first.EndMS != 1200 || first.Text != result.Turns[0].Transcript || second.SegmentID != "segment-2" || second.TurnID != "turn-2" || second.StartMS != 1600 || second.EndMS != 2250 || second.Text != result.Turns[1].Transcript {
|
|
t.Fatalf("ASR text or observed media timing was lost: %+v", parsed.Transcript)
|
|
}
|
|
}
|
|
|
|
func TestFinalResultPayloadRejectsMissingOrInventedCallFacts(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
change func(*FinalCallFacts, *Result)
|
|
}{
|
|
{"missing_approved_caller", func(f *FinalCallFacts, _ *Result) { f.CallerProfileID = "" }},
|
|
{"unapproved_outcome", func(f *FinalCallFacts, _ *Result) { f.Outcome = "new_state" }},
|
|
{"no_sip_reason", func(f *FinalCallFacts, _ *Result) { f.ReasonMessage = "" }},
|
|
{"ended_before_started", func(f *FinalCallFacts, _ *Result) { f.EndedAt = f.StartedAt.Add(-time.Second) }},
|
|
{"missing_media_window", func(_ *FinalCallFacts, r *Result) { r.CaptureWindows = nil }},
|
|
{"reversed_media_window", func(_ *FinalCallFacts, r *Result) {
|
|
r.CaptureWindows[0].EndedAt = r.CaptureWindows[0].StartedAt.Add(-time.Millisecond)
|
|
}},
|
|
{"window_outside_confirmed_call", func(f *FinalCallFacts, r *Result) { r.CaptureWindows[0].EndedAt = f.EndedAt.Add(time.Millisecond) }},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
facts, result := approvedResultFixture()
|
|
tc.change(&facts, &result)
|
|
if payload, err := FinalResultPayload(facts, result); err == nil || len(payload) != 0 {
|
|
t.Fatalf("unconfirmed fact became a final result: payload_size=%d err=%v", len(payload), err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFinalResultPayloadNoAnswerHasNoFabricatedTranscriptOrAsset(t *testing.T) {
|
|
facts, _ := approvedResultFixture()
|
|
facts.Outcome, facts.ReasonMessage = "no_answer", "SIP no answer"
|
|
payload, err := FinalResultPayload(facts, Result{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var result struct {
|
|
Transcript []any `json:"transcript"`
|
|
OptOut bool `json:"opt_out"`
|
|
Recording map[string]any `json:"recording"`
|
|
}
|
|
if err := json.Unmarshal(payload, &result); err != nil || len(result.Transcript) != 0 || result.OptOut || len(result.Recording) != 0 {
|
|
t.Fatalf("no-answer outcome invented media facts: %+v err=%v", result, err)
|
|
}
|
|
}
|