L0LiveLoaderIntegrationTests.cs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. using System.Text.RegularExpressions;
  2. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Idempotency;
  3. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.L0;
  4. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.L1;
  5. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Receipt;
  6. using SqlSugar;
  7. using Xunit;
  8. namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
  9. /// <summary>
  10. /// Phase 5B-2 L0 实库集成测试:用**生产类**连真实 aidopdev 验证
  11. /// ① MdpIqcReceiptStateLoader/SqlSugarIqcReceiptStdReader 从 mdp_std_iqc_receipt_state 读回正确状态;
  12. /// ② IqcInventoryEventConsumer 的 freshness 门禁:标准层同步水位落后事件 → Retryable(旧快照不过账)。
  13. /// **门禁**:仅当环境变量 AIDOP_IT=1 时执行(无 DB 的普通 CI 跳过,不失败)。std 测试行由 5B-2 CLI 预置(P5B_RCV / 真实6行)。
  14. /// </summary>
  15. [Trait("Category", "Integration")]
  16. public class L0LiveLoaderIntegrationTests
  17. {
  18. private const long T = 950000000000001L;
  19. private static bool Enabled => Environment.GetEnvironmentVariable("AIDOP_IT") == "1";
  20. private static ISqlSugarClient BuildAidopdev()
  21. {
  22. const string path = "/home/yy968/work/New9S/AiDOPWarehouse/server/Admin.NET.Application/Configuration/Database.json";
  23. if (!File.Exists(path)) return null;
  24. // 取活动(非注释)的 aidopdev 连接串——凭据来自项目配置,不写入测试源码
  25. var cs = File.ReadLines(path)
  26. .Select(l => l.Trim())
  27. .Where(l => !l.StartsWith("//") && l.Contains("\"ConnectionString\"") && l.Contains("Database=aidopdev") && l.Contains("123.60.180.165"))
  28. .Select(l => Regex.Match(l, "\"ConnectionString\"\\s*:\\s*\"([^\"]+)\"").Groups[1].Value)
  29. .FirstOrDefault(v => !string.IsNullOrEmpty(v));
  30. if (string.IsNullOrEmpty(cs)) return null;
  31. return new SqlSugarScope(new ConnectionConfig
  32. {
  33. ConfigId = "aidopdev-it", DbType = DbType.MySql, ConnectionString = cs, IsAutoCloseConnection = true,
  34. });
  35. }
  36. private static IqcInventoryEvent Evt(DateTime when) => new IqcInventoryEvent
  37. {
  38. TenantId = T, DomainCode = "P5B", Fbillno = "P5B_RCV", Receiver = "P5B_RCTNBR", RctQcNbr = "P5B_QC",
  39. Pd = 0, Clfs = "", Dhsl = 100m, Bhgsl = 0m, User = "it", EventTime = when,
  40. };
  41. // ① 生产 loader 读回受控可收行(真实 aidopdev)
  42. [Fact]
  43. public async Task Live_Loader_ReadsControlledReceivableState()
  44. {
  45. if (!Enabled) return;
  46. var db = BuildAidopdev();
  47. Assert.NotNull(db);
  48. var loader = new MdpIqcReceiptStateLoader(new SqlSugarIqcReceiptStdReader(db));
  49. var st = await loader.LoadAsync(T, "P5B", "P5B_RCV");
  50. Assert.True(st.Found);
  51. Assert.True(st.HasPendingReceipt); // receipt_pending_qty=8>0
  52. Assert.True(st.HasReceivableDetails); // receivable_detail_count=4>0
  53. Assert.Equal("P5B_IT", st.ItemNum);
  54. Assert.Equal("P5B_L1", st.Location);
  55. Assert.Equal("PO-RCV", st.PurOrd);
  56. Assert.Equal(1, st.PurLine);
  57. Assert.Equal(100m, st.OrderedQty);
  58. Assert.Equal(20m, st.BeforeReceivedQty); // received_cum_qty
  59. Assert.Equal(2m, st.ReturnedQty);
  60. Assert.Equal(5m, st.SampleQty);
  61. Assert.Equal("", st.Refs); // UNRESOLVED 恒空
  62. Assert.NotNull(st.SourceSyncedAt);
  63. }
  64. // ① 生产 loader 读回真实贴源行(未匹配收货 → 正确反映为不可收)
  65. [Fact]
  66. public async Task Live_Loader_ReadsRealStdRow_NotReceivable()
  67. {
  68. if (!Enabled) return;
  69. var db = BuildAidopdev();
  70. Assert.NotNull(db);
  71. var loader = new MdpIqcReceiptStateLoader(new SqlSugarIqcReceiptStdReader(db));
  72. var st = await loader.LoadAsync(T, "8010", "INSP-UAT-20260604-01");
  73. Assert.True(st.Found);
  74. Assert.False(st.HasPendingReceipt); // 真实数据链未连通 → 收货空
  75. Assert.False(st.HasReceivableDetails);
  76. }
  77. // ② freshness:标准层水位(2026-08-09)落后事件(2026-08-10) → Retryable、不过账
  78. [Fact]
  79. public async Task Live_Freshness_StaleStd_BlocksPosting()
  80. {
  81. if (!Enabled) return;
  82. var db = BuildAidopdev();
  83. Assert.NotNull(db);
  84. var (fakeDb, consumer) = ConsumerWithRealLoader(db);
  85. var r = await consumer.ConsumeAsync(Evt(new DateTime(2026, 8, 10))); // 事件晚于 sync_time
  86. Assert.Equal(IqcConsumeOutcome.Retryable, r.Outcome);
  87. Assert.Equal(0, fakeDb.TotalPersisted); // 旧快照未过账
  88. }
  89. // ② freshness:水位不落后(事件 2026-08-08 <= sync_time) → 通过门禁进入编排(Processed)
  90. [Fact]
  91. public async Task Live_Freshness_CurrentStd_Proceeds()
  92. {
  93. if (!Enabled) return;
  94. var db = BuildAidopdev();
  95. Assert.NotNull(db);
  96. var (fakeDb, consumer) = ConsumerWithRealLoader(db);
  97. var r = await consumer.ConsumeAsync(Evt(new DateTime(2026, 8, 8))); // 事件早于 sync_time
  98. Assert.Equal(IqcConsumeOutcome.Processed, r.Outcome);
  99. Assert.Equal(1, fakeDb.InvMaster.Count);
  100. }
  101. // 真实 loader + 内存幂等/完成锚 + Fake 编排(freshness 门禁在编排前触发;编排仅在通过后执行)
  102. private static (FakeInventoryDatabase, IqcInventoryEventConsumer) ConsumerWithRealLoader(ISqlSugarClient db)
  103. {
  104. var fdb = new FakeInventoryDatabase();
  105. fdb.SeedMaster(T, "P5B", "P5B_IT", "P5B_L1");
  106. var store = new InMemoryIqcPostingStore();
  107. var idem = new IqcPostingIdempotencyService(store);
  108. var factory = new FakeIqcReceiptUnitOfWorkFactory(fdb);
  109. var loader = new MdpIqcReceiptStateLoader(new SqlSugarIqcReceiptStdReader(db));
  110. var consumer = new IqcInventoryEventConsumer(
  111. idem, new InMemoryBusinessCompletionStore(fdb), loader,
  112. new IqcReceiptOrchestrator(factory), new IqcMrbSelectionTaskService(factory));
  113. return (fdb, consumer);
  114. }
  115. }