openac-mosswartmassacre/tests/OpenAC.MosswartMassacre.Tests/Inventory/InventoryFeatureTests.cs
Erik c1e8a2cf18 Inventory: stamp an appraisal when it arrives
The original's appraisal time was the wall clock at the moment the
appraisal came in. The reader stamped it when it first built the record,
which for a coalesced update can be minutes later; the appraisal event now
stamps it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 12:41:00 +02:00

273 lines
11 KiB
C#

using AcDream.Plugin.Abstractions;
using Newtonsoft.Json.Linq;
using OpenAC.MosswartMassacre.Inventory;
using static OpenAC.MosswartMassacre.Tests.Inventory.InventoryWorld;
namespace OpenAC.MosswartMassacre.Tests.Inventory;
public sealed class InventoryFeatureTests
{
private const string Prefix = "[Mosswart Massacre] ";
[Fact]
public void First_login_appraises_what_needs_it_and_sends_only_when_the_answers_are_in()
{
using var harness = new PluginHarness("Mossy", arrange: host =>
{
Own(host, Tachi(appraised: false));
Own(host, Tapers());
});
LoginAndSettle(harness);
harness.Tick(0.1);
Assert.Contains(Prefix + "Requesting id information for all armor/weapon inventory. This will take a few minutes...", harness.ChatLines);
Assert.Equal([0x80000202u], harness.Host.Automation.Objects.IdentifyRequests);
harness.Tick(0.1);
Assert.Empty(harness.FramesOfTypeRaw("full_inventory"));
Own(harness.Host, Tachi(appraised: true), TachiProps(), hostIdTime: 5000);
Raise(harness.Host, 0x80000202u, PluginObjectChangeKind.IdentReceived);
harness.Tick(0.1);
JObject frame = JObject.Parse(harness.FramesOfTypeRaw("full_inventory").Single());
Assert.Equal(2, (int)frame["item_count"]!);
Assert.True((bool)frame["items"]![0]!["HasIdData"]!);
Assert.Contains(Prefix + "Requesting id information for all armor/weapon inventory completed. Log file written.", harness.ChatLines);
Assert.Contains(Prefix + "Inventory sent to MosswartOverlord", harness.ChatLines);
Assert.NotNull(harness.Host.Storage.ReadText(InventoryFeature.CacheKey));
}
[Fact]
public void A_later_login_sends_at_once_and_keeps_the_saved_appraisal_of_an_unappraised_item()
{
using var harness = new PluginHarness("Mossy", arrange: host =>
Own(host, Tachi(appraised: true), TachiProps(), hostIdTime: 5000));
LoginAndSettle(harness);
harness.Tick(0.1);
harness.Tick(0.1);
Assert.Single(harness.FramesOfTypeRaw("full_inventory"));
// Relog: the session's appraisal is gone, the saved copy is not.
harness.Plugin.OnLogoff();
Own(harness.Host, Tachi(appraised: false));
harness.Frames.Clear();
harness.Host.Automation.Objects.IdentifyRequests.Clear();
LoginAndSettle(harness);
harness.Tick(0.1);
JObject frame = JObject.Parse(harness.FramesOfTypeRaw("full_inventory").Single());
JToken item = frame["items"]![0]!;
Assert.True((bool)item["HasIdData"]!);
Assert.Equal(50, (int)item["MaxDamage"]!);
Assert.Equal("Light Weapons", (string)item["EquipSkill"]!);
// Saved data present, so the original did not ask again from the dump
// (MossyInventory.cs:253); the login stream already asked once.
Assert.Equal([0x80000202u], harness.Host.Automation.Objects.IdentifyRequests);
}
[Fact]
public void Sendinventory_sends_at_once_when_everything_is_appraised()
{
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
LoginAndSettle(harness);
harness.Tick(0.1);
harness.Tick(0.1);
harness.Frames.Clear();
harness.Context.Commands.Dispatch("sendinventory");
Assert.Single(harness.FramesOfTypeRaw("full_inventory"));
Assert.Contains(Prefix + "[INV] All 1 items already identified. Sending...", harness.ChatLines);
Assert.Contains(Prefix + "[INV] Inventory upload completed", harness.ChatLines);
}
[Fact]
public void Sendinventory_waits_for_appraisals_and_refuses_when_streaming_is_off()
{
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
LoginAndSettle(harness);
harness.Tick(0.1);
harness.Tick(0.1);
harness.Frames.Clear();
Own(harness.Host, Ring());
harness.Context.Commands.Dispatch("sendinventory");
Assert.Contains(Prefix + "[INV] Scanning 2 items, requesting ID for 1. Waiting for identification...", harness.ChatLines);
harness.Tick(0.1);
Assert.Empty(harness.FramesOfTypeRaw("full_inventory"));
Own(harness.Host, Ring(), Props(ints: new() { [107] = 10, [108] = 20 }), hostIdTime: 42);
Raise(harness.Host, 0x80000404u, PluginObjectChangeKind.IdentReceived);
harness.Tick(0.1);
Assert.Single(harness.FramesOfTypeRaw("full_inventory"));
Assert.Contains(Prefix + "[INV] All items identified. Inventory upload completed.", harness.ChatLines);
harness.Context.Settings.Current.WebSocketEnabled = false;
harness.Context.Commands.Dispatch("sendinventory");
Assert.Contains(Prefix + "[INV] WebSocket streaming is disabled", harness.ChatLines);
}
[Fact]
public void The_login_stream_is_tracked_without_adds_and_items_after_it_are_added()
{
using var harness = new PluginHarness("Mossy");
harness.Login();
InventoryFeature feature = harness.Feature<InventoryFeature>();
feature.IsBackendReady = () => true;
Own(harness.Host, Tapers());
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Created);
Assert.False(feature.IsSettled);
for (int i = 0; i < 40 && !feature.IsSettled; i++)
harness.Tick(0.1);
Assert.Empty(harness.FramesOfTypeRaw("inventory_delta"));
Assert.Equal(1, feature.TrackedCount);
Own(harness.Host, Breastplate(), BreastplateProps(), hostIdTime: 9);
Raise(harness.Host, 0x80000303u, PluginObjectChangeKind.Created);
Assert.Equal("add", (string)JObject.Parse(harness.FramesOfTypeRaw("inventory_delta").Single())["action"]!);
}
[Fact]
public void Changes_are_held_until_the_jittered_flush_between_two_and_five_minutes()
{
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
InventoryFeature feature = LoginAndSettle(harness);
harness.Frames.Clear();
Own(harness.Host, Tapers(60));
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Updated);
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Updated);
Assert.Equal(1, feature.DirtyCount);
for (int i = 0; i < 1190; i++)
harness.Tick(0.1);
Assert.Empty(harness.FramesOfTypeRaw("inventory_delta"));
for (int i = 0; i < 1820 && harness.FramesOfTypeRaw("inventory_delta").Count == 0; i++)
harness.Tick(0.1);
JObject update = JObject.Parse(harness.FramesOfTypeRaw("inventory_delta").Single());
Assert.Equal("update", (string)update["action"]!);
Assert.Equal(60, (int)update["item"]!["IntValues"]!["218103814"]!);
}
[Fact]
public void The_flush_interval_is_two_minutes_plus_up_to_three()
{
using var harness = new PluginHarness("Mossy");
InventoryFeature feature = harness.Feature<InventoryFeature>();
int[] samples = Enumerable.Range(0, 2000).Select(_ => feature.NextFlushIntervalMs()).ToArray();
Assert.All(samples, ms => Assert.InRange(ms, 120000, 300000));
Assert.True(samples.Max() - samples.Min() > 150000);
}
[Fact]
public void An_item_moved_out_of_the_inventory_is_removed_and_one_in_a_side_pack_is_added()
{
const uint pack = 0x80000900u;
using var harness = new PluginHarness("Mossy", arrange: host =>
{
Own(host, Tapers());
host.Automation.Objects.Replace(new PluginWorldObject(pack, 1u, "Pack", PluginObjectClass.Container, 0x200u, Me, 0u));
});
LoginAndSettle(harness);
harness.Frames.Clear();
Own(harness.Host, Ring() with { ContainerObjectId = pack, WielderObjectId = 0u, EquippedLocation = 0u });
Raise(harness.Host, 0x80000404u, PluginObjectChangeKind.Moved);
Own(harness.Host, Tapers() with { ContainerObjectId = 0x7000AAAAu });
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Moved);
string[] actions = harness.FramesOfTypeRaw("inventory_delta")
.Select(f => (string)JObject.Parse(f)["action"]!).ToArray();
Assert.Equal(["add", "remove"], actions);
}
[Fact]
public void A_relog_starts_tracking_afresh()
{
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
InventoryFeature feature = LoginAndSettle(harness);
Own(harness.Host, Tapers(60));
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Updated);
Assert.Equal(1, feature.DirtyCount);
harness.Plugin.OnLogoff();
Assert.Equal(0, feature.DirtyCount);
Assert.Equal(0, feature.TrackedCount);
harness.Host.Automation.Character.Name = "Horan";
harness.Frames.Clear();
LoginAndSettle(harness);
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Updated);
Assert.Empty(harness.FramesOfTypeRaw("inventory_delta"));
Assert.Equal(1, feature.DirtyCount);
}
[Fact]
public void Logoff_sends_the_inventory_while_the_socket_is_still_up()
{
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
LoginAndSettle(harness);
harness.Tick(0.1);
harness.Tick(0.1);
harness.Frames.Clear();
harness.Host.Events.RaiseLogoff();
Assert.Single(harness.FramesOfTypeRaw("full_inventory"));
}
[Fact]
public void The_taper_count_follows_the_stacks_in_the_packs()
{
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
InventoryFeature feature = LoginAndSettle(harness);
Assert.Equal(57, feature.PrismaticTaperCount);
Own(harness.Host, Tapers(12));
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Updated);
Assert.Equal(12, feature.PrismaticTaperCount);
Forget(harness.Host, 0x80000101u);
harness.Host.Events.RaiseObjectChanged(new PluginObjectChange(0x80000101u, PluginObjectChangeKind.Released));
Assert.Equal(0, feature.PrismaticTaperCount);
}
[Fact]
public void Inventory_log_off_sends_no_login_inventory()
{
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
harness.Login();
harness.Context.Settings.Current.InventoryLog = false;
InventoryFeature feature = harness.Feature<InventoryFeature>();
feature.IsBackendReady = () => true;
for (int i = 0; i < 40; i++)
harness.Tick(0.1);
Assert.Empty(harness.FramesOfTypeRaw("full_inventory"));
}
[Fact]
public void The_appraisal_time_is_the_wall_clock_when_the_appraisal_arrived()
{
using var harness = new PluginHarness("Mossy");
InventoryFeature feature = LoginAndSettle(harness);
Own(harness.Host, Tachi(appraised: false));
Raise(harness.Host, 0x80000202u, PluginObjectChangeKind.Created);
Own(harness.Host, Tachi(appraised: true), TachiProps(), hostIdTime: 5000);
Raise(harness.Host, 0x80000202u, PluginObjectChangeKind.IdentReceived);
harness.Clock.Advance(TimeSpan.FromMinutes(3));
harness.Frames.Clear();
feature.FlushDirtyItems();
JObject update = JObject.Parse(harness.FramesOfTypeRaw("inventory_delta").Single());
Assert.Equal(1790325015, (int)update["item"]!["LastIdTime"]!);
}
}