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

137 lines
5.6 KiB
Go

package dispatcher
import (
"context"
"testing"
"time"
agentv1 "git.ipao.vip/rogee/go-sip/gen/agent/v1"
)
type mockActiveStopRecorder struct {
state agentv1.ExecutionState
calls []struct {
agentID string
executionID string
action agentv1.ControlAction
policy agentv1.ActiveCallPolicy
}
}
func (r *mockActiveStopRecorder) Control(_ context.Context, agentID string, binding *agentv1.ExecutionBinding, action agentv1.ControlAction, policy agentv1.ActiveCallPolicy) (*agentv1.ApplyTaskControlResponse, error) {
r.calls = append(r.calls, struct {
agentID string
executionID string
action agentv1.ControlAction
policy agentv1.ActiveCallPolicy
}{agentID, binding.ExecutionId, action, policy})
state := r.state
if state == agentv1.ExecutionState_EXECUTION_STATE_UNSPECIFIED {
state = agentv1.ExecutionState_EXECUTION_STATE_TERMINAL
}
return &agentv1.ApplyTaskControlResponse{
Receipt: &agentv1.OperationReceipt{Result: agentv1.ResultCode_RESULT_CODE_APPLIED},
AppliedTaskRevision: binding.TaskRevision + 1,
State: state,
}, nil
}
func TestAuthorizedMockInFlightExecutionsRemainAddressableForStop(t *testing.T) {
at := time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)
d, st, configServer := newLocalV01TestDispatcher(t, at)
defer configServer.Close()
defer st.Close()
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,
}
ctx := context.Background()
route := "d." + localTestDispatcherID + ".task." + localTestTaskID + ".in"
issued := make(map[string]bool, 2)
for _, commandID := range []string{"command-active-a", "command-active-b"} {
if err := d.AcceptLocalV01Command(ctx, route, localExecuteCommandBody(t, commandID, at.Add(2*time.Hour))); err != nil {
t.Fatalf("accept %s: %v", commandID, err)
}
var executionID string
if err := st.DB().QueryRow(`SELECT execution_id FROM tasks WHERE task_item_id=?`, commandID).Scan(&executionID); err != nil {
t.Fatal(err)
}
if _, err := d.DispatchAuthorizedLocal(ctx, executionID, "agent-1", coordinator); err != nil {
t.Fatalf("issue %s: %v", commandID, err)
}
issued[executionID] = true // D has not yet persisted either terminal observation.
}
if client.attempts != 2 {
t.Fatalf("Agent received %d instructions, want two", client.attempts)
}
targets, err := st.ActiveTaskExecutionControls(localTestTenantID, localTestTenantKey, localTestTaskID)
if err != nil || len(targets) != 2 {
t.Fatalf("stop cannot address both in-flight Agent calls: targets=%+v err=%v", targets, err)
}
for _, target := range targets {
if target.AgentID != "agent-1" || target.Binding == nil || !issued[target.Binding.ExecutionId] {
t.Fatalf("stop selected an unrelated or unbound call: %+v", target)
}
}
control := &mockActiveStopRecorder{state: agentv1.ExecutionState_EXECUTION_STATE_OBSERVED}
broker := &fakeQueueBroker{}
queues := newTaskQueueController(d, broker, control)
assignment, err := st.LocalTaskAssignment(localTestDispatcherID, localTestTaskID)
if err != nil {
t.Fatal(err)
}
if err := queues.ApplyActiveCallPolicy(ctx, assignment, "pause", "hangup"); err == nil {
t.Fatal("Agent applied receipt without terminal call state was accepted as hangup")
}
control.calls = nil
control.state = agentv1.ExecutionState_EXECUTION_STATE_TERMINAL
reader := &fakeTaskControlStatusReader{status: localControlStatus("stopped", 3)}
processor := newLocalTaskControlProcessor(d, reader, queues)
if err := processor.Handle(ctx, localControlRoutingKey(), localTaskControlBody(t, at, "stop")); err != nil {
t.Fatalf("persist stop, hang up both calls and drain the owned queue: %v", err)
}
if len(control.calls) != 2 || len(broker.drained) != 1 {
t.Fatalf("stop skipped an active call or the backlog: controls=%+v drained=%v", control.calls, broker.drained)
}
for _, call := range control.calls {
if call.agentID != "agent-1" || !issued[call.executionID] ||
call.action != agentv1.ControlAction_CONTROL_ACTION_STOP || call.policy != agentv1.ActiveCallPolicy_ACTIVE_CALL_POLICY_HANGUP {
t.Fatalf("stop sent an unbound control: %+v", call)
}
}
// Already-issued calls can still report their original terminal facts after
// the stop barrier; neither a new instruction nor a second PUT is allowed.
for _, target := range targets {
authorized, err := d.issuedMockAuthorization(target.ExecutionID)
if err != nil {
t.Fatal(err)
}
terminal := &agentv1.ExecutionSnapshot{
Binding: authorized.Binding, State: agentv1.ExecutionState_EXECUTION_STATE_TERMINAL,
CallState: "mock_no_answer", AttemptId: authorized.Binding.AttemptId,
ObservedAtUnixMs: at.Add(time.Second).UnixMilli(),
}
if err := d.completeAuthorizedMockTerminal(authorized, terminal); err != nil {
t.Fatalf("stopped task discarded an existing call result: %v", err)
}
}
if client.attempts != 2 {
t.Fatalf("stop redialed an existing call: %d attempts", client.attempts)
}
var results, held int
if err := st.DB().QueryRow(`SELECT COUNT(*) FROM outbox WHERE json_extract(CAST(body AS TEXT), '$.event_type')='call.result'`).Scan(&results); err != nil {
t.Fatal(err)
}
if err := st.DB().QueryRow(`SELECT COUNT(*) FROM reservations WHERE state='held'`).Scan(&held); err != nil {
t.Fatal(err)
}
if results != 2 || held != 0 {
t.Fatalf("stop lost the two in-flight final results or kept quota: results=%d held=%d", results, held)
}
}