Merge slice/flagtracker-metas: quests, Mossy Tracker, meta sync

Resolved FeatureCatalog (both blocks kept; the main window's Tools buttons now
open the Mossy Tracker and Metas panels). Tests from both sides adjusted to
the combined plugin: quest tests look at quest frames only (chat is streamed
too), and the stream tests ignore the /myquests request sent at login.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-25 12:29:10 +02:00
commit 2e5d768ca9
19 changed files with 3984 additions and 1 deletions

View file

@ -0,0 +1,235 @@
using OpenAC.MosswartMassacre.Quests;
namespace OpenAC.MosswartMassacre.Tests.Quests;
public sealed class QuestTests
{
// Real answer lines in the shape the server writes them (ACE's /myquests:
// "stamp - N solves (last)\"message\" max delta"). The first is the example
// in the original's parser comment, the second the one UtilityBelt quotes.
private const string CommentSample = "SomeQuest - 5 solves (1640995200)\"Quest description here\" 10 3600";
private const string UbSample =
"regaliamaskuber - 1 solves (1522807913)\"Player has collected the mask from a Virinidi Profatrix.\" -1 72000";
private static long UnixNow(PluginHarness harness) =>
new DateTimeOffset(harness.Clock.UtcNow).ToUnixTimeSeconds();
private static string Line(string stamp, int solves, long lastSolved, int max, long delta) =>
$"{stamp} - {solves} solves ({lastSolved})\"A message\" {max} {delta}";
[Fact]
public void Login_asks_the_server_and_reads_the_answer_for_three_seconds()
{
using var harness = new PluginHarness();
harness.Login();
QuestManager quests = harness.Feature<QuestManager>();
Assert.Equal(["/myquests"], harness.Host.Automation.Chat.Submitted);
Assert.True(quests.IsRefreshing);
harness.Host.Automation.Chat.Deliver(CommentSample);
harness.Host.Automation.Chat.Deliver(UbSample);
harness.Host.Automation.Chat.Deliver("You say, \"hello - 3 solves\"");
harness.Run(3.1);
harness.Host.Automation.Chat.Deliver("late - 1 solves (1)\"x\" 1 1");
Assert.False(quests.IsRefreshing);
Assert.Equal(2, quests.QuestList.Count);
Quest first = quests.QuestList[0];
Assert.Equal("SomeQuest", first.Id);
Assert.Equal(5, first.Solves);
Assert.Equal(1640995200, first.Timestamp);
Assert.Equal("Quest description here", first.Description);
Assert.Equal(10, first.MaxSolves);
Assert.Equal(3600, first.Delta);
Assert.Equal(1640995200 + 3600, first.ExpireTime);
Quest second = quests.QuestList[1];
Assert.Equal("regaliamaskuber", second.Id);
Assert.Equal(-1, second.MaxSolves);
Assert.Equal(72000, second.Delta);
Assert.Equal("Player has collected the mask from a Virinidi Profatrix.", second.Description);
}
[Fact]
public void A_refresh_while_one_is_reading_is_ignored_and_the_answer_stays_in_chat()
{
using var harness = new PluginHarness();
harness.Login();
harness.Context.Commands.Dispatch("refreshquests");
Assert.Equal(["/myquests"], harness.Host.Automation.Chat.Submitted);
Assert.Contains("[Mosswart Massacre] [QUEST] Refreshing quest data...", harness.ChatLines);
harness.Host.Automation.Chat.Deliver(UbSample);
// The original never ate the answer lines.
Assert.Contains(harness.Host.Automation.Chat.Delivered, message => message.Text == UbSample);
harness.Run(3.1);
harness.Context.Commands.Dispatch("refreshquests");
Assert.Equal(["/myquests", "/myquests"], harness.Host.Automation.Chat.Submitted);
Assert.Empty(harness.Feature<QuestManager>().QuestList);
}
[Fact]
public void A_line_whose_number_overflows_is_reported_like_the_original()
{
using var harness = new PluginHarness();
harness.Login();
harness.Host.Automation.Chat.Deliver("big - 1 solves (1)\"x\" 1 99999999999");
Assert.Empty(harness.Feature<QuestManager>().QuestList);
Assert.Contains(harness.ChatLines, line => line.StartsWith(
"[Mosswart Massacre] Error parsing quest line 'big - 1 solves (1)\"x\" 1 99999999999': ",
StringComparison.Ordinal));
}
[Fact]
public void The_quest_frame_is_exactly_the_original_one()
{
// The original: WebSocket.SendQuestDataAsync serialized
// new { type = "quest", timestamp = DateTime.UtcNow.ToString("o"),
// character_name = CharacterFilter.Name, quest_name, countdown }
// with JsonConvert.SerializeObject, and QuestStreamingService passed
// QuestManager.GetFriendlyQuestName(quest.Id) and FormatCountdown.
using var harness = new PluginHarness();
harness.Login();
long now = UnixNow(harness);
harness.Host.Automation.Chat.Deliver(Line("stipendtimer_0812", 3, now - 100, -1, 100 + 93600));
harness.Feature<QuestManager>().StreamPriorityQuests();
Assert.Equal(
["{\"type\":\"quest\",\"timestamp\":\"2026-09-25T08:30:15.1230000Z\",\"character_name\":\"Mossy\",\"quest_name\":\"Stipend Collection Timer\",\"countdown\":\"1d 02h\"}"],
harness.Frames.Where(IsQuest));
}
[Fact]
public void Only_the_three_priority_quests_stream_once_each_in_answer_order()
{
using var harness = new PluginHarness();
harness.Login();
long now = UnixNow(harness);
harness.Host.Automation.Chat.Deliver(Line("insatiableeaterjaw", 1, now - 10, -1, 5));
harness.Host.Automation.Chat.Deliver(Line("somethingelse", 1, now, -1, 500));
harness.Host.Automation.Chat.Deliver(Line("augmentationblankgemacquired", 1, now, -1, 3 * 3600 + 5 * 60 + 30));
harness.Host.Automation.Chat.Deliver(Line("insatiableeaterjaw", 9, now, -1, 99999));
harness.Feature<QuestManager>().StreamPriorityQuests();
// "insatiableeaterjaw" is in the name table twice; the later entry,
// "Insatiable Eater Jaw", is the one the original sent.
Assert.Equal(
[
"{\"type\":\"quest\",\"timestamp\":\"2026-09-25T08:30:15.1230000Z\",\"character_name\":\"Mossy\",\"quest_name\":\"Insatiable Eater Jaw\",\"countdown\":\"READY\"}",
"{\"type\":\"quest\",\"timestamp\":\"2026-09-25T08:30:15.1230000Z\",\"character_name\":\"Mossy\",\"quest_name\":\"Blank Augmentation Gem Pickup Timer\",\"countdown\":\"3h 05m\"}",
],
harness.Frames.Where(IsQuest));
}
[Fact]
public void The_stream_runs_every_thirty_seconds_after_login()
{
using var harness = new PluginHarness();
harness.Login();
long now = UnixNow(harness);
harness.Host.Automation.Chat.Deliver(Line("stipendtimer_0812", 1, now, -1, 3600 + 30));
harness.Run(29.5);
Assert.Empty(harness.FramesOfType("quest"));
harness.Run(1);
Assert.Single(harness.FramesOfType("quest"));
Assert.Equal("1h 00m", (string?)harness.FramesOfType("quest").Single()["countdown"]);
harness.Run(30);
Assert.Equal(2, harness.FramesOfType("quest").Count());
}
private static bool IsQuest(string frame) =>
frame.StartsWith("{\"type\":\"quest\"", StringComparison.Ordinal);
[Fact]
public void Nothing_streams_while_streaming_is_off_or_after_logoff()
{
using var off = new PluginHarness(webSocketEnabled: false);
off.Login();
off.Host.Automation.Chat.Deliver(Line("stipendtimer_0812", 1, UnixNow(off), -1, 60));
off.Feature<QuestManager>().StreamPriorityQuests();
Assert.Empty(off.Frames);
using var harness = new PluginHarness();
harness.Login();
harness.Host.Automation.Chat.Deliver(Line("stipendtimer_0812", 1, UnixNow(harness), -1, 60));
harness.Plugin.OnLogoff();
harness.Run(31);
Assert.DoesNotContain(harness.Frames, IsQuest);
Assert.Equal(0, harness.Host.Automation.Chat.ReceivedSubscriberCount);
}
[Fact]
public void Quest_status_lists_the_priority_quests()
{
using var harness = new PluginHarness();
harness.Login();
harness.Host.Automation.Chat.Deliver(Line("stipendtimer_0812", 1, UnixNow(harness), -1, 60));
harness.Context.Commands.Dispatch("queststatus");
Assert.Contains("[Mosswart Massacre] Timer Active: True", harness.ChatLines);
Assert.Contains("[Mosswart Massacre] Quest Count: 1", harness.ChatLines);
Assert.Contains("[Mosswart Massacre] Priority Quests Found: 1", harness.ChatLines);
Assert.Contains("[Mosswart Massacre] - Stipend Collection Timer (stipendtimer_0812)", harness.ChatLines);
}
[Theory]
[InlineData(-5L, "READY")]
[InlineData(0L, "READY")]
[InlineData(1L, "1s")]
[InlineData(59L, "59s")]
[InlineData(60L, "1m 00s")]
[InlineData(69L, "1m 09s")]
[InlineData(3599L, "59m 59s")]
[InlineData(3600L, "1h 00m")]
[InlineData(3660L, "1h 01m")]
[InlineData(86399L, "23h 59m")]
[InlineData(86400L, "1d 00h")]
[InlineData(93600L, "1d 02h")]
[InlineData(10L * 86400 + 23 * 3600 + 3599, "10d 23h")]
public void Countdown_formats_like_the_original(long seconds, string expected)
{
Assert.Equal(expected, QuestStreamingService.FormatCountdown(seconds));
}
[Theory]
[InlineData(0, "0s")]
[InlineData(-1, "0s")]
[InlineData(59, "59s")]
[InlineData(60, "1m")]
[InlineData(3600, "1h")]
[InlineData(72000, "20h")]
[InlineData(90061, "1d 1h 1m 1s")]
[InlineData(1209600, "14d")]
public void Cooldown_formats_like_the_original(int seconds, string expected)
{
Assert.Equal(expected, QuestManager.FormatSeconds(seconds));
}
[Fact]
public void Last_solved_is_month_first_in_utc_whatever_the_culture()
{
System.Globalization.CultureInfo saved = System.Globalization.CultureInfo.CurrentCulture;
try
{
System.Globalization.CultureInfo.CurrentCulture = new System.Globalization.CultureInfo("sv-SE");
Assert.Equal("04/04/2018 05:31:53", QuestManager.FormatTimeStamp(1522819913));
}
finally
{
System.Globalization.CultureInfo.CurrentCulture = saved;
}
}
}