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(() => _rm.ReserveAsync(b, key, CancellationToken.None).GetAwaiter().GetResult()); Assert.Contains("holder-A", ex.Message); } [Fact] public void ValidateOwner_DoesNotShrinkLease_KeepsFullTtl() { // 回归:ValidateOwner 曾以 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 不应砍短租约,剩余应≈TTL,实际剩余 {(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); } } }