using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S6.Manufacturing; /// S6 Phase 1C 录入/判定/完成 源码契约测试:快照唯一判定源、后端判定、幂等、事务、完成校验、tenant、入参不信任判定。 public class S6ProcessInspectionEntryContractTests { private static string Svc() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "S6ProcessInspectionService.cs")); private static string Dto() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "Dto", "S6ProcessInspectionDto.cs")); private static string Judge() => File.ReadAllText(FindFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "S6ProcessInspectionJudge.cs")); [Fact] public void Save_BackendJudges_FromSnapshotConfig_NotLiveSpec() { var s = Svc(); Assert.Contains("S6ProcessInspectionJudge.JudgeSample", s); Assert.Contains("LoadSnapshotConfigAsync", s); Assert.Contains("FROM ado_s6_process_inspection_snapshot_line", s); // 判定路径不回查 S0 检规 Assert.DoesNotContain("FROM qms_gcjygfzb z\n WHERE z.tenant_id=@TenantId AND z.glid=@SpecId AND", s); // 仅生成时读,判定不读 Assert.Contains("result_type AS ResultType, lower_limit AS LowerLimit, upper_limit AS UpperLimit, sample_count AS SampleCount", s); } [Fact] public void SaveInput_DoesNotAccept_JudgementOrTenantOrLimits() { var d = Dto(); // 入参 DTO 只有 SnapshotLineId/SampleIndex/ActualValue/Remark Assert.Contains("public long SnapshotLineId", d); Assert.Contains("public string? ActualValue", d); Assert.DoesNotContain("public string? Judgement { get; set; }\n public long TenantId", d); // S6SaveResultSample 不含 Judgement / TenantId / LowerLimit / OverallResult var sampleClass = d.Substring(d.IndexOf("class S6SaveResultSample")); sampleClass = sampleClass.Substring(0, sampleClass.IndexOf("}") + 1); Assert.DoesNotContain("Judgement", sampleClass); Assert.DoesNotContain("TenantId", sampleClass); Assert.DoesNotContain("LowerLimit", sampleClass); } [Fact] public void Save_TenantScoped_AndTransactional() { var s = Svc(); Assert.Contains("AidopTenantScope.ResolveOrThrow", s); Assert.Contains("BeginTran()", s); Assert.Contains("CommitTran()", s); Assert.Contains("RollbackTran()", s); Assert.DoesNotContain("tenant_id IS NULL", s); // 结果表读写均带 tenant Assert.Contains("WHERE tenant_id=@t AND bill_id=@bill", s); } [Fact] public void Save_IdempotentUpsert_ByBillLineSample() { var s = Svc(); Assert.Contains("INSERT INTO ado_s6_process_inspection_result_item", s); Assert.Contains("ON DUPLICATE KEY UPDATE", s); // UK(bill,line,sample) 幂等 } [Fact] public void Complete_EnforcesCompleteness() { var s = Svc(); Assert.Contains("AllItemsCompleted", s); Assert.Contains("INSPECTION_INCOMPLETE", s); } [Fact] public void Header_CompletedAt_ConsistentWithStatus() { // completed_at 只在 COMPLETED 有值;回退时清空,避免 IN_PROGRESS 残留完成时间。 var s = Svc(); Assert.Contains("completed_at=CASE WHEN @status='COMPLETED' THEN @completedAt ELSE NULL END", s); } [Fact] public void Overall_And_Item_BackendComputed() { var s = Svc(); Assert.Contains("AggregateItem", s); Assert.Contains("AggregateOverall", s); Assert.Contains("RecomputeAndPersistHeaderAsync", s); } [Fact] public void Judge_NonNumeric_AutoOkPassNgFail() { var j = Judge(); Assert.Contains("\"OK\" => S6Judgement.Pass", j); Assert.Contains("\"NG\" => S6Judgement.Fail", j); // NUMERIC 无界限 → INVALID(不默认 PASS) Assert.Contains("if (lo == null && up == null) return S6Judgement.Invalid", j); Assert.Contains("if (actual == null) return S6Judgement.Invalid", j); } 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)); } }