From b544d5cccd76735e034c8055918a50927b437474 Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期二, 10 十一月 2026 08:29:04 +0800
Subject: [PATCH] fix: 维护模式重复进入失败与按钮灰死 + Broker 并发验证用例

---
 tests/Aging.MvpTests/LayeredResourceManagerTests.cs |   82 +++++++++++++++++++++++++++++++++++++++++
 1 files changed, 82 insertions(+), 0 deletions(-)

diff --git a/tests/Aging.MvpTests/LayeredResourceManagerTests.cs b/tests/Aging.MvpTests/LayeredResourceManagerTests.cs
new file mode 100644
index 0000000..0a64da4
--- /dev/null
+++ b/tests/Aging.MvpTests/LayeredResourceManagerTests.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IO;
+using System.Threading;
+using Aging.Infrastructure.Core;
+using Aging.Infrastructure.Contracts;
+using Xunit;
+
+namespace Aging.MvpTests
+{
+    public class LayeredResourceManagerTests : IDisposable
+    {
+        private readonly string _dbPath;
+        private readonly LayeredResourceManager _rm;
+
+        public LayeredResourceManagerTests()
+        {
+            _dbPath = Path.Combine(Path.GetTempPath(),
+                "arb_" + Guid.NewGuid().ToString("N") + ".db");
+            _rm = new LayeredResourceManager(new DbLeaseStore(_dbPath, "local-main"));
+        }
+
+        public void Dispose() { try { File.Delete(_dbPath); File.Delete(_dbPath + "-wal"); File.Delete(_dbPath + "-shm"); } catch { } }
+
+        [Fact]
+        public void ReserveAsync_Then_ValidateOwner_True()
+        {
+            var owner = new ResourceOwnerToken(OwnerType.Maintenance, "W01");
+            var key = new ResourceKey("CAN-002", "CAN");
+            var res = _rm.ReserveAsync(owner, key, CancellationToken.None)
+                         .GetAwaiter().GetResult();
+            Assert.Equal(ReservationState.Active, res.State);
+            Assert.True(_rm.ValidateOwner(owner, key));
+        }
+
+        [Fact]
+        public void ReserveAsync_Conflict_ExceptionCarriesHolder()
+        {
+            var a = new ResourceOwnerToken(OwnerType.Maintenance, "holder-A");
+            var b = new ResourceOwnerToken(OwnerType.Production, "requester-B");
+            var key = new ResourceKey("CAN-002", "CAN");
+            _rm.ReserveAsync(a, key, CancellationToken.None).GetAwaiter().GetResult();
+            var ex = Assert.Throws<ResourceConflictException>(() =>
+                _rm.ReserveAsync(b, key, CancellationToken.None).GetAwaiter().GetResult());
+            Assert.Contains("holder-A", ex.Message);
+        }
+
+        [Fact]
+        public void ValidateOwner_DoesNotShrinkLease_KeepsFullTtl()
+        {
+            // 鍥炲綊锛歏alidateOwner 鏇句互 Renew(grant,1) 鏍¢獙锛屼細鎶婄绾﹀埌鏈熺爫鍒� now+1s锛�
+            // 瀵艰嚧浣滅敤鍩熺绾﹀績璺� Renew 鍥犫�滃凡杩囨湡鈥濆け璐モ墺2 鈫� 璇姤鈥滅绾﹀凡澶辨晥鈥濄��
+            var owner = new ResourceOwnerToken(OwnerType.Production, "auto_x");
+            var key = new ResourceKey("CAN-001", "Generic");
+            _rm.ReserveAsync(owner, key, CancellationToken.None).GetAwaiter().GetResult();
+
+            // 妯℃嫙 DeviceService 姣忔鎿嶄綔鍓嶇殑鏍呮爮鏍¢獙
+            Assert.True(_rm.ValidateOwner(owner, key));
+
+            var holder = _rm.Coordinator.ProbeConflict(key.ToString());
+            Assert.NotNull(holder);
+            long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+            Assert.True(holder!.ExpiresUtcMs - nowMs > 60_000,
+                $"ValidateOwner 涓嶅簲鐮嶇煭绉熺害锛屽墿浣欏簲鈮圱TL锛屽疄闄呭墿浣� {(holder.ExpiresUtcMs - nowMs) / 1000}s");
+
+            // 鏍¢獙鍚庡績璺冲紡缁湡浠嶅簲鎴愬姛锛堢绾︽湭杩囨湡锛�
+            Assert.True(_rm.TryGetGrants(owner.TokenId, out var grants));
+            Assert.True(_rm.Coordinator.Renew(grants![0], 30));
+        }
+
+        [Fact]
+        public void ReleaseReservationAsync_Then_OtherCanReserve()
+        {
+            var a = new ResourceOwnerToken(OwnerType.Maintenance, "A");
+            var b = new ResourceOwnerToken(OwnerType.Production, "B");
+            var key = new ResourceKey("CAN-002", "CAN");
+            _rm.ReserveAsync(a, key, CancellationToken.None).GetAwaiter().GetResult();
+            _rm.ReleaseReservationAsync(a, key, CancellationToken.None).GetAwaiter().GetResult();
+            var ok = _rm.ReserveAsync(b, key, CancellationToken.None).GetAwaiter().GetResult();
+            Assert.Equal(ReservationState.Active, ok.State);
+        }
+    }
+}

--
Gitblit v1.9.1