using Admin.NET.Plugin.AiDOP.MaterialWarehouse; using Microsoft.Extensions.Logging.Abstractions; using SqlSugar; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.MaterialWarehouse; /// /// 库存物化有界等待锁的进程内行为。不连真实库。 /// 跨连接互斥留在 S5 集成测试,仅当 AIDOP_IT=1 时执行。 /// public class InventoryInboundLockGuardAcquireTests { [Fact] public void ProductionDefaults_AreTenMinutesAndTwoSeconds() { Assert.Equal(TimeSpan.FromMinutes(10), InventoryInboundLockGuard.DefaultWaitTimeout); Assert.Equal(TimeSpan.FromSeconds(2), InventoryInboundLockGuard.DefaultPollInterval); } [Fact] public async Task AcquireAsync_PreCancelledToken_StopsBeforeWaiting() { using var db = ClosedClient(); using var cts = new CancellationTokenSource(); cts.Cancel(); var started = DateTime.UtcNow; await Assert.ThrowsAnyAsync(() => InventoryInboundLockGuard.AcquireAsync( db, NullLogger.Instance, "aidop:t:inv-in:cancel", TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(50), cts.Token)); Assert.True(DateTime.UtcNow - started < TimeSpan.FromSeconds(2)); } [Fact] public async Task AcquireAsync_UnreachableDatabase_TimesOutWithFixedMessage() { using var db = ClosedClient(); var ex = await Assert.ThrowsAsync(() => InventoryInboundLockGuard.AcquireAsync( db, NullLogger.Instance, "aidop:t:inv-in:timeout", TimeSpan.FromMilliseconds(200), TimeSpan.FromMilliseconds(50))); Assert.Equal("inventory materialization lock busy", ex.Message); } [Fact] public void TenantMaterializeEntry_WaitsForLock_BackgroundPathsStayNonBlocking() { var src = File.ReadAllText(Path.Combine(RepoRoot(), "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "InventoryMdpSyncService.cs")); var method = Slice(src, "public async Task TransformTransStdFromStgAsync(", "private async Task RunAsync"); Assert.Contains("InventoryInboundLockGuard.AcquireAsync(", method, StringComparison.Ordinal); Assert.DoesNotContain("TryAcquireAsync", method, StringComparison.Ordinal); Assert.True(Count(src, "InventoryInboundLockGuard.TryAcquireAsync(") >= 3); } private static SqlSugarClient ClosedClient() => new(new ConnectionConfig { DbType = DbType.MySql, ConnectionString = "Server=127.0.0.1;Port=1;User ID=x;Password=x;Database=x;Connection Timeout=1;Pooling=false;", IsAutoCloseConnection = true, }); private static string RepoRoot() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AGENTS.md"))) dir = dir.Parent; return dir?.FullName ?? throw new InvalidOperationException("repo root not found"); } private static string Slice(string src, string start, string end) { var i = src.IndexOf(start, StringComparison.Ordinal); var j = src.IndexOf(end, i, StringComparison.Ordinal); Assert.True(i >= 0 && j > i); return src[i..j]; } private static int Count(string src, string token) { var n = 0; var i = 0; while ((i = src.IndexOf(token, i, StringComparison.Ordinal)) >= 0) { n++; i += token.Length; } return n; } }