package rules import ( "testing" "git.yeij.top/AskaEth/Scoreboard/server/internal/domain" ) func testSnapshot(t *testing.T, profiles []Profile, events []EventInfo) *Snapshot { t.Helper() pack := RulePack{ Format: FormatName, FormatVersion: FormatVersion, Meta: Meta{Code: "demo", Name: "当前规则"}, Standings: Standings{IndividualToTeamWeight: 1, TieBreak: DefaultTieBreak}, Profiles: profiles, } canonical := pack.Canonical() return &Snapshot{Pack: &canonical, Events: events} } func currentSnapshot(t *testing.T) *Snapshot { t.Helper() return testSnapshot(t, []Profile{ { Code: "individual_top6", Name: "个人项目前六名", AwardRanks: 6, TieMode: domain.TieSkip, Rules: []Rule{{1, 10}, {2, 8}, {3, 6}, {4, 3}, {5, 2}, {6, 1}}, }, { Code: "team_top6", Name: "团体项目前六名", AwardRanks: 2, TieMode: domain.TieSkip, Rules: []Rule{{1, 15}, {2, 10}}, }, }, []EventInfo{ {ID: 1, Code: "EVT_IND", Name: "个人示例", EventType: domain.EventTypeIndividual, ResultType: domain.ResultTypeTime, Scored: true, Multiplier: 1, Channels: []Channel{{Scope: domain.ScopeAthlete, Basis: domain.BasisResult, Profile: "individual_top6"}}}, {ID: 2, Code: "EVT_TEAM", Name: "团体示例", EventType: domain.EventTypeTeam, ResultType: domain.ResultTypeNone, Scored: true, Multiplier: 1, Channels: []Channel{{Scope: domain.ScopeTeam, Basis: domain.BasisManual, Profile: "team_top6"}}}, {ID: 3, Code: "EVT_MATCH", Name: "对抗示例", EventType: domain.EventTypeMatch, ResultType: domain.ResultTypeNone, Scored: true, Multiplier: 1, Channels: []Channel{{Scope: domain.ScopeTeam, Basis: domain.BasisManual, Profile: "team_top6"}}}, }) } const nextPackYAML = `format: scoreboard-rules formatVersion: 1 meta: { code: demo, name: 目标规则包 } standings: individualToTeamWeight: 2 tieBreak: [points, gold, silver, bronze, teamCode] medalCountsTeamEvents: true showIndividualBoard: true showIndividualMedals: true profiles: - code: individual_top6 name: 个人项目前六名 awardRanks: 6 tieMode: skip rules: - { rank: 1, points: 12 } - { rank: 2, points: 9 } - { rank: 3, points: 7 } - { rank: 4, points: 4 } - { rank: 5, points: 3 } - { rank: 6, points: 2 } - code: special_top3 name: 特别项目前三名 awardRanks: 3 tieMode: higher rules: - { rank: 1, points: 5 } - { rank: 2, points: 3 } - { rank: 3, points: 1 } channelTemplates: - match: { eventType: individual } channels: - { scope: athlete, basis: result, profile: individual_top6 } - match: { eventType: team } channels: - { scope: team, basis: manual, profile: special_top3 } eventOverrides: - eventCode: EVT_MATCH channels: [] - eventCode: EVT_GHOST channels: - { scope: team, basis: manual, profile: special_top3 } ` func TestDiffDetectsAddModifyRemove(t *testing.T) { cur := currentSnapshot(t) next, err := Parse([]byte(nextPackYAML)) if err != nil { t.Fatalf("Parse: %v", err) } cs, err := Diff(cur, next) if err != nil { t.Fatalf("Diff: %v", err) } if cs.Empty() { t.Fatal("差异不应为空") } kinds := map[string]ChangeKind{} for _, p := range cs.Profiles { kinds[p.Code] = p.Kind } if kinds["individual_top6"] != ChangeModified { t.Errorf("individual_top6 应为 modified,实际 %q", kinds["individual_top6"]) } if kinds["team_top6"] != ChangeRemoved { t.Errorf("team_top6 应为 removed,实际 %q", kinds["team_top6"]) } if kinds["special_top3"] != ChangeAdded { t.Errorf("special_top3 应为 added,实际 %q", kinds["special_top3"]) } for _, p := range cs.Profiles { if p.Code == "individual_top6" && len(p.Fields) == 0 { t.Error("修改的模板应列出变化字段") } if p.Code == "special_top3" && (p.Before != nil || p.After == nil) { t.Error("新增模板的 Before 应为空、After 应有值") } } channels := map[string]ChannelChange{} for _, c := range cs.Channels { channels[c.EventCode] = c } if c, ok := channels["EVT_TEAM"]; !ok || c.Kind != ChangeModified { t.Errorf("EVT_TEAM 通道应变为 special_top3: %+v", c) } if c, ok := channels["EVT_MATCH"]; !ok || c.Kind != ChangeRemoved || len(c.After) != 0 { t.Errorf("EVT_MATCH 通道应被清空: %+v", c) } if _, ok := channels["EVT_IND"]; ok { t.Error("EVT_IND 通道未变化,不应出现在差异里") } if len(cs.UnknownEvents) != 1 || cs.UnknownEvents[0] != "EVT_GHOST" { t.Errorf("未知项目识别错误: %v", cs.UnknownEvents) } if len(cs.Settings) != 1 || cs.Settings[0].Key != settingIndividualToTeamWeight { t.Errorf("设置差异错误: %+v", cs.Settings) } if cs.Settings[0].Before != "1" || cs.Settings[0].After != "2" { t.Errorf("权重差异值错误: %+v", cs.Settings[0]) } } func TestDiffIdenticalIsEmpty(t *testing.T) { cur := currentSnapshot(t) same, err := Diff(cur, cur.Pack) if err != nil { t.Fatalf("Diff: %v", err) } if !same.Empty() { t.Errorf("同一份规则包不应产生差异: %+v", same) } } func TestDiffRejectsInvalidPack(t *testing.T) { cur := currentSnapshot(t) bad, err := Parse([]byte(minimalPackYAML)) if err != nil { t.Fatalf("Parse: %v", err) } bad.FormatVersion = 99 if _, err := Diff(cur, bad); err == nil { t.Fatal("非法规则包应报错") } } func TestResolveTargetsPrecedence(t *testing.T) { cur := currentSnapshot(t) next, err := Parse([]byte(nextPackYAML)) if err != nil { t.Fatalf("Parse: %v", err) } targets, unknown, err := resolveTargets(cur, next) if err != nil { t.Fatalf("resolveTargets: %v", err) } if got := targets["EVT_IND"]; len(got) != 1 || got[0].Profile != "individual_top6" { t.Errorf("模板展开错误: %+v", got) } if got := targets["EVT_TEAM"]; len(got) != 1 || got[0].Profile != "special_top3" { t.Errorf("模板展开错误: %+v", got) } if got, ok := targets["EVT_MATCH"]; !ok || len(got) != 0 { t.Errorf("override 的 channels: [] 应清空通道: %+v", got) } if _, ok := targets["EVT_GHOST"]; ok { t.Error("库里不存在的项目不应出现在目标状态里") } if len(unknown) != 1 || unknown[0] != "EVT_GHOST" { t.Errorf("未知项目识别错误: %v", unknown) } }