167 lines
6.3 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|