Files
LlamaCasty/ytLive.Tests/ScoreboardStateTests.cs
T
gramps 3ed8386303 feat(scoreboard): unit 1 — session tallies, with the gift double-count guard
ScoreboardState folds the six live-chat event kinds into the numbers a creator puts on screen:
last member, last Super Chat, last gift, members gained, milestones, and session money per
currency. Scope is the running session (creator ruling): reset at start, dead at End, no
persistence.

Two rules are load-bearing:
- money is keyed by currency and never summed across them; the tile shows the dominant currency by
  VALUE, so a single EUR tip cannot flip the display away from the larger USD total
- a gifted membership is counted ONCE, from membershipGiftingEvent (which carries the count);
  giftMembershipReceivedEvent is the recipient's view of the same purchase and changes nothing.
  The test fires five recipient events after one purchase of five and the total must not move.
2026-10-30 14:35:42 -07:00

69 lines
3.1 KiB
C#

using Xunit;
using ytLive.Models;
namespace ytLive.Tests;
/// <summary>Session-scoped scoreboard tallies. The rules that matter: money is never added across
/// currencies, and a gifted membership is counted ONCE (from the purchase), never again from the
/// recipient's event — that double-count is the easy mistake, and the creator called it out.</summary>
public class ScoreboardStateTests
{
private static ChatMessage Message(ChatEventKind kind, string author = "viewer", double amount = 0,
string? currency = "USD", int giftCount = 0, string? display = null) => new()
{
Kind = kind, AuthorName = author, SuperChatAmount = amount,
SuperChatCurrency = currency, GiftNewMembersCount = giftCount, SuperChatDisplayString = display,
};
[Fact]
public void Tallies_follow_the_six_events_and_never_double_count_a_gift()
{
var state = new ScoreboardState();
state.Apply(Message(ChatEventKind.NewMember, "Paid"));
Assert.Equal(1, state.NewMembers);
Assert.Equal("Paid", state.LastMemberName);
// A gift purchase of 5 memberships counts 5...
state.Apply(Message(ChatEventKind.MemberGift, "Gifter", giftCount: 5));
Assert.Equal(6, state.NewMembers);
Assert.Equal(5, state.LastGiftCount);
Assert.Equal("Gifter", state.LastGifterName);
// ...and the recipients' own events add NOTHING (they are the same memberships).
for (var i = 0; i < 5; i++) state.Apply(Message(ChatEventKind.GiftReceived, $"Recipient{i}", giftCount: 1));
Assert.Equal(6, state.NewMembers);
Assert.Equal(5, state.LastGiftCount);
Assert.Equal("Gifter", state.LastGifterName);
state.Apply(Message(ChatEventKind.MemberMilestone, "Longtimer"));
Assert.Equal(1, state.MilestoneCount);
// Super Chat and Super Sticker both carry money; the last one drives the "latest" tile.
state.Apply(Message(ChatEventKind.SuperChat, "Tipper", 10, "USD", display: "$10.00"));
state.Apply(Message(ChatEventKind.SuperSticker, "Sticker", 5, "USD"));
Assert.Equal(15, state.SessionTotal.Total, 3);
Assert.Equal("USD", state.SessionTotal.Currency);
Assert.Equal("Sticker", state.LastSuperChatAuthor);
Assert.False(state.HasMixedCurrencies);
// A second currency is tracked separately, never summed into the first.
state.Apply(Message(ChatEventKind.SuperChat, "Euro", 3, "EUR"));
Assert.Equal(15, state.MoneyByCurrency["USD"], 3);
Assert.Equal(3, state.MoneyByCurrency["EUR"], 3);
Assert.True(state.HasMixedCurrencies);
Assert.Equal("USD", state.SessionTotal.Currency); // dominant by value, not by recency
// Plain chat is ignored entirely.
state.Apply(Message(ChatEventKind.None, "Chatter"));
Assert.Equal(6, state.NewMembers);
Assert.Equal(1, state.MilestoneCount);
// A new session starts clean.
state.Reset();
Assert.Equal(0, state.NewMembers);
Assert.Equal(0, state.SessionTotal.Total);
Assert.Null(state.LastMemberName);
}
}