using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse; /// /// P1-A 源码契约测试:qms_qcp_inspbill.FBILLSTATUS 完成态回写。 /// /// 固化四条不变量: /// ① 终态只由流程结束回调 OnFlowCompleted 写,且仅 Approved 才写; /// ② NG 单不得在主管通过(N2)时提前写完成 —— 靠「只在实例终结回调里写」结构性保证, /// 故 supervisor-approve / sqe-submit-disposition 所在的 Service 内不得出现 FBILLSTATUS 写入; /// ③ 取值复用既有 done-word '检验完成',不自造新枚举、不采用无写入者的 COMPLETED/C/A/NG; /// ④ 回写必须租户隔离,且不越界改 pd/clfs/FINSPECTSTATUS。 /// public class IqcBillStatusCompletionContractTests { private static string Handler() => File.ReadAllText(FindFile( "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "IqcInspBillFlowBizHandler.cs")); private static string FlowService() => File.ReadAllText(FindFile( "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "IqcInspBillFlowService.cs")); private static string TaskEntryService() => File.ReadAllText(FindFile( "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "IqcTaskEntryService.cs")); private static string InspBillService() => File.ReadAllText(FindFile( "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "IqcInspBillService.cs")); // ── ① 终态只在 OnFlowCompleted 写,且仅 Approved ── [Fact] public void Handler_WritesBillStatus_OnlyOnApprovedCompletion() { var s = Handler(); Assert.Contains("public async Task OnFlowCompleted", s); Assert.Contains("if (finalStatus != FlowInstanceStatusEnum.Approved) return;", s); Assert.Contains("UPDATE qms_qcp_inspbill SET FBILLSTATUS=@Status", s); } // ── ② NG 不得提前完成:Service 侧不得出现 FBILLSTATUS 写入 ── [Fact] public void FlowService_DoesNotWriteBillStatus() { var s = FlowService(); Assert.DoesNotContain("SET FBILLSTATUS", s); Assert.DoesNotContain("FBILLSTATUS=", s); // submit-result 仍只写这四列 Assert.Contains("UPDATE qms_qcp_inspbill SET pd=@pd, dhsl=@dhsl, bhgsl=@bhgsl, clfs=@clfs", s); } /// 生成时仍写 '检验中',是唯一的初始态写入点。 [Fact] public void GenerateInspBill_StillWritesInProgressStatus() { var s = TaskEntryService(); Assert.Contains("'检验中'", s); Assert.DoesNotContain("SET FBILLSTATUS", s); } // ── ③ 取值复用既有 done-word,不自造枚举 ── [Fact] public void BillStatusDone_ReusesExistingVocabulary() { var s = Handler(); Assert.Contains("public const string BillStatusDone = \"检验完成\";", s); Assert.Contains("IqcInspBillFlowConst.BillStatusDone", s); // 不得引入库内那些无代码写入者的历史 seed 值 Assert.DoesNotContain("\"COMPLETED\"", s); Assert.DoesNotContain("\"CREATED\"", s); } // ── ④ 租户隔离 + 不越界 ── [Fact] public void Handler_CompletionWrite_IsTenantScoped() { var s = Handler(); Assert.Contains("WHERE id=@id AND tenant_id=@TenantId", s); Assert.Contains("_userManager.TenantId", s); } [Fact] public void Handler_DoesNotTouchOtherBusinessFields() { var s = Handler(); // 不改判定/处理方式/审核人/报检分录状态 Assert.DoesNotContain("SET pd=", s); Assert.DoesNotContain("SET clfs=", s); Assert.DoesNotContain("FAUDITORID=", s); Assert.DoesNotContain("qms_qcp_insappnentry", s); } /// 详情接口仍原样返回 FBILLSTATUS,不得在 GET 侧动态伪造完成态。 [Fact] public void DetailApi_ReturnsRawBillStatus_NoDerivedCompletion() { var s = InspBillService(); Assert.Contains("ib.FBILLSTATUS AS BillStatus", s); Assert.DoesNotContain("检验完成", s); } /// B-2 检规快照逻辑未被本次修改波及。 [Fact] public void SpecSnapshotColumns_StillWrittenOnlyAtGeneration() { var gen = TaskEntryService(); Assert.Contains("jgbh, jgbb, jgsxrq, jygfid", gen); Assert.Contains("spec.IsMatched", gen); var handler = Handler(); Assert.DoesNotContain("jgbh", handler); Assert.DoesNotContain("jygfid", handler); } 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)); } }