package main import ( "context" "errors" "io" "net" "os" "path/filepath" "testing" "time" agentpb "git.ipao.vip/rogee/go-sip/gen/agent" "git.ipao.vip/rogee/go-sip/internal/agent" "git.ipao.vip/rogee/go-sip/internal/asterisk" "git.ipao.vip/rogee/go-sip/internal/rpc" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) type definiteEndClient struct { agentpb.AgentControlServiceClient calls int fail error } func (c *definiteEndClient) ReportCallEnded(_ context.Context, req *agentpb.ReportCallEndedRequest, _ ...grpc.CallOption) (*agentpb.ReportCallEndedResponse, error) { c.calls++ if c.fail != nil { return nil, c.fail } return &agentpb.ReportCallEndedResponse{Receipt: &agentpb.OperationReceipt{Result: agentpb.ResultCode_RESULT_CODE_APPLIED, FactId: req.SourceEventId}}, nil } func TestOnlyDefinitelyUnissuedRealCallReportsConfirmedEnd(t *testing.T) { backend := &definiteEndClient{} client := agent.RecordingClient{Client: backend, DispatcherID: "dispatcher-test", TenantID: 1, SourceEventID: "call-pre-dial", Session: func(context.Context) (*agentpb.RequestMeta, error) { return &agentpb.RequestMeta{AgentId: "agent-test", CellId: "cell-test", BootId: "boot-test", DispatcherEpoch: "epoch-test", SessionGeneration: 1}, nil }} before := &asterisk.NativeCallFailure{Phase: "rtp_listen", Cause: errors.New("no RTP socket")} if err := reportDefiniteNonDialFailure(context.Background(), before, client); err != nil || backend.calls != 1 { t.Fatalf("definite pre-dial failure was not reported: calls=%d err=%v", backend.calls, err) } unknown := &asterisk.NativeCallFailure{Phase: "originate", Cause: errors.New("response lost")} if err := reportDefiniteNonDialFailure(context.Background(), unknown, client); err != nil || backend.calls != 1 { t.Fatalf("unknown ARI outcome must stay occupied: calls=%d err=%v", backend.calls, err) } backend.fail = errors.New("no authenticated end receipt") if err := reportDefiniteNonDialFailure(context.Background(), before, client); err == nil { t.Fatal("failed end reporting must not pretend capacity was released") } } func TestRealAgentCommandStartsPinnedServerWithoutMockFixturesOrDial(t *testing.T) { ca, agentCert, agentKey, dispatcherCert, dispatcherKey, dispatcherLeaf := localCommandCertificates(t) settings, _ := currentAgentSetupFixture(t) for path, data := range map[string][]byte{settings.CAFile: ca, settings.CertFile: agentCert, settings.KeyFile: agentKey} { if err := os.WriteFile(path, data, 0600); err != nil { t.Fatal(err) } } listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } listen := listener.Addr().String() listener.Close() hepPort, err := net.ListenPacket("udp4", "127.0.0.1:0") if err != nil { t.Fatal(err) } hepListen := hepPort.LocalAddr().String() hepPort.Close() for name, value := range map[string]string{ "AGENT_ID": settings.AgentID, "CELL_ID": settings.CellID, "AGENT_GRPC_LISTEN": listen, "AGENT_SESSION_PATH": settings.SessionPath, "AGENT_RECOVERY_ROOT": settings.RecoveryRoot, "DISPATCHER_ID": settings.DispatcherID, "DISPATCHER_GRPC_ENDPOINT": settings.DispatcherEndpoint, "DISPATCHER_GRPC_SERVER_NAME": settings.DispatcherServerName, "MTLS_CA_FILE": settings.CAFile, "MTLS_CERT_FILE": settings.CertFile, "MTLS_KEY_FILE": settings.KeyFile, "MTLS_PEER_CERT_FINGERPRINTS": rpc.CertificateFingerprint(dispatcherLeaf), "ASTERISK_CONFIG_DIR": filepath.Join(settings.RecoveryRoot, "asterisk"), "ASTERISK_BIN": "/usr/sbin/asterisk", "ASTERISK_LIBRARY_DIR": "/usr/lib/asterisk/modules", "AGENT_EVIDENCE_ROOT": filepath.Join(settings.RecoveryRoot, "evidence"), "AGENT_OSS_ALLOWED_HOST": "bucket.oss-cn-beijing.aliyuncs.com", "AGENT_HEP_LISTEN_ADDR": hepListen, "AGENT_MOCK_SCENARIO_FILE": "", "AGENT_MOCK_APPLIED_SIP_FILE": "", } { t.Setenv(name, value) } ctx, cancel := context.WithCancel(context.Background()) cmd := newAgentCommand() cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetContext(ctx) cmd.SetArgs([]string{"--mode", "nonprod-real"}) finished := make(chan error, 1) go func() { finished <- cmd.Execute() }() t.Cleanup(func() { cancel() select { case err := <-finished: if err != nil { t.Errorf("real Agent shutdown failed: %v", err) } case <-time.After(5 * time.Second): t.Error("real Agent did not stop after cancellation") } }) tlsConfig, err := rpc.NewClientTLSConfig(ca, dispatcherCert, dispatcherKey, "agent.local") if err != nil { t.Fatal(err) } dialCtx, stop := context.WithTimeout(context.Background(), 6*time.Second) defer stop() conn, err := grpc.DialContext(dialCtx, listen, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), grpc.WithBlock()) if err != nil { select { case startupErr := <-finished: t.Fatalf("nonprod-real startup failed: %v (dial: %v)", startupErr, err) default: t.Fatalf("nonprod-real pinned server unavailable: %v", err) } } defer conn.Close() status, err := agentpb.NewAgentControlServiceClient(conn).GetAgentStatus(dialCtx, &agentpb.GetAgentStatusRequest{ Meta: &agentpb.RequestMeta{ProtocolVersion: "agent.v1", RequestId: "status", TraceId: "status", OperationId: "status", AgentId: settings.AgentID, CellId: settings.CellID}, Target: &agentpb.AgentBinding{AgentId: settings.AgentID, CellId: settings.CellID}, }) if err != nil || status.GetStatus().GetAgentId() != settings.AgentID { t.Fatalf("nonprod-real Agent did not expose its pinned status: status=%v err=%v", status, err) } } func TestRealAgentServerRequiresBoundNativeSIPAndNoMock(t *testing.T) { settings, _ := currentAgentSetupFixture(t) settings.Mode = "nonprod-real" settings.AsteriskConfigDir = filepath.Join(settings.RecoveryRoot, "asterisk") settings.AsteriskBin = "/usr/sbin/asterisk" settings.AsteriskLibraryDir = "/usr/lib/asterisk/modules" settings.EvidenceRoot = filepath.Join(settings.RecoveryRoot, "evidence") settings.OSSAllowedHost = "bucket.oss-cn-beijing.aliyuncs.com" client := &isolatedAgentRecordingClient{} mirror := asterisk.NewHEPMirror() server, err := newRealAgentServer(context.Background(), settings, client, mirror) if err != nil || server == nil { t.Fatalf("real Agent must use the native SIP and non-Mock delivery boundary: %v", err) } if _, err := os.Lstat(settings.SessionPath); !os.IsNotExist(err) { t.Fatalf("server assembly must not touch the existing session: %v", err) } var typedNil *isolatedAgentRecordingClient var nilClient agentpb.AgentControlServiceClient = typedNil if server, err := newRealAgentServer(context.Background(), settings, nilClient, mirror); err == nil || server != nil { t.Fatal("typed-nil Dispatcher must block real Agent startup") } if server, err := newRealAgentServer(context.Background(), settings, client, nil); err == nil || server != nil { t.Fatal("real Agent must require an initialized SIP mirror") } settings.MockScenarioFile = "old-mock.json" if server, err := newRealAgentServer(context.Background(), settings, client, mirror); err == nil || server != nil { t.Fatal("real Agent must not accept isolated Mock fixtures") } }