using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S6.Manufacturing; /// /// S6 Phase 1A · 生产指令中台消费层契约测试(源码契约断言,无需活库)。 /// 覆盖:STD 扩列 + specification enrich(Domain+ItemNum)、消费层强租户隔离、只读中台不直连源库、真分页、筛选。 /// public class S6ProductionInstructionContractTests { private static string ReadS2Transform() => File.ReadAllText(FindFile( "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Production", "S2MdpSyncTransformService.cs")); private static string ReadService() => File.ReadAllText(FindFile( "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "S6ProductionInstructionService.cs")); [Fact] public void Std_Schedule_HasNewNullableColumns() { var s2 = ReadS2Transform(); Assert.Contains("specification VARCHAR(200) NULL", s2); Assert.Contains("lot_serial VARCHAR(100) NULL", s2); Assert.Contains("drawing_no VARCHAR(100) NULL", s2); Assert.Contains("project VARCHAR(200) NULL", s2); Assert.Contains("work_order_type VARCHAR(50) NULL", s2); Assert.Contains("labor_variance DECIMAL(18,6) NULL", s2); // 唯一键不变 Assert.Contains("uk_std_work_order_schedule (tenant_id, work_order)", s2); } [Fact] public void Specification_EnrichedFromItemMaster_ByDomainAndItemNum_NotItemNumOnly() { var s2 = ReadS2Transform(); // specification 来自 ItemMaster.Descr1 Assert.Contains("im.Descr1", s2); Assert.Contains("FROM ItemMaster", s2); // 关联必须同时按 Domain + ItemNum(去重取一,实测同键 Descr1 无冲突) Assert.Contains("GROUP BY `Domain`, ItemNum", s2); Assert.Contains("im.`Domain` = JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.Domain'))", s2); Assert.Contains("im.ItemNum = JSON_UNQUOTE(JSON_EXTRACT(s.raw_data,'$.ItemNum'))", s2); // 新列进入 upsert Assert.Contains("specification=VALUES(specification)", s2); Assert.Contains("lot_serial=VALUES(lot_serial)", s2); } [Fact] public void EnvelopeSourcedColumns_MappedFromRawData() { var s2 = ReadS2Transform(); Assert.Contains("JSON_EXTRACT(s.raw_data,'$.LotSerial')", s2); Assert.Contains("JSON_EXTRACT(s.raw_data,'$.Drawing')", s2); Assert.Contains("JSON_EXTRACT(s.raw_data,'$.Project')", s2); Assert.Contains("JSON_EXTRACT(s.raw_data,'$.WoTyped')", s2); Assert.Contains("JSON_EXTRACT(s.raw_data,'$.LbrVar')", s2); } [Fact] public void Consumer_UsesStrictTenantScope_OnEverySql() { var svc = ReadService(); Assert.Contains("AidopTenantScope.ResolveOrThrow", svc); Assert.Contains("m.tenant_id = @TenantId", svc); // COUNT / PAGE / DETAIL 都带租户过滤:至少出现 2 处显式 tenant 谓词(page where + detail where) var occurrences = svc.Split("m.tenant_id = @TenantId").Length - 1; Assert.True(occurrences >= 2, $"expected tenant predicate on page+detail, found {occurrences}"); } [Fact] public void Consumer_ReadsMdpStdOnly_NeverSourceTables() { var svc = ReadService(); Assert.Contains("FROM mdp_std_work_order_schedule", svc); Assert.DoesNotContain("WorkOrdMaster", svc); Assert.DoesNotContain("ItemMaster", svc); Assert.DoesNotContain("mes_morder", svc); } [Fact] public void Consumer_HasNoTenantFallback_AndNoSelectStar() { var svc = ReadService(); Assert.DoesNotContain("tenant_id IS NULL", svc); Assert.DoesNotContain("tenant_id = 0", svc); Assert.DoesNotContain("SELECT *", svc); // 不硬编码工厂 Assert.DoesNotContain("factory_id = 1", svc); Assert.DoesNotContain("factory_id = 8010", svc); } [Fact] public void Consumer_TruePagination_AndFilters() { var svc = ReadService(); Assert.Contains("LIMIT {pageSize} OFFSET {offset}", svc); Assert.Contains("SELECT COUNT(1) FROM mdp_std_work_order_schedule", svc); Assert.Contains("m.work_order LIKE @WorkOrderNo", svc); Assert.Contains("m.lot_serial LIKE @LotSerial", svc); Assert.Contains("m.item_code LIKE @ItemCode", svc); Assert.Contains("m.status = @Status", svc); Assert.Contains("m.order_date >= @OrderDateStart", svc); Assert.Contains("DATE_ADD(@OrderDateEnd, INTERVAL 1 DAY)", svc); Assert.Contains("ORDER BY m.order_date DESC, m.work_order DESC", svc); } private static string FindFile(params string[] parts) { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null) { var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray()); if (File.Exists(candidate)) return candidate; dir = dir.Parent; } throw new FileNotFoundException(string.Join("/", parts)); } }