report confirmed real calls whose media setup fails

This commit is contained in:
2026-10-04 21:23:32 +08:00
parent 7b3d2b577b
commit 08f827ecdf
2 changed files with 69 additions and 6 deletions
+28 -6
View File
@@ -99,21 +99,43 @@ func (r *ApprovedRecordedRealCall) Prepare(approved ApprovedExecution) (func(con
if err != nil {
return err // unknown origination is never retried or reported as a completed call
}
if call.ctx == nil || call.session == nil || call.close == nil {
if call.close != nil {
return errors.Join(errors.New("real call media unavailable"), call.close())
startedAt := time.Now().UTC() // Originate returns only after the actual channel entered Stasis.
// A real channel may have answered even if media/AI setup subsequently
// fails. Report a failed call only after its hangup is confirmed; never
// keep a known-ended call occupying capacity or invent a recording.
reportSetupFailure := func(reason string, cause error) error {
if call.close == nil {
return errors.Join(cause, errors.New("real call has no hangup function; outcome unknown"))
}
if err := call.close(); err != nil {
return errors.Join(cause, err)
}
payload, err := callflow.FinalResultPayload(callflow.FinalCallFacts{
TaskID: approved.TaskID, CallerProfileID: approved.CallerProfileID,
Callee: approved.Callee, TrunkID: approved.SelectedTrunkID,
StartedAt: startedAt, EndedAt: time.Now().UTC(), Outcome: "failed", ReasonMessage: reason,
}, callflow.Result{})
if err != nil {
return errors.Join(cause, err)
}
reportCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), r.ReportTimeout)
defer cancel()
return errors.Join(cause, r.Delivery.Complete(reportCtx, agent.CompletedRecording{ResultPayload: payload}))
}
if call.ctx == nil || call.session == nil {
return reportSetupFailure("real media was unavailable", errors.New("real call media unavailable"))
}
if call.close == nil {
return errors.New("real call has no hangup function; outcome unknown")
}
capture, err := callflow.NewRecordingSession(call.session, r.MaxWAVBytes)
if err != nil {
return errors.Join(err, call.close())
return reportSetupFailure("real recording setup failed", err)
}
startedAt := time.Now().UTC() // Originate returns only after the actual channel entered Stasis.
hangup := func(context.Context) error { return call.close() }
pipeline, err := ai.NewCall(approved.AI, hangup)
if err != nil {
return errors.Join(err, call.close())
return reportSetupFailure("real AI instance setup failed", err)
}
observed, runErr := RunApprovedCall(call.ctx, approved, capture, hangup, pipeline)
if err := call.close(); err != nil {
@@ -78,6 +78,47 @@ func TestApprovedRecordedRealCallReportsOnlyEndedObservedCall(t *testing.T) {
}
}
func TestApprovedRecordedRealCallReportsConfirmedMediaSetupFailure(t *testing.T) {
fixture, stub, puts, _, approved := recordedMockFixture(t, nil, time.Second)
approved.CallerID, approved.DialedCallee, approved.RingTimeout = "BD93205882", "7089"+approved.Callee, time.Second
var originate, hangup int
runner := &ApprovedRecordedRealCall{
MaxWAVBytes: 4096, MediaPayloadType: 118, ReportTimeout: time.Second,
Delivery: fixture.Delivery,
originator: func(ctx context.Context, _ asterisk.NativeDial) (realMedia, error) {
originate++
return realMedia{ctx: ctx, close: func() error { hangup++; return nil }}, nil // answered channel, but no RTP capture session
},
}
err := runner.Run(context.Background(), approved)
if err == nil || originate != 1 || hangup != 1 || strings.Join(stub.calls, ",") != "end,result" || puts.Load() != 0 {
t.Fatalf("confirmed ended call without RTP still needs true failed result and empty recording: err=%v originate=%d hangup=%d calls=%v puts=%d", err, originate, hangup, stub.calls, puts.Load())
}
var result struct {
Outcome string `json:"outcome"`
Recording map[string]any `json:"recording"`
ReasonMessage string `json:"reason_message"`
}
if err := json.Unmarshal(stub.result, &result); err != nil || result.Outcome != "failed" || len(result.Recording) != 0 || result.ReasonMessage == "" {
t.Fatalf("false success after media setup failure: %+v %v", result, err)
}
}
func TestApprovedRecordedRealCallMissingMediaWithUnknownHangupDoesNotReport(t *testing.T) {
fixture, stub, _, _, approved := recordedMockFixture(t, nil, time.Second)
approved.CallerID, approved.DialedCallee, approved.RingTimeout = "BD93205882", "7089"+approved.Callee, time.Second
runner := &ApprovedRecordedRealCall{
MaxWAVBytes: 4096, MediaPayloadType: 118, ReportTimeout: time.Second,
Delivery: fixture.Delivery,
originator: func(ctx context.Context, _ asterisk.NativeDial) (realMedia, error) {
return realMedia{ctx: ctx, close: func() error { return errors.New("hangup response missing") }}, nil
},
}
if err := runner.Run(context.Background(), approved); err == nil || len(stub.calls) != 0 {
t.Fatalf("ambiguous hangup cannot be reported as a confirmed end: err=%v calls=%v", err, stub.calls)
}
}
func TestApprovedRecordedRealCallDoesNotReportUnknownHangup(t *testing.T) {
fixture, stub, puts, _, approved := recordedMockFixture(t, nil, time.Second)
approved.CallerID, approved.DialedCallee, approved.RingTimeout = "BD93205882", "7089"+approved.Callee, time.Second