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