mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-09-03 22:17:16 +00:00
fix(lol): omit score when a finished match has none published
lolesports drives event.state off the broadcast timeline but fills
result.outcome and result.gameWins from a separate per-game ingestion
path. A match therefore reads as "completed" for hours before gaining a
score, and in that window every team carries {"outcome": null,
"gameWins": 0}.
The renderer used `Result != nil` as its has-a-score test, which cannot
catch this: the result object is present, only its contents are empty.
The absent gameWins fell through to Go's zero value and printed as a
literal 0, so an unscored series was reported as a definitive draw
("MKOI 0-0 KC") with neither side bolded.
Gate the score on a declared outcome instead, and render unscored
finished matches as the matchup alone with a pending marker. An outcome
on either side is enough to trust the score - it proves the ingestion
ran. inProgress keeps rendering 0-0, which is truthful for a live series
that has not resolved its first game.
Also collapses the score extraction both branches duplicated into
shared helpers.
This commit is contained in:
@@ -84,6 +84,42 @@ func teamLabel(t Team) string {
|
||||
return "TBD"
|
||||
}
|
||||
|
||||
// declaredOutcome returns a team's upstream-declared series outcome ("win" or
|
||||
// "loss"), or "" when upstream has not published one. Two distinct upstream
|
||||
// shapes collapse to "": a missing `result` object, and the far more common
|
||||
// `{"outcome": null, "gameWins": 0}`.
|
||||
func declaredOutcome(t Team) string {
|
||||
if t.Result == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Result.Outcome
|
||||
}
|
||||
|
||||
// seriesWins returns a team's games won in the series, 0 when upstream has sent
|
||||
// no result. Only meaningful once an outcome has been declared — see
|
||||
// scoreIsPublished.
|
||||
func seriesWins(t Team) int {
|
||||
if t.Result == nil {
|
||||
return 0
|
||||
}
|
||||
return t.Result.GameWins
|
||||
}
|
||||
|
||||
// scoreIsPublished reports whether a finished series has a score we can quote.
|
||||
//
|
||||
// lolesports drives `event.state` off the broadcast timeline and fills
|
||||
// `result.outcome`/`result.gameWins` from a separate per-game ingestion path,
|
||||
// so a match reads as "completed" for hours before (or without ever) gaining a
|
||||
// score. In that window every team carries `{"outcome": null, "gameWins": 0}`,
|
||||
// which a nil-check cannot catch because the object itself is present — and
|
||||
// Go's zero value for the absent gameWins then renders as a literal 0.
|
||||
//
|
||||
// An outcome on either side is enough: it proves the ingestion ran, so the
|
||||
// gameWins alongside it are real even if the other side's result is sparse.
|
||||
func scoreIsPublished(t1, t2 Team) bool {
|
||||
return declaredOutcome(t1) != "" || declaredOutcome(t2) != ""
|
||||
}
|
||||
|
||||
// formatEventLine renders one match as a single line. Already HTML-safe;
|
||||
// caller can join with "\n" inside a league section.
|
||||
func formatEventLine(e ScheduleEvent) string {
|
||||
@@ -107,31 +143,28 @@ func formatEventLine(e ScheduleEvent) string {
|
||||
|
||||
switch e.State {
|
||||
case "completed":
|
||||
var w1, w2 int
|
||||
if t1.Result != nil {
|
||||
w1 = t1.Result.GameWins
|
||||
}
|
||||
if t2.Result != nil {
|
||||
w2 = t2.Result.GameWins
|
||||
// Upstream says the series is over but has published no outcome, so we
|
||||
// have no score to report. Show the matchup and say so rather than let
|
||||
// the absent gameWins render as a 0–0 that nobody played.
|
||||
if !scoreIsPublished(t1, t2) {
|
||||
return fmt.Sprintf("☑️ %s vs %s%s%s · score pending", t1Label, t2Label, bo, block)
|
||||
}
|
||||
left := t1Label
|
||||
if t1.Result != nil && t1.Result.Outcome == "win" {
|
||||
if declaredOutcome(t1) == "win" {
|
||||
left = "<b>" + t1Label + "</b>"
|
||||
}
|
||||
right := t2Label
|
||||
if t2.Result != nil && t2.Result.Outcome == "win" {
|
||||
if declaredOutcome(t2) == "win" {
|
||||
right = "<b>" + t2Label + "</b>"
|
||||
}
|
||||
return fmt.Sprintf("✅ %s %d–%d %s%s%s", left, w1, w2, right, bo, block)
|
||||
return fmt.Sprintf("✅ %s %d–%d %s%s%s",
|
||||
left, seriesWins(t1), seriesWins(t2), right, bo, block)
|
||||
case "inProgress":
|
||||
var w1, w2 int
|
||||
if t1.Result != nil {
|
||||
w1 = t1.Result.GameWins
|
||||
}
|
||||
if t2.Result != nil {
|
||||
w2 = t2.Result.GameWins
|
||||
}
|
||||
return fmt.Sprintf("🔴 LIVE %s %d–%d %s%s%s", t1Label, w1, w2, t2Label, bo, block)
|
||||
// No published-score guard here: a live series legitimately sits at 0–0
|
||||
// until its first game resolves, and upstream declares no outcome until
|
||||
// the series ends.
|
||||
return fmt.Sprintf("🔴 LIVE %s %d–%d %s%s%s",
|
||||
t1Label, seriesWins(t1), seriesWins(t2), t2Label, bo, block)
|
||||
default:
|
||||
t, err := time.Parse(time.RFC3339, e.StartTime)
|
||||
if err != nil {
|
||||
|
||||
@@ -66,6 +66,74 @@ func TestFormatEventLine_Completed_BoldsWinner(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Upstream flips state to "completed" when the broadcast window closes, but
|
||||
// fills gameWins/outcome from a separate per-game ingestion path. In the gap it
|
||||
// sends {"outcome": null, "gameWins": 0} for both teams — a shape that must not
|
||||
// be reported as a real 0–0 draw.
|
||||
func TestFormatEventLine_CompletedWithoutResults_OmitsScore(t *testing.T) {
|
||||
pending := &TeamResult{} // json `{"outcome": null, "gameWins": 0}`
|
||||
e := ScheduleEvent{
|
||||
StartTime: "2026-07-25T17:30:00Z",
|
||||
State: "completed",
|
||||
BlockName: "Week 1",
|
||||
League: League{Slug: "lec", Name: "LEC"},
|
||||
Match: Match{
|
||||
Teams: []Team{
|
||||
{Code: "MKOI", Result: pending},
|
||||
{Code: "KC", Result: pending},
|
||||
},
|
||||
Strategy: Strategy{Type: "bestOf", Count: 3},
|
||||
},
|
||||
}
|
||||
got := formatEventLine(e)
|
||||
if strings.Contains(got, "0–0") {
|
||||
t.Errorf("fabricated 0–0 score for unscored match: %q", got)
|
||||
}
|
||||
if strings.Contains(got, "✅") {
|
||||
t.Errorf("unscored match should not use the scored-result glyph: %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "MKOI vs KC") {
|
||||
t.Errorf("missing matchup: %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "Bo3") || !strings.Contains(got, "Week 1") {
|
||||
t.Errorf("lost static metadata: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A missing result object entirely (no `result` key) is the same class of
|
||||
// unknown as a null outcome.
|
||||
func TestFormatEventLine_CompletedNilResult_OmitsScore(t *testing.T) {
|
||||
e := mkEvent("completed", "lck", "LCK", "T1", "GEN", "2026-05-09T05:00:00Z")
|
||||
got := formatEventLine(e)
|
||||
if strings.Contains(got, "0–0") {
|
||||
t.Errorf("fabricated 0–0 score for nil-result match: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// One side declaring an outcome is enough to trust the score, even if the
|
||||
// other side's result is absent.
|
||||
func TestFormatEventLine_CompletedPartialResult_KeepsScore(t *testing.T) {
|
||||
e := ScheduleEvent{
|
||||
StartTime: "2026-05-09T05:00:00Z",
|
||||
State: "completed",
|
||||
League: League{Slug: "lck", Name: "LCK"},
|
||||
Match: Match{
|
||||
Teams: []Team{
|
||||
{Code: "T1", Result: &TeamResult{Outcome: "win", GameWins: 2}},
|
||||
{Code: "GEN"},
|
||||
},
|
||||
Strategy: Strategy{Count: 3},
|
||||
},
|
||||
}
|
||||
got := formatEventLine(e)
|
||||
if !strings.Contains(got, "2–0") {
|
||||
t.Errorf("score dropped despite a declared outcome: %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "<b>T1</b>") {
|
||||
t.Errorf("winner not bolded: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatEventLine_InProgress(t *testing.T) {
|
||||
w := &TeamResult{GameWins: 1}
|
||||
e := ScheduleEvent{
|
||||
|
||||
Reference in New Issue
Block a user