S6IpqcDisplayContractTests.cs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. using Xunit;
  2. namespace Admin.NET.Plugin.AiDOP.Tests.S6.Manufacturing;
  3. /// <summary>
  4. /// S6 DISPLAY FIX · Batch 1 源码契约测试。
  5. ///
  6. /// D-04:IPQC head transform 的字符串列必须经 <c>MdpJsonSql.Str</c> 把 JSON null 归一为 SQL NULL,
  7. /// 否则 <c>JSON_UNQUOTE(JSON_EXTRACT(...))</c> 会产出字面量字符串 'null' 并一路透到页面。
  8. /// detail transform 早已正确,本测试同时钉住其不回归。
  9. /// D-01:完成态 message 必须区分 APPROVED(整批合格)与 DISPOSED(整批不合格已处置),
  10. /// 不得只依据 ApprovalFlowInstance.Status 一律说"主管审核通过"。
  11. /// </summary>
  12. public class S6IpqcDisplayContractTests
  13. {
  14. private static string SyncService() => File.ReadAllText(FindFile(
  15. "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "IpqcInspectionMdpSyncService.cs"));
  16. private static string FlowService() => File.ReadAllText(FindFile(
  17. "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "IpqcInspectionFlowService.cs"));
  18. private static string JsonSql() => File.ReadAllText(FindFile(
  19. "server", "Plugins", "Admin.NET.Plugin.AiDOP", "DataPlatform", "MdpJsonSql.cs"));
  20. /// <summary>head transform 的每个字符串列都必须走 MdpJsonSql.Str。</summary>
  21. [Theory]
  22. [InlineData("djbh")]
  23. [InlineData("cplx")]
  24. [InlineData("scph")]
  25. [InlineData("lydjbh")]
  26. [InlineData("jgpd")]
  27. [InlineData("fj")]
  28. [InlineData("bz")]
  29. [InlineData("jyr")]
  30. [InlineData("gxbm")]
  31. [InlineData("gxmc")]
  32. [InlineData("sczyry")]
  33. [InlineData("bdbh")]
  34. [InlineData("bbh")]
  35. [InlineData("wlbm")]
  36. [InlineData("wlmc")]
  37. [InlineData("jgbb")]
  38. [InlineData("jgbh")]
  39. [InlineData("status")]
  40. public void HeadTransform_StringField_UsesNullSafeHelper(string field)
  41. {
  42. Assert.Contains($"MdpJsonSql.Str(\"m\", \"{field}\")", SyncService());
  43. }
  44. /// <summary>数值/日期列用对应类型 helper,不得被误改成字符串 helper。</summary>
  45. [Fact]
  46. public void HeadTransform_NumericAndDateFields_UseTypedHelpers()
  47. {
  48. var s = SyncService();
  49. Assert.Contains("MdpJsonSql.Dec(\"m\", \"ybl\", 18, 6)", s);
  50. Assert.Contains("MdpJsonSql.DateTimeSec(\"m\", \"sxrq\")", s);
  51. Assert.DoesNotContain("MdpJsonSql.Str(\"m\", \"ybl\")", s);
  52. Assert.DoesNotContain("MdpJsonSql.Str(\"m\", \"sxrq\")", s);
  53. }
  54. /// <summary>
  55. /// 除身份键 source_row_id / source_biz_key 的两处兜底外,转换 SQL 不得再出现裸 JSON_UNQUOTE。
  56. /// 这两处刻意保留:它们是 NOT NULL 的 upsert 身份列,不是展示列,改动会影响唯一键语义。
  57. /// </summary>
  58. [Fact]
  59. public void TransformSql_NoBareJsonUnquote_ExceptIdentityKeyFallbacks()
  60. {
  61. var s = SyncService();
  62. var bare = s.Split("JSON_UNQUOTE(JSON_EXTRACT(").Length - 1;
  63. Assert.Equal(2, bare);
  64. Assert.Contains("IFNULL(m.source_row_id, JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.id')))", s);
  65. Assert.Contains("IFNULL(NULLIF(m.source_biz_key,''), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.djbh')))", s);
  66. }
  67. /// <summary>detail transform 既有的 null 安全行为不得回归。</summary>
  68. [Fact]
  69. public void DetailTransform_StillUsesNullSafeHelpers()
  70. {
  71. var s = SyncService();
  72. Assert.Contains("MdpJsonSql.Str(\"d\", \"jyxm\")", s);
  73. Assert.Contains("MdpJsonSql.Str(\"d\", \"pd\")", s);
  74. Assert.Contains("MdpJsonSql.Dec(\"d\", \"jysl\", 18, 6)", s);
  75. Assert.Contains("MdpJsonSql.DateTimeSec(\"d\", \"InspectionTime\")", s);
  76. }
  77. /// <summary>helper 语义钉死:JSON null / 字符串 'null' → SQL NULL。</summary>
  78. [Fact]
  79. public void JsonSqlStrHelper_NormalizesLiteralNullToSqlNull()
  80. {
  81. Assert.Contains("NULLIF({Ext(alias, field)},'null')", JsonSql());
  82. }
  83. /// <summary>完成态文案必须由 businessStatus 决定,而非只看实例状态。</summary>
  84. [Fact]
  85. public void CompletedMessage_BranchesOnBusinessStatus()
  86. {
  87. var s = FlowService();
  88. Assert.Contains("BuildCompletedMessage(output.BusinessStatus)", s);
  89. Assert.Contains("IpqcInspectionFlowConst.StatusApproved => \"流程已完成(整批合格·主管审核通过)\"", s);
  90. Assert.Contains("IpqcInspectionFlowConst.StatusDisposed => \"流程已完成(整批不合格·已处置)\"", s);
  91. }
  92. /// <summary>两个完成终态的文案不得相同——这正是 D-01 的缺陷本身。</summary>
  93. [Fact]
  94. public void CompletedMessage_ApprovedAndDisposed_AreNotIdentical()
  95. {
  96. var s = FlowService();
  97. Assert.DoesNotContain(
  98. "inst.Status == FlowInstanceStatusEnum.Approved ? \"流程已完成(主管审核通过)\"", s);
  99. Assert.Contains("_ => \"流程已完成\",", s); // 业务状态缺失时中性表述,不臆断"审核通过"
  100. }
  101. /// <summary>非 Approved 的结束态文案保持原样,未被本批波及。</summary>
  102. [Fact]
  103. public void NonApprovedTerminal_MessageUnchanged()
  104. {
  105. Assert.Contains("$\"流程已结束({inst.Status})\"", FlowService());
  106. }
  107. /// <summary>
  108. /// 状态机未被改动:节点码、网关判据、审批人解析、can* 判据、任务归属均保持原样。
  109. /// D-01 只允许改"给 UI 的解释性文案"。
  110. /// </summary>
  111. [Fact]
  112. public void StateMachine_Untouched()
  113. {
  114. var s = FlowService();
  115. Assert.Contains("myTask.NodeId == IpqcInspectionFlowConst.NodeInspect", s);
  116. Assert.Contains("myTask.NodeId == IpqcInspectionFlowConst.NodeSupervisor", s);
  117. Assert.Contains("myTask.NodeId == IpqcInspectionFlowConst.NodeDisposition", s);
  118. Assert.Contains("output.CanSupervisorApprove = true;", s);
  119. Assert.Contains("output.CanSupervisorReturn = true;", s);
  120. Assert.Contains("output.CanSubmitDisposition = true;", s);
  121. // 整批判定仍驱动网关,未被文案改动波及
  122. Assert.Contains("var dispositionRequired = input.Qualified ? 0 : 1;", s);
  123. }
  124. private static string FindFile(params string[] parts)
  125. {
  126. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  127. while (dir != null)
  128. {
  129. var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
  130. if (File.Exists(candidate)) return candidate;
  131. dir = dir.Parent;
  132. }
  133. throw new FileNotFoundException(string.Join("/", parts));
  134. }
  135. }