package dispatcher import ( "context" "encoding/json" "errors" "testing" "git.ipao.vip/rogee/go-sip/internal/configread" ) func TestSelectTrunkHonorsTaskLineLimit(t *testing.T) { for _, tc := range []struct { name string limit, task, total int64 wantErr error }{ {"available", 1, 0, 0, nil}, {"other task occupancy", 1, 0, 1, nil}, {"disabled", 0, 0, 0, ErrRuleWait}, {"task line full", 1, 1, 1, ErrRuleWait}, {"unknown line occupied", 1, 2, 0, ErrRuleWait}, {"global line full", 5, 0, 2, ErrRuleWait}, {"negative limit", -1, 0, 0, ErrRuleInvalid}, } { t.Run(tc.name, func(t *testing.T) { snapshot := policySnapshot(t) snapshot.Task.AllowedTrunks = []configread.AllowedTrunk{{TrunkID: "trunk-mock", Concurrency: tc.limit}} choice, err := SelectTrunk(snapshot, "15003164745", monday(9, 30), map[string]int64{"trunk-mock": tc.total}, map[string]int64{"trunk-mock": tc.task}, map[string]int64{"trunk-mock": 8}) if !errors.Is(err, tc.wantErr) { t.Fatalf("selected=%q error=%v, want %v", choice.TrunkID, err, tc.wantErr) } }) } } func TestSelectTrunkSkipsDisabledAndSaturatedTaskLinesInConfiguredOrder(t *testing.T) { for _, limit := range []int64{0, 1} { snapshot := policySnapshot(t) var trunks []trunkConfig if err := json.Unmarshal(snapshot.SIP.Trunks, &trunks); err != nil { t.Fatal(err) } second := trunks[0] second.TrunkID = "trunk-second" trunks = append(trunks, second) var err error snapshot.SIP.Trunks, err = json.Marshal(trunks) if err != nil { t.Fatal(err) } snapshot.Task.AllowedTrunks = []configread.AllowedTrunk{{TrunkID: "trunk-mock", Concurrency: limit}, {TrunkID: "trunk-second", Concurrency: 1}} choice, err := SelectTrunk(snapshot, "15003164745", monday(9, 30), map[string]int64{"trunk-mock": 1}, map[string]int64{"trunk-mock": 1}, map[string]int64{"trunk-mock": 8, "trunk-second": 8}) if err != nil || choice.TrunkID != "trunk-second" { t.Fatalf("limit=%d selected=%q err=%v", limit, choice.TrunkID, err) } } } func TestSelectTrunkDoesNotHideMissingLineBehindZeroConcurrency(t *testing.T) { snapshot := policySnapshot(t) snapshot.Task.AllowedTrunks = []configread.AllowedTrunk{{TrunkID: "missing", Concurrency: 0}} if _, err := SelectTrunk(snapshot, "15003164745", monday(9, 30), nil, nil, nil); !errors.Is(err, ErrRuleInvalid) { t.Fatalf("disabled line hid a broken reference: %v", err) } } func TestSelectTrunkRejectsDuplicateTaskLineIDs(t *testing.T) { snapshot := policySnapshot(t) snapshot.Task.AllowedTrunks = []configread.AllowedTrunk{{TrunkID: "trunk-mock", Concurrency: 1}, {TrunkID: "trunk-mock", Concurrency: 2}} _, err := SelectTrunk(snapshot, "15003164745", monday(9, 30), nil, nil, map[string]int64{"trunk-mock": 8}) if !errors.Is(err, ErrRuleInvalid) { t.Fatalf("duplicate task line accepted: %v", err) } } func editedTaskLine(t *testing.T, snapshot configread.Snapshot, revision, limit int64) configread.Snapshot { t.Helper() var task map[string]json.RawMessage if err := json.Unmarshal(snapshot.Task.Raw, &task); err != nil { t.Fatal(err) } for key, value := range map[string]any{ "task_revision": revision, "allowed_trunk_ids": []configread.AllowedTrunk{{TrunkID: "trunk-mock", Concurrency: limit}}, } { raw, err := json.Marshal(value) if err != nil { t.Fatal(err) } task[key] = raw } raw, err := json.Marshal(task) if err != nil { t.Fatal(err) } if err := json.Unmarshal(raw, &snapshot.Task); err != nil { t.Fatal(err) } return snapshot } func TestExecuteControllerKeepsUnknownTaskLineSlotUntilConfirmedEnd(t *testing.T) { c, origin, _, _ := newExecuteFixture(t) snapshot := editedTaskLine(t, policySnapshot(t), 2, 1) if state, err := c.Store.CompleteEdit(snapshot, "edit-line-limit"); err != nil || state != "applied" { t.Fatalf("edit=%q err=%v", state, err) } ctx := context.Background() lostReply := errors.New("simulated originate reply lost") origin.err = lostReply if err := c.ProcessExecute(ctx, executeBody(t, "call-first", "15003164745")); !errors.Is(err, lostReply) { t.Fatalf("unknown originate outcome=%v", err) } origin.err = nil if err := c.ProcessExecute(ctx, executeBody(t, "call-second", "15003164745")); err != nil { t.Fatal(err) } if len(origin.calls) != 1 { t.Fatalf("task-line limit exceeded: attempts=%d", len(origin.calls)) } if err := c.ProcessPending(ctx); err != nil || len(origin.calls) != 1 { t.Fatalf("unknown call was retried or lost its slot: calls=%d err=%v", len(origin.calls), err) } if err := c.Store.FinishExecute(c.DispatcherID, "call-first"); err != nil { t.Fatal(err) } if err := c.ProcessPending(ctx); err != nil || len(origin.calls) != 2 { t.Fatalf("confirmed end did not admit the pending call: calls=%d err=%v", len(origin.calls), err) } if origin.calls[1].Snapshot.Task.TaskRevision != 2 || origin.calls[1].Snapshot.Task.AllowedTrunks[0].Concurrency != 1 { t.Fatal("new task-line limit was not frozen into the approved call") } } type editingTaskOriginator struct { *fakeOriginator edit func() } func (o *editingTaskOriginator) LoadedTrunks(ctx context.Context) (map[string]int64, error) { if o.edit != nil { edit := o.edit o.edit = nil edit() } return o.fakeOriginator.LoadedTrunks(ctx) } func TestExecuteControllerRejectsSelectionIfTaskChangesBeforeReservation(t *testing.T) { c, origin, _, _ := newExecuteFixture(t) c.Originator = &editingTaskOriginator{fakeOriginator: origin, edit: func() { snapshot := editedTaskLine(t, policySnapshot(t), 2, 0) if state, err := c.Store.CompleteEdit(snapshot, "disable-before-reserve"); err != nil || state != "applied" { t.Fatalf("edit=%q err=%v", state, err) } }} if err := c.ProcessExecute(context.Background(), executeBody(t, "call-stale-choice", "15003164745")); err != nil { t.Fatal(err) } if len(origin.calls) != 0 { t.Fatal("stale selection originated after the task line was disabled") } }