using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S6.Manufacturing; /// /// S6 Phase 1B 源码契约测试:解析器 map-only + tenant-safe;生成服务 事务/幂等/快照/仅MATCHED写;前端无人工选择器。 /// public class S6ProcessInspectionContractTests { private static string Resolver() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "S6ProcessSpecResolver.cs")); private static string Service() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "S6ProcessInspectionService.cs")); private static string Vue() => File.ReadAllText(FindFile("Web", "src", "views", "aidop", "s6", "production-record", "productionInstructionList.vue")); [Fact] public void Resolver_RuntimeReadsMapTable_TenantAndItemScoped() { var s = Resolver(); Assert.Contains("FROM ado_s6_ipqc_spec_material_map", s); Assert.Contains("m.tenant_id=@TenantId AND m.item_code=@ItemCode AND m.match_status='EXACT'", s); Assert.Contains("g.tenant_id=@TenantId", s); // 检规同租户 } [Fact] public void Resolver_NoIdDescFallback_NoManualPick() { var s = Resolver(); Assert.DoesNotContain("ORDER BY g.id DESC", s); Assert.DoesNotContain("ORDER BY id DESC LIMIT 1", s); // 裁决按 wjbh 分组 Assert.Contains("GroupBy(c => (c.Wjbh", s); } [Fact] public void Service_StrictTenantScope_EveryQuery() { var s = Service(); Assert.Contains("AidopTenantScope.ResolveOrThrow", s); Assert.DoesNotContain("tenant_id IS NULL", s); Assert.DoesNotContain("tenant_id = 0", s); var n = s.Split("tenant_id=@TenantId").Length - 1; Assert.True(n >= 4, $"expected tenant predicate on load/idempotency/insert/snapshot/detail, found {n}"); } [Fact] public void Service_OnlyMatchedWrites() { var s = Service(); Assert.Contains("resolve.Status != S6SpecResolveStatus.Matched", s); Assert.Contains("return res; // 非唯一解析:不写库", s); Assert.Contains("resolve.CandidateCount != 1", s); } [Fact] public void Service_TransactionalGeneration() { var s = Service(); Assert.Contains("BeginTran()", s); Assert.Contains("CommitTran()", s); Assert.Contains("RollbackTran()", s); } [Fact] public void Service_IdempotentReuseActiveBill() { var s = Service(); Assert.Contains("inspection_status IN ('DRAFT','ACTIVE')", s); Assert.Contains("Reused = true", s); } [Fact] public void Service_SnapshotFreezesFromS0Detail_WithResultType() { var s = Service(); Assert.Contains("INSERT INTO ado_s6_process_inspection_snapshot_head", s); Assert.Contains("INSERT INTO ado_s6_process_inspection_snapshot_line", s); Assert.Contains("FROM qms_gcjygfzb z", s); // 快照冻结自 S0 明细 Assert.Contains("z.lrlx", s); // ResultType 进快照 } [Fact] public void Frontend_NoManualSpecSelector_CallsPrepare() { var v = Vue(); Assert.Contains("prepareProcessInspection", v); Assert.DoesNotContain(":disabled=\"true\"", v); // 按钮不再禁用 // 无人工检规下拉/选择器 Assert.DoesNotContain("specSelect", v); Assert.DoesNotContain("选择检规", v); } 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)); } }