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;
}
}