Files
go-sip/internal/rpc/approved_worker_test.go
T

221 lines
8.5 KiB
Go

package rpc
import (
"context"
"errors"
"strings"
"sync/atomic"
"testing"
"time"
"git.ipao.vip/rogee/go-sip/internal/agent"
)
func workerTestExecution(now time.Time) ApprovedExecution {
return ApprovedExecution{DispatcherID: "c046b893-8628-4589-ae50-619d049248a6", TenantID: 42,
TaskID: "task-a", SourceEventID: "event-a", CallID: "call-a",
DialBefore: now.Add(time.Minute), MaxCallDuration: time.Minute}
}
func prepareWorkerRun(run func(context.Context, ApprovedExecution) error) func(ApprovedExecution) (func(context.Context) error, error) {
return func(call ApprovedExecution) (func(context.Context) error, error) {
return func(ctx context.Context) error { return run(ctx, call) }, nil
}
}
func TestApprovedCallWorkerOutlivesInitiatingUnaryAndWaitsForPhysicalEnd(t *testing.T) {
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
process, stopProcess := context.WithCancel(context.Background())
defer stopProcess()
request, closeRequest := context.WithCancel(context.Background())
defer closeRequest()
calls := &agent.TaskCalls{}
started := make(chan context.Context, 1)
finish := make(chan struct{})
defer func() {
select {
case <-finish:
default:
close(finish)
}
}()
failures := make(chan error, 1)
worker := ApprovedCallWorker{Lifecycle: process, Calls: calls, Now: func() time.Time { return now },
Prepare: prepareWorkerRun(func(ctx context.Context, _ ApprovedExecution) error {
started <- ctx
<-ctx.Done()
<-finish // simulated hangup must finish before TaskCalls releases the call
return nil
}),
OnFailure: func(_ ApprovedExecution, err error) error { failures <- err; return nil },
}
approved := workerTestExecution(now)
if err := worker.Originate(request, approved); err != nil {
t.Fatal(err)
}
var callContext context.Context
select {
case callContext = <-started:
case <-time.After(2 * time.Second):
t.Fatal("accepted call did not start")
}
closeRequest() // the unary response must not cancel the active call
if callContext.Err() != nil {
t.Fatal("call inherited the completed unary request deadline")
}
task := agent.TaskIdentity{DispatcherID: approved.DispatcherID, TenantID: approved.TenantID, TaskID: approved.TaskID}
if _, err := calls.Register(task, approved.CallID, func() {}); err == nil {
t.Fatal("accepted call was not registered before the RPC returned")
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
controlDone := make(chan error, 1)
go func() { controlDone <- calls.Apply(ctx, task, "pause", "hangup") }()
select {
case <-callContext.Done():
case <-ctx.Done():
t.Fatal("Agent control did not cancel the active call")
}
select {
case err := <-controlDone:
t.Fatalf("control was acknowledged before physical hangup finished: %v", err)
default:
}
close(finish)
if err := <-controlDone; err != nil {
t.Fatalf("control did not wait for the Agent runner: %v", err)
}
select {
case err := <-failures:
t.Fatalf("successful cancellation generated a false failure: %v", err)
default:
}
}
func TestApprovedCallWorkerRejectsExpiredOrClosedTaskBeforeIssuing(t *testing.T) {
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
process, stopProcess := context.WithCancel(context.Background())
defer stopProcess()
calls := &agent.TaskCalls{}
var starts atomic.Int32
worker := ApprovedCallWorker{Lifecycle: process, Calls: calls, Now: func() time.Time { return now },
Prepare: prepareWorkerRun(func(context.Context, ApprovedExecution) error { starts.Add(1); return nil }),
OnFailure: func(ApprovedExecution, error) error { return nil },
}
approved := workerTestExecution(now)
approved.DialBefore = now.Add(-time.Millisecond)
if err := worker.Originate(context.Background(), approved); !errors.Is(err, ErrApprovedDialExpired) {
t.Fatalf("expired authorization was admitted: %v", err)
}
approved.DialBefore = now.Add(time.Minute)
task := agent.TaskIdentity{DispatcherID: approved.DispatcherID, TenantID: approved.TenantID, TaskID: approved.TaskID}
if err := calls.Apply(context.Background(), task, "pause", "hangup"); err != nil {
t.Fatal(err)
}
if err := worker.Originate(context.Background(), approved); !errors.Is(err, agent.ErrTaskAdmissionClosed) {
t.Fatalf("paused task was admitted: %v", err)
}
if starts.Load() != 0 {
t.Fatal("Agent issued a call despite expired authorization or task pause")
}
worker.OnFailure = nil
if err := worker.Originate(context.Background(), approved); err == nil || !strings.Contains(err.Error(), "failure") {
t.Fatalf("missing failure observability was accepted: %v", err)
}
}
func TestApprovedCallWorkerProcessStopCancelsAndReportsFailureOnce(t *testing.T) {
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
process, stopProcess := context.WithCancel(context.Background())
defer stopProcess()
calls := &agent.TaskCalls{}
started := make(chan struct{})
reported := make(chan error, 1)
var attempts atomic.Int32
worker := ApprovedCallWorker{Lifecycle: process, Calls: calls, Now: func() time.Time { return now },
Prepare: prepareWorkerRun(func(ctx context.Context, _ ApprovedExecution) error {
attempts.Add(1)
close(started)
<-ctx.Done()
return ctx.Err()
}),
OnFailure: func(_ ApprovedExecution, err error) error { reported <- err; return nil },
}
approved := workerTestExecution(now)
if err := worker.Originate(context.Background(), approved); err != nil {
t.Fatal(err)
}
select {
case <-started:
case <-time.After(2 * time.Second):
t.Fatal("accepted worker did not start")
}
stopProcess()
select {
case err := <-reported:
if !errors.Is(err, context.Canceled) {
t.Fatalf("lost process-shutdown failure reason: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("worker failed silently after process shutdown")
}
task := agent.TaskIdentity{DispatcherID: approved.DispatcherID, TenantID: approved.TenantID, TaskID: approved.TaskID}
if err := calls.Apply(context.Background(), task, "stop", "drain"); err != nil {
t.Fatalf("failed worker was not released: %v", err)
}
if attempts.Load() != 1 {
t.Fatalf("worker was replayed after an unknown outcome: %d", attempts.Load())
}
}
func TestApprovedCallWorkerRefusesExpiredAuthorizationBeforeDispatchAck(t *testing.T) {
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
process, stopProcess := context.WithCancel(context.Background())
defer stopProcess()
calls := &agent.TaskCalls{}
var checks, starts, reports atomic.Int32
worker := ApprovedCallWorker{Lifecycle: process, Calls: calls,
Now: func() time.Time {
if checks.Add(1) == 1 {
return now // approved when entering the worker
}
return now.Add(2 * time.Minute) // expired when the scheduled worker begins
},
Prepare: prepareWorkerRun(func(context.Context, ApprovedExecution) error { starts.Add(1); return nil }),
OnFailure: func(ApprovedExecution, error) error { reports.Add(1); return nil },
}
approved := workerTestExecution(now)
if err := worker.Originate(context.Background(), approved); !errors.Is(err, ErrApprovedDialExpired) {
t.Fatalf("expired scheduled call received an execution ACK: %v", err)
}
if starts.Load() != 0 || reports.Load() != 0 {
t.Fatal("no-dial pre-dispatch expiry started work or fabricated a call result")
}
task := agent.TaskIdentity{DispatcherID: approved.DispatcherID, TenantID: approved.TenantID, TaskID: approved.TaskID}
if err := calls.Apply(context.Background(), task, "stop", "drain"); err != nil {
t.Fatalf("refused call remained registered: %v", err)
}
}
func TestApprovedCallWorkerPreparationRejectsBeforeDispatchAck(t *testing.T) {
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
calls := &agent.TaskCalls{}
cause := errors.New("synthetic Mock script does not match approved AI")
var reports atomic.Int32
worker := ApprovedCallWorker{Lifecycle: context.Background(), Calls: calls, Now: func() time.Time { return now },
Prepare: func(ApprovedExecution) (func(context.Context) error, error) { return nil, cause },
OnFailure: func(ApprovedExecution, error) error { reports.Add(1); return nil },
}
approved := workerTestExecution(now)
if err := worker.Originate(context.Background(), approved); !errors.Is(err, ErrApprovedCallPreparation) || !errors.Is(err, cause) {
t.Fatalf("invalid per-call Mock parameters were acknowledged: %v", err)
}
if reports.Load() != 0 {
t.Fatal("no-dial preparation error fabricated a completed call")
}
task := agent.TaskIdentity{DispatcherID: approved.DispatcherID, TenantID: approved.TenantID, TaskID: approved.TaskID}
if err := calls.Apply(context.Background(), task, "stop", "drain"); err != nil {
t.Fatalf("failed preparation registered a call: %v", err)
}
}