Add isolated SIP call tool, optional debug capture and split interface schemas

This commit is contained in:
2026-10-08 12:44:45 +08:00
parent 98b3fd469d
commit 385bda0822
49 changed files with 3688 additions and 28 deletions
+11
View File
@@ -44,10 +44,21 @@ func (l Loader) ariOptions() (*native.Options, error) {
// OpenARI proves that the native server accepts this Agent credential without
// creating a channel or dialing. The caller owns and must close the client.
func (l Loader) OpenARI() (ari.Client, error) {
return l.openARIApplication("go-sip-agent")
}
// OpenLineTestARI owns separate subscriptions and never steals the business
// application's WebSocket. It does not originate a channel.
func (l Loader) OpenLineTestARI() (ari.Client, error) {
return l.openARIApplication("sip-call")
}
func (l Loader) openARIApplication(application string) (ari.Client, error) {
options, err := l.ariOptions()
if err != nil {
return nil, err
}
options.Application = application
client, err := native.Connect(options)
if err != nil {
return nil, fmt.Errorf("connect native Agent ARI: %T", err)
+28 -7
View File
@@ -17,8 +17,9 @@ import (
"github.com/CyCoreSystems/ari/v5/client/native"
)
// NativeDial contains only the Dispatcher-selected SIP identity and the
// Agent's explicitly configured SLIN16 RTP payload profile.
// NativeDial contains explicit SIP identity and the business Agent's SLIN16
// profile. Business calls use Dispatcher-selected values; isolated line tests
// use their declared ENV identity and omit media resources.
type NativeDial struct {
ExecutionID, TrunkID, DialedCallee, CallerID string
AnswerTimeout time.Duration
@@ -118,8 +119,13 @@ func (l Loader) Originate(ctx context.Context, request NativeDial) (*NativeCall,
return dialWithClient(ctx, client, request)
}
func dialWithClient(ctx context.Context, client ari.Client, request NativeDial) (_ *NativeCall, err error) {
call := &NativeCall{client: client}
func dialWithClient(ctx context.Context, client ari.Client, request NativeDial) (*NativeCall, error) {
return dialWithOptions(ctx, client, request, false)
}
// lineTest omits media/AI resources and uses a distinct ARI application.
func dialWithOptions(ctx context.Context, client ari.Client, request NativeDial, lineTest bool) (_ *NativeCall, err error) {
call := &NativeCall{client: client, verifyEnd: lineTest}
defer func() {
if err != nil {
cleanupErr := call.Close()
@@ -141,9 +147,13 @@ func dialWithClient(ctx context.Context, client ari.Client, request NativeDial)
if request.BindSIPCall == nil {
return nil, &NativeCallFailure{Phase: "validate", Cause: errors.New("HEP SIP call binding is required before native Dial")}
}
call.Media, err = media.ListenRTPWithFormat("127.0.0.1:0", request.MediaPayloadType, media.FormatSLIN16, 16000)
if err != nil {
return nil, &NativeCallFailure{Phase: "rtp_listen", Cause: err}
if lineTest {
originate.App = "sip-call"
} else {
call.Media, err = media.ListenRTPWithFormat("127.0.0.1:0", request.MediaPayloadType, media.FormatSLIN16, 16000)
if err != nil {
return nil, &NativeCallFailure{Phase: "rtp_listen", Cause: err}
}
}
key := ari.NewKey(ari.ChannelKey, request.ExecutionID)
call.subscription = client.Bus().Subscribe(key, "StasisStart", "ChannelStateChange", "StasisEnd", "ChannelHangupRequest", "ChannelDestroyed")
@@ -179,6 +189,11 @@ func dialWithClient(ctx context.Context, client ari.Client, request NativeDial)
call.verifyEnd = true // Created channel has no ExternalMedia or bridge yet.
return nil, &NativeCallFailure{Phase: "answer_wait", Cause: err}
}
if lineTest {
call.monitorDone = make(chan struct{})
go call.watch(request.ExecutionID)
return call, nil
}
bridgeKey := ari.NewKey(ari.BridgeKey, request.ExecutionID+"-bridge")
call.bridge, err = client.Bridge().Create(bridgeKey, "mixing", "go-sip-agent")
if err != nil {
@@ -254,6 +269,12 @@ func (c *NativeCall) watch(channelID string) {
}
}
// EndConfirmed reports an observed ChannelDestroyed or post-Hangup ARI 404.
// An accepted Hangup request alone is not evidence of termination.
func (c *NativeCall) EndConfirmed() bool {
return c != nil && (c.destroyed.Load() || c.confirmedEnd)
}
// Close releases each resource once. Errors remain visible to the caller; a
// failed Hangup never silently turns an uncertain call into a success.
func (c *NativeCall) Close() error {
+13
View File
@@ -0,0 +1,13 @@
package asterisk
import (
"context"
"github.com/CyCoreSystems/ari/v5"
)
// DialLineTest is the independent sip-call application's single native Dial.
// The caller owns host-idle checks, temporary SIP configuration and optional
// capture. No ExternalMedia, bridge, AI session or recording is created.
func DialLineTest(ctx context.Context, client ari.Client, request NativeDial) (*NativeCall, error) {
return dialWithOptions(ctx, client, request, true)
}
+42
View File
@@ -0,0 +1,42 @@
package asterisk
import (
"context"
"github.com/CyCoreSystems/ari/v5"
"testing"
"time"
)
func TestLineTestUsesIsolatedApplicationAndNoMediaOrAI(t *testing.T) {
events := make(chan ari.Event, 3)
channels := &testChannels{events: events, dataErr: nativeHTTPStatusError{code: 404}}
client := &testARIClient{channels: channels, bridges: &testBridges{}, bus: &testBus{sub: answerEvents{events: events}}}
call, err := dialWithOptions(context.Background(), client, NativeDial{ExecutionID: "siptest-1", TrunkID: "siptest-shuqi", DialedCallee: "708918601010101", CallerID: "BD123", AnswerTimeout: time.Second, BindSIPCall: testBindSIPCall}, true)
if err != nil {
t.Fatal(err)
}
if channels.issued != 1 || channels.create.App != "sip-call" || call.Media != nil || len(client.bridges.attached) != 0 || channels.media.App != "" {
t.Fatal("line test must send one Dial without ExternalMedia, bridge, or business application")
}
if err = call.Close(); err != nil {
t.Fatal(err)
}
if !call.EndConfirmed() {
t.Fatal("teardown must be confirmed")
}
}
func TestLineTestDoesNotInventConfirmedTermination(t *testing.T) {
events := make(chan ari.Event, 3)
client := &testARIClient{channels: &testChannels{events: events}, bridges: &testBridges{}, bus: &testBus{sub: answerEvents{events: events}}}
call, err := dialWithOptions(context.Background(), client, NativeDial{ExecutionID: "siptest-1", TrunkID: "siptest-shuqi", DialedCallee: "708918601010101", CallerID: "BD123", AnswerTimeout: time.Second, BindSIPCall: testBindSIPCall}, true)
if err != nil {
t.Fatal(err)
}
if err = call.Close(); err != nil {
t.Fatal(err)
}
if call.EndConfirmed() {
t.Fatal("successful Hangup HTTP is not proof that the channel disappeared")
}
}