diff --git a/AcDream.slnx b/AcDream.slnx
index 760229e..5a5029d 100644
--- a/AcDream.slnx
+++ b/AcDream.slnx
@@ -15,5 +15,6 @@
+
diff --git a/tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj b/tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj
new file mode 100644
index 0000000..f99c6e2
--- /dev/null
+++ b/tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/AcDream.UI.Abstractions.Tests/NullCommandBusTests.cs b/tests/AcDream.UI.Abstractions.Tests/NullCommandBusTests.cs
new file mode 100644
index 0000000..6f34d5e
--- /dev/null
+++ b/tests/AcDream.UI.Abstractions.Tests/NullCommandBusTests.cs
@@ -0,0 +1,22 @@
+namespace AcDream.UI.Abstractions.Tests;
+
+public sealed class NullCommandBusTests
+{
+ private sealed record FakeCmd(int Value);
+
+ [Fact]
+ public void Publish_DoesNotThrow_OnAnyRecordType()
+ {
+ var bus = NullCommandBus.Instance;
+
+ bus.Publish(new FakeCmd(42));
+ bus.Publish("a string command");
+ bus.Publish(12345);
+ }
+
+ [Fact]
+ public void Instance_IsSingleton()
+ {
+ Assert.Same(NullCommandBus.Instance, NullCommandBus.Instance);
+ }
+}
diff --git a/tests/AcDream.UI.Abstractions.Tests/PanelContextTests.cs b/tests/AcDream.UI.Abstractions.Tests/PanelContextTests.cs
new file mode 100644
index 0000000..2a665c9
--- /dev/null
+++ b/tests/AcDream.UI.Abstractions.Tests/PanelContextTests.cs
@@ -0,0 +1,24 @@
+namespace AcDream.UI.Abstractions.Tests;
+
+public sealed class PanelContextTests
+{
+ [Fact]
+ public void Fields_RoundTripThroughConstructor()
+ {
+ var ctx = new PanelContext(DeltaSeconds: 0.016f, Commands: NullCommandBus.Instance);
+
+ Assert.Equal(0.016f, ctx.DeltaSeconds);
+ Assert.Same(NullCommandBus.Instance, ctx.Commands);
+ }
+
+ [Fact]
+ public void RecordEquality_ByValue()
+ {
+ var a = new PanelContext(1f / 60f, NullCommandBus.Instance);
+ var b = new PanelContext(1f / 60f, NullCommandBus.Instance);
+
+ // Record-struct equality is value-based on DeltaSeconds + reference-based
+ // on Commands (since ICommandBus is a reference type, same instance → equal).
+ Assert.Equal(a, b);
+ }
+}
diff --git a/tests/AcDream.UI.Abstractions.Tests/VitalsVMTests.cs b/tests/AcDream.UI.Abstractions.Tests/VitalsVMTests.cs
new file mode 100644
index 0000000..3abf2c3
--- /dev/null
+++ b/tests/AcDream.UI.Abstractions.Tests/VitalsVMTests.cs
@@ -0,0 +1,80 @@
+using AcDream.Core.Combat;
+using AcDream.UI.Abstractions.Panels.Vitals;
+
+namespace AcDream.UI.Abstractions.Tests;
+
+public sealed class VitalsVMTests
+{
+ [Fact]
+ public void HealthPercent_ReturnsCombatStateValue_AfterUpdateHealth()
+ {
+ var combat = new CombatState();
+ uint guid = 0x5000_0042u;
+ combat.OnUpdateHealth(guid, 0.42f);
+
+ var vm = new VitalsVM(combat);
+ vm.SetLocalPlayerGuid(guid);
+
+ Assert.Equal(0.42f, vm.HealthPercent, precision: 3);
+ }
+
+ [Fact]
+ public void HealthPercent_ReturnsOne_WhenGuidUnknown()
+ {
+ var combat = new CombatState();
+ var vm = new VitalsVM(combat);
+
+ // No SetLocalPlayerGuid call — defaults to 0 which CombatState has never seen.
+ Assert.Equal(1f, vm.HealthPercent);
+ }
+
+ [Fact]
+ public void HealthPercent_ReturnsOne_WhenGuidSetButNeverUpdated()
+ {
+ var combat = new CombatState();
+ var vm = new VitalsVM(combat);
+ vm.SetLocalPlayerGuid(0xDEAD_BEEFu);
+
+ Assert.Equal(1f, vm.HealthPercent);
+ }
+
+ [Fact]
+ public void StaminaPercent_IsNull_ForD2aScope()
+ {
+ // D.2a explicitly defers Stamina until LocalPlayerState + PlayerDescription
+ // wiring. When that arrives VitalsVM.StaminaPercent becomes non-null and
+ // VitalsPanel starts drawing the Stam bar automatically.
+ var vm = new VitalsVM(new CombatState());
+ Assert.Null(vm.StaminaPercent);
+ }
+
+ [Fact]
+ public void ManaPercent_IsNull_ForD2aScope()
+ {
+ var vm = new VitalsVM(new CombatState());
+ Assert.Null(vm.ManaPercent);
+ }
+
+ [Fact]
+ public void SetLocalPlayerGuid_ReroutesHealthLookup_WithoutStaleCache()
+ {
+ // Simulate the realistic GameWindow flow: VM is constructed pre-login
+ // with GUID=0, then SetLocalPlayerGuid is called at EnterWorld.
+ var combat = new CombatState();
+ uint playerGuid = 0x5003_E219u;
+ combat.OnUpdateHealth(playerGuid, 0.75f);
+
+ var vm = new VitalsVM(combat);
+ // Before SetLocalPlayerGuid — reads GUID=0 → returns safe 1.0.
+ Assert.Equal(1f, vm.HealthPercent);
+
+ vm.SetLocalPlayerGuid(playerGuid);
+ Assert.Equal(0.75f, vm.HealthPercent, precision: 3);
+ }
+
+ [Fact]
+ public void Constructor_ThrowsOnNullCombat()
+ {
+ Assert.Throws(() => new VitalsVM(null!));
+ }
+}