| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- 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;
- /// <summary>
- /// 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行)。
- /// </summary>
- [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);
- }
- }
|