Files
go-sip/internal/dispatcher/task_queue_v3_origination_test.go
T

167 lines
6.3 KiB
Go

package dispatcher
import (
"context"
"encoding/json"
"testing"
"time"
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
"google.golang.org/grpc"
)
type fakeAuthorizedAgentClient struct {
agentpb.AgentControlServiceClient
attempts int
mockOrigins int
deadlineClosed bool
last *agentpb.ExecuteAuthorizedRequest
}
func (f *fakeAuthorizedAgentClient) ExecuteAuthorized(_ context.Context, request *agentpb.ExecuteAuthorizedRequest, _ ...grpc.CallOption) (*agentpb.ExecuteAuthorizedResponse, error) {
f.attempts++
f.last = request
if f.deadlineClosed {
return &agentpb.ExecuteAuthorizedResponse{
Receipt: &agentpb.OperationReceipt{Result: agentpb.ResultCode_RESULT_CODE_REJECTED},
State: agentpb.ExecutionState_EXECUTION_STATE_TERMINAL,
}, nil
}
f.mockOrigins++
return &agentpb.ExecuteAuthorizedResponse{
Receipt: &agentpb.OperationReceipt{Result: agentpb.ResultCode_RESULT_CODE_APPLIED},
State: agentpb.ExecutionState_EXECUTION_STATE_TERMINAL,
}, nil
}
func (f *fakeAuthorizedAgentClient) QueryExecution(_ context.Context, request *agentpb.QueryExecutionRequest, _ ...grpc.CallOption) (*agentpb.QueryExecutionResponse, error) {
binding := request.Binding
if f.last != nil && f.last.Binding.GetExecutionId() == request.Binding.GetExecutionId() {
binding = f.last.Binding
}
callState := "mock_no_answer"
if f.deadlineClosed {
callState = "mock_deadline_closed_without_dial"
}
return &agentpb.QueryExecutionResponse{Snapshot: &agentpb.ExecutionSnapshot{
Binding: binding, State: agentpb.ExecutionState_EXECUTION_STATE_TERMINAL,
CallState: callState, AttemptId: binding.AttemptId,
ObservedAtUnixMs: time.Date(2026, 9, 21, 1, 30, 1, 0, time.UTC).UnixMilli(),
}}, nil
}
func TestTaskConsumerIssuesOnlyOneAuthorizedMockInstruction(t *testing.T) {
at := time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)
client := &fakeAuthorizedAgentClient{}
coordinator := NewAgentCoordinator(func() time.Time { return at })
if err := coordinator.Register("agent-1", client); err != nil {
t.Fatal(err)
}
coordinator.sessions["agent-1"] = AgentSession{AgentID: "agent-1", CellID: "cell-1", BootID: "boot-1", DispatcherEpoch: "epoch-1", SessionGeneration: 1}
route := "d." + localTestDispatcherID + ".task." + localTestTaskID + ".in"
for _, tc := range []struct {
name string
refused bool
agentDeadline bool
wantInstructions int
wantMockOrigins int
}{
{"approved call", false, false, 1, 1},
{"refused call", true, false, 0, 0},
{"Agent deadline closed without dialing", false, true, 1, 0},
} {
t.Run(tc.name, func(t *testing.T) {
client.deadlineClosed = tc.agentDeadline
d, st, configServer := newLocalV01TestDispatcher(t, at)
defer configServer.Close()
defer st.Close()
broker := &fakeQueueBroker{}
queues := newTaskQueueController(d, broker, nil)
if err := queues.EnableMockAuthorizedOrigination("agent-1", coordinator); err != nil {
t.Fatal(err)
}
assignment, err := st.LocalTaskAssignment(localTestDispatcherID, localTestTaskID)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err := queues.StartTask(ctx, assignment); err != nil || broker.handler == nil {
t.Fatalf("task consumer: handler=%v err=%v", broker.handler != nil, err)
}
body := localExecuteCommandBody(t, "command-queue-authorized", at.Add(2*time.Hour))
if tc.refused {
if err := d.AcceptLocalV01Command(ctx, route, body); err != nil {
t.Fatal(err)
}
d.now = func() time.Time { return at.Add(90 * time.Minute) } // Shanghai 11:00, right-open.
}
before := client.attempts
beforeOrigins := client.mockOrigins
if err := broker.handler(ctx, route, body); err != nil {
t.Fatalf("accepted command must ACK after durable final decision: %v", err)
}
if client.attempts-before != tc.wantInstructions || client.mockOrigins-beforeOrigins != tc.wantMockOrigins {
t.Fatalf("Agent instructions/originations=%d/%d want=%d/%d", client.attempts-before, client.mockOrigins-beforeOrigins, tc.wantInstructions, tc.wantMockOrigins)
}
if tc.refused {
d.now = func() time.Time { return at.Add(time.Minute) }
}
if err := broker.handler(ctx, route, body); err != nil {
t.Fatalf("duplicate delivery must ACK: %v", err)
}
if client.attempts-before != tc.wantInstructions || client.mockOrigins-beforeOrigins != tc.wantMockOrigins {
t.Fatal("duplicate message retried or revived an Agent originate")
}
var decision string
if err := st.DB().QueryRow(`SELECT decision FROM local_v02_origination_decisions LIMIT 1`).Scan(&decision); err != nil {
t.Fatal(err)
}
wantDecision := "issued"
if tc.refused {
wantDecision = "refused"
}
if decision != wantDecision {
t.Fatalf("decision=%q want=%q", decision, wantDecision)
}
var taskStatus, reservationState string
if err := st.DB().QueryRow(`SELECT t.status,r.state FROM tasks t JOIN reservations r ON r.execution_id=t.execution_id WHERE t.task_item_id='command-queue-authorized'`).Scan(&taskStatus, &reservationState); err != nil || taskStatus != "finished" || reservationState != "released" {
t.Fatalf("ended or refused Mock execution held quota: task=%q reservation=%q err=%v", taskStatus, reservationState, err)
}
messages, err := st.ClaimOutbox(10)
if err != nil {
t.Fatal(err)
}
results := 0
for _, message := range messages {
var event struct {
EventType string `json:"event_type"`
Payload struct {
Outcome string `json:"outcome"`
Recording struct {
Status string `json:"status"`
ReasonCode string `json:"reason_code"`
} `json:"recording"`
} `json:"payload"`
}
if err := json.Unmarshal(message.Body, &event); err != nil {
t.Fatal(err)
}
if event.EventType == "call.result" {
results++
wantOutcome, wantReason := "no_answer", "no_answer"
if tc.refused || tc.agentDeadline {
wantOutcome, wantReason = "failed", "not_attempted"
}
if event.Payload.Outcome != wantOutcome || event.Payload.Recording.Status != "not_created" || event.Payload.Recording.ReasonCode != wantReason {
t.Fatalf("incorrect Mock final outcome: %+v", event.Payload)
}
}
}
if results != 1 {
t.Fatalf("one accepted execution produced %d final results", results)
}
})
}
}