package services // Pure-function tests for the trigger-group duration sort introduced // by t-paliad-296 / m/paliad#128. No DB needed — feeds synthetic // UIDeadlines and a ruleByID map directly into the helper. import ( "testing" "github.com/google/uuid" "mgit.msbls.de/m/paliad/internal/models" ) // makeRule is a tiny constructor for a synthetic rule with just the // fields the sort reads (parent_id, duration_value, duration_unit, // submission_code, trigger_event_id). func makeRule(t *testing.T, parent *uuid.UUID, code string, val int, unit string) (uuid.UUID, models.DeadlineRule) { t.Helper() id := uuid.New() codeCopy := code return id, models.DeadlineRule{ ID: id, ParentID: parent, SubmissionCode: &codeCopy, DurationValue: val, DurationUnit: unit, } } func makeDeadline(id uuid.UUID, code string) UIDeadline { return UIDeadline{ RuleID: id.String(), Code: code, } } // TestSortDeadlinesByDurationWithinTriggerGroup_PostDecision is the // canonical scenario from m's report — four post-decision optional // events anchored on the same decision must render with 1-month rules // before 2-month rules. func TestSortDeadlinesByDurationWithinTriggerGroup_PostDecision(t *testing.T) { decisionID := uuid.New() // Catalog order matches mig 132 sequence_order: cons_orders(60), // cost_app(70), rectification(70), appeal_spawn(80). consOrdID, consOrdRule := makeRule(t, &decisionID, "upc.inf.cfi.cons_orders", 2, "months") costAppID, costAppRule := makeRule(t, &decisionID, "upc.inf.cfi.cost_app", 1, "months") rectID, rectRule := makeRule(t, &decisionID, "upc.inf.cfi.rectification", 1, "months") appealID, appealRule := makeRule(t, &decisionID, "upc.inf.cfi.appeal_spawn", 2, "months") ruleByID := map[uuid.UUID]models.DeadlineRule{ consOrdID: consOrdRule, costAppID: costAppRule, rectID: rectRule, appealID: appealRule, } deadlines := []UIDeadline{ makeDeadline(consOrdID, "upc.inf.cfi.cons_orders"), makeDeadline(costAppID, "upc.inf.cfi.cost_app"), makeDeadline(rectID, "upc.inf.cfi.rectification"), makeDeadline(appealID, "upc.inf.cfi.appeal_spawn"), } sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID) // 1-month tier first (cost_app, rectification — alphabetical by // submission_code), then 2-month tier (appeal_spawn, cons_orders // — submission_code ASC tiebreak per spec). want := []string{ "upc.inf.cfi.cost_app", "upc.inf.cfi.rectification", "upc.inf.cfi.appeal_spawn", "upc.inf.cfi.cons_orders", } for i, w := range want { if deadlines[i].Code != w { t.Errorf("deadlines[%d].Code = %q, want %q", i, deadlines[i].Code, w) } } } // TestSortDeadlinesByDurationWithinTriggerGroup_UnitWeight asserts the // unit-weight ordering: days < weeks < months < years, with shorter // durations of the same unit winning their tier. func TestSortDeadlinesByDurationWithinTriggerGroup_UnitWeight(t *testing.T) { parentID := uuid.New() d14ID, d14Rule := makeRule(t, &parentID, "x.14days", 14, "days") d2wID, d2wRule := makeRule(t, &parentID, "x.2weeks", 2, "weeks") d1mID, d1mRule := makeRule(t, &parentID, "x.1month", 1, "months") d6mID, d6mRule := makeRule(t, &parentID, "x.6months", 6, "months") d1yID, d1yRule := makeRule(t, &parentID, "x.1year", 1, "years") ruleByID := map[uuid.UUID]models.DeadlineRule{ d14ID: d14Rule, d2wID: d2wRule, d1mID: d1mRule, d6mID: d6mRule, d1yID: d1yRule, } deadlines := []UIDeadline{ makeDeadline(d6mID, "x.6months"), makeDeadline(d1yID, "x.1year"), makeDeadline(d2wID, "x.2weeks"), makeDeadline(d14ID, "x.14days"), makeDeadline(d1mID, "x.1month"), } sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID) want := []string{"x.14days", "x.2weeks", "x.1month", "x.6months", "x.1year"} for i, w := range want { if deadlines[i].Code != w { t.Errorf("deadlines[%d].Code = %q, want %q", i, deadlines[i].Code, w) } } } // TestSortDeadlinesByDurationWithinTriggerGroup_NoCrossGroupReorder // guards the hard rule: rules with different parents must keep their // relative position. Sorting only ever permutes adjacent same-parent // rows. func TestSortDeadlinesByDurationWithinTriggerGroup_NoCrossGroupReorder(t *testing.T) { parentAID := uuid.New() parentBID := uuid.New() a3mID, a3mRule := makeRule(t, &parentAID, "ga.3months", 3, "months") b1mID, b1mRule := makeRule(t, &parentBID, "gb.1month", 1, "months") a14dID, a14dRule := makeRule(t, &parentAID, "ga.14days", 14, "days") b2mID, b2mRule := makeRule(t, &parentBID, "gb.2months", 2, "months") ruleByID := map[uuid.UUID]models.DeadlineRule{ a3mID: a3mRule, b1mID: b1mRule, a14dID: a14dRule, b2mID: b2mRule, } // Interleaved groups: A, B, A, B. Each group has one rule between // each other group's rules — the consecutive-run walk should treat // each as its own one-element run and not reorder anything. deadlines := []UIDeadline{ makeDeadline(a3mID, "ga.3months"), makeDeadline(b1mID, "gb.1month"), makeDeadline(a14dID, "ga.14days"), makeDeadline(b2mID, "gb.2months"), } sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID) want := []string{"ga.3months", "gb.1month", "ga.14days", "gb.2months"} for i, w := range want { if deadlines[i].Code != w { t.Errorf("deadlines[%d].Code = %q, want %q (interleaved groups must not reorder across)", i, deadlines[i].Code, w) } } } // TestSortDeadlinesByDurationWithinTriggerGroup_ConditionalLast asserts // that court-set / conditional rows (no concrete date in the duration // ladder) sort LAST within their group, regardless of their stated // duration value. func TestSortDeadlinesByDurationWithinTriggerGroup_ConditionalLast(t *testing.T) { parentID := uuid.New() dID, dRule := makeRule(t, &parentID, "x.duration", 2, "months") cID, cRule := makeRule(t, &parentID, "x.conditional", 1, "months") csID, csRule := makeRule(t, &parentID, "x.courtset", 1, "months") d2ID, d2Rule := makeRule(t, &parentID, "x.short", 14, "days") ruleByID := map[uuid.UUID]models.DeadlineRule{ dID: dRule, cID: cRule, csID: csRule, d2ID: d2Rule, } deadlines := []UIDeadline{ {RuleID: cID.String(), Code: "x.conditional", IsConditional: true}, {RuleID: dID.String(), Code: "x.duration"}, {RuleID: csID.String(), Code: "x.courtset", IsCourtSet: true}, {RuleID: d2ID.String(), Code: "x.short"}, } sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID) // Concrete rows first (sorted by duration): x.short (14d) then // x.duration (2mo). Then the two no-date rows, tiebroken by code: // x.conditional < x.courtset alphabetically. want := []string{"x.short", "x.duration", "x.conditional", "x.courtset"} for i, w := range want { if deadlines[i].Code != w { t.Errorf("deadlines[%d].Code = %q, want %q", i, deadlines[i].Code, w) } } } // TestSortDeadlinesByDurationWithinTriggerGroup_RootsNotMerged guards // the root-rule exception: top-level rules (parent_id=nil, no // trigger_event_id) must never be sorted against each other — they // represent distinct anchor points (SoC vs oral hearing vs decision) // whose proceeding-sequence order is non-negotiable. func TestSortDeadlinesByDurationWithinTriggerGroup_RootsNotMerged(t *testing.T) { rootSoCID, rootSoCRule := makeRule(t, nil, "x.soc", 0, "months") rootOralID, rootOralRule := makeRule(t, nil, "x.oral", 0, "months") rootDecID, rootDecRule := makeRule(t, nil, "x.decision", 0, "months") ruleByID := map[uuid.UUID]models.DeadlineRule{ rootSoCID: rootSoCRule, rootOralID: rootOralRule, rootDecID: rootDecRule, } deadlines := []UIDeadline{ makeDeadline(rootSoCID, "x.soc"), makeDeadline(rootOralID, "x.oral"), makeDeadline(rootDecID, "x.decision"), } sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID) // Roots must keep their input order — they're not in the same // trigger group as each other. want := []string{"x.soc", "x.oral", "x.decision"} for i, w := range want { if deadlines[i].Code != w { t.Errorf("deadlines[%d].Code = %q, want %q (roots must not be sorted against each other)", i, deadlines[i].Code, w) } } }