Files
go-sip/internal/callflow/result_payload_test.go
T

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)
}
}