using System.Text.RegularExpressions; using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Idempotency; using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.L0; using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.L1; using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt; using SqlSugar; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse; /// /// Phase 5B-2 L0 实库集成测试:用**生产类**连真实 aidopdev 验证 /// ① MdpIqcReceiptStateLoader/SqlSugarIqcReceiptStdReader 从 mdp_std_iqc_receipt_state 读回正确状态; /// ② IqcInventoryEventConsumer 的 freshness 门禁:标准层同步水位落后事件 → Retryable(旧快照不过账)。 /// **门禁**:仅当环境变量 AIDOP_IT=1 时执行(无 DB 的普通 CI 跳过,不失败)。std 测试行由 5B-2 CLI 预置(P5B_RCV / 真实6行)。 /// [Trait("Category", "Integration")] public class L0LiveLoaderIntegrationTests { private const long T = 950000000000001L; private static bool Enabled => Environment.GetEnvironmentVariable("AIDOP_IT") == "1"; private static ISqlSugarClient BuildAidopdev() { const string path = "/home/yy968/work/New9S/AiDOPWarehouse/server/Admin.NET.Application/Configuration/Database.json"; if (!File.Exists(path)) return null; // 取活动(非注释)的 aidopdev 连接串——凭据来自项目配置,不写入测试源码 var cs = File.ReadLines(path) .Select(l => l.Trim()) .Where(l => !l.StartsWith("//") && l.Contains("\"ConnectionString\"") && l.Contains("Database=aidopdev") && l.Contains("123.60.180.165")) .Select(l => Regex.Match(l, "\"ConnectionString\"\\s*:\\s*\"([^\"]+)\"").Groups[1].Value) .FirstOrDefault(v => !string.IsNullOrEmpty(v)); if (string.IsNullOrEmpty(cs)) return null; return new SqlSugarScope(new ConnectionConfig { ConfigId = "aidopdev-it", DbType = DbType.MySql, ConnectionString = cs, IsAutoCloseConnection = true, }); } private static IqcInventoryEvent Evt(DateTime when) => new IqcInventoryEvent { TenantId = T, DomainCode = "P5B", Fbillno = "P5B_RCV", Receiver = "P5B_RCTNBR", RctQcNbr = "P5B_QC", Pd = 0, Clfs = "", Dhsl = 100m, Bhgsl = 0m, User = "it", EventTime = when, }; // ① 生产 loader 读回受控可收行(真实 aidopdev) [Fact] public async Task Live_Loader_ReadsControlledReceivableState() { if (!Enabled) return; var db = BuildAidopdev(); Assert.NotNull(db); var loader = new MdpIqcReceiptStateLoader(new SqlSugarIqcReceiptStdReader(db)); var st = await loader.LoadAsync(T, "P5B", "P5B_RCV"); Assert.True(st.Found); Assert.True(st.HasPendingReceipt); // receipt_pending_qty=8>0 Assert.True(st.HasReceivableDetails); // receivable_detail_count=4>0 Assert.Equal("P5B_IT", st.ItemNum); Assert.Equal("P5B_L1", st.Location); Assert.Equal("PO-RCV", st.PurOrd); Assert.Equal(1, st.PurLine); Assert.Equal(100m, st.OrderedQty); Assert.Equal(20m, st.BeforeReceivedQty); // received_cum_qty Assert.Equal(2m, st.ReturnedQty); Assert.Equal(5m, st.SampleQty); Assert.Equal("", st.Refs); // UNRESOLVED 恒空 Assert.NotNull(st.SourceSyncedAt); } // ① 生产 loader 读回真实贴源行(未匹配收货 → 正确反映为不可收) [Fact] public async Task Live_Loader_ReadsRealStdRow_NotReceivable() { if (!Enabled) return; var db = BuildAidopdev(); Assert.NotNull(db); var loader = new MdpIqcReceiptStateLoader(new SqlSugarIqcReceiptStdReader(db)); var st = await loader.LoadAsync(T, "8010", "INSP-UAT-20260604-01"); Assert.True(st.Found); Assert.False(st.HasPendingReceipt); // 真实数据链未连通 → 收货空 Assert.False(st.HasReceivableDetails); } // ② freshness:标准层水位(2026-08-09)落后事件(2026-08-10) → Retryable、不过账 [Fact] public async Task Live_Freshness_StaleStd_BlocksPosting() { if (!Enabled) return; var db = BuildAidopdev(); Assert.NotNull(db); var (fakeDb, consumer) = ConsumerWithRealLoader(db); var r = await consumer.ConsumeAsync(Evt(new DateTime(2026, 8, 10))); // 事件晚于 sync_time Assert.Equal(IqcConsumeOutcome.Retryable, r.Outcome); Assert.Equal(0, fakeDb.TotalPersisted); // 旧快照未过账 } // ② freshness:水位不落后(事件 2026-08-08 <= sync_time) → 通过门禁进入编排(Processed) [Fact] public async Task Live_Freshness_CurrentStd_Proceeds() { if (!Enabled) return; var db = BuildAidopdev(); Assert.NotNull(db); var (fakeDb, consumer) = ConsumerWithRealLoader(db); var r = await consumer.ConsumeAsync(Evt(new DateTime(2026, 8, 8))); // 事件早于 sync_time Assert.Equal(IqcConsumeOutcome.Processed, r.Outcome); Assert.Equal(1, fakeDb.InvMaster.Count); } // 真实 loader + 内存幂等/完成锚 + Fake 编排(freshness 门禁在编排前触发;编排仅在通过后执行) private static (FakeInventoryDatabase, IqcInventoryEventConsumer) ConsumerWithRealLoader(ISqlSugarClient db) { var fdb = new FakeInventoryDatabase(); fdb.SeedMaster(T, "P5B", "P5B_IT", "P5B_L1"); var store = new InMemoryIqcPostingStore(); var idem = new IqcPostingIdempotencyService(store); var factory = new FakeIqcReceiptUnitOfWorkFactory(fdb); var loader = new MdpIqcReceiptStateLoader(new SqlSugarIqcReceiptStdReader(db)); var consumer = new IqcInventoryEventConsumer( idem, new InMemoryBusinessCompletionStore(fdb), loader, new IqcReceiptOrchestrator(factory), new IqcMrbSelectionTaskService(factory)); return (fdb, consumer); } }