using Xunit;
namespace Admin.NET.Plugin.AiDOP.Tests.S6.Manufacturing;
///
/// S6 DISPLAY FIX · Batch 1 源码契约测试。
///
/// D-04:IPQC head transform 的字符串列必须经 MdpJsonSql.Str 把 JSON null 归一为 SQL NULL,
/// 否则 JSON_UNQUOTE(JSON_EXTRACT(...)) 会产出字面量字符串 'null' 并一路透到页面。
/// detail transform 早已正确,本测试同时钉住其不回归。
/// D-01:完成态 message 必须区分 APPROVED(整批合格)与 DISPOSED(整批不合格已处置),
/// 不得只依据 ApprovalFlowInstance.Status 一律说"主管审核通过"。
///
public class S6IpqcDisplayContractTests
{
private static string SyncService() => File.ReadAllText(FindFile(
"server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "IpqcInspectionMdpSyncService.cs"));
private static string FlowService() => File.ReadAllText(FindFile(
"server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "IpqcInspectionFlowService.cs"));
private static string JsonSql() => File.ReadAllText(FindFile(
"server", "Plugins", "Admin.NET.Plugin.AiDOP", "DataPlatform", "MdpJsonSql.cs"));
/// head transform 的每个字符串列都必须走 MdpJsonSql.Str。
[Theory]
[InlineData("djbh")]
[InlineData("cplx")]
[InlineData("scph")]
[InlineData("lydjbh")]
[InlineData("jgpd")]
[InlineData("fj")]
[InlineData("bz")]
[InlineData("jyr")]
[InlineData("gxbm")]
[InlineData("gxmc")]
[InlineData("sczyry")]
[InlineData("bdbh")]
[InlineData("bbh")]
[InlineData("wlbm")]
[InlineData("wlmc")]
[InlineData("jgbb")]
[InlineData("jgbh")]
[InlineData("status")]
public void HeadTransform_StringField_UsesNullSafeHelper(string field)
{
Assert.Contains($"MdpJsonSql.Str(\"m\", \"{field}\")", SyncService());
}
/// 数值/日期列用对应类型 helper,不得被误改成字符串 helper。
[Fact]
public void HeadTransform_NumericAndDateFields_UseTypedHelpers()
{
var s = SyncService();
Assert.Contains("MdpJsonSql.Dec(\"m\", \"ybl\", 18, 6)", s);
Assert.Contains("MdpJsonSql.DateTimeSec(\"m\", \"sxrq\")", s);
Assert.DoesNotContain("MdpJsonSql.Str(\"m\", \"ybl\")", s);
Assert.DoesNotContain("MdpJsonSql.Str(\"m\", \"sxrq\")", s);
}
///
/// 除身份键 source_row_id / source_biz_key 的两处兜底外,转换 SQL 不得再出现裸 JSON_UNQUOTE。
/// 这两处刻意保留:它们是 NOT NULL 的 upsert 身份列,不是展示列,改动会影响唯一键语义。
///
[Fact]
public void TransformSql_NoBareJsonUnquote_ExceptIdentityKeyFallbacks()
{
var s = SyncService();
var bare = s.Split("JSON_UNQUOTE(JSON_EXTRACT(").Length - 1;
Assert.Equal(2, bare);
Assert.Contains("IFNULL(m.source_row_id, JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.id')))", s);
Assert.Contains("IFNULL(NULLIF(m.source_biz_key,''), JSON_UNQUOTE(JSON_EXTRACT(m.raw_data,'$.djbh')))", s);
}
/// detail transform 既有的 null 安全行为不得回归。
[Fact]
public void DetailTransform_StillUsesNullSafeHelpers()
{
var s = SyncService();
Assert.Contains("MdpJsonSql.Str(\"d\", \"jyxm\")", s);
Assert.Contains("MdpJsonSql.Str(\"d\", \"pd\")", s);
Assert.Contains("MdpJsonSql.Dec(\"d\", \"jysl\", 18, 6)", s);
Assert.Contains("MdpJsonSql.DateTimeSec(\"d\", \"InspectionTime\")", s);
}
/// helper 语义钉死:JSON null / 字符串 'null' → SQL NULL。
[Fact]
public void JsonSqlStrHelper_NormalizesLiteralNullToSqlNull()
{
Assert.Contains("NULLIF({Ext(alias, field)},'null')", JsonSql());
}
/// 完成态文案必须由 businessStatus 决定,而非只看实例状态。
[Fact]
public void CompletedMessage_BranchesOnBusinessStatus()
{
var s = FlowService();
Assert.Contains("BuildCompletedMessage(output.BusinessStatus)", s);
Assert.Contains("IpqcInspectionFlowConst.StatusApproved => \"流程已完成(整批合格·主管审核通过)\"", s);
Assert.Contains("IpqcInspectionFlowConst.StatusDisposed => \"流程已完成(整批不合格·已处置)\"", s);
}
/// 两个完成终态的文案不得相同——这正是 D-01 的缺陷本身。
[Fact]
public void CompletedMessage_ApprovedAndDisposed_AreNotIdentical()
{
var s = FlowService();
Assert.DoesNotContain(
"inst.Status == FlowInstanceStatusEnum.Approved ? \"流程已完成(主管审核通过)\"", s);
Assert.Contains("_ => \"流程已完成\",", s); // 业务状态缺失时中性表述,不臆断"审核通过"
}
/// 非 Approved 的结束态文案保持原样,未被本批波及。
[Fact]
public void NonApprovedTerminal_MessageUnchanged()
{
Assert.Contains("$\"流程已结束({inst.Status})\"", FlowService());
}
///
/// 状态机未被改动:节点码、网关判据、审批人解析、can* 判据、任务归属均保持原样。
/// D-01 只允许改"给 UI 的解释性文案"。
///
[Fact]
public void StateMachine_Untouched()
{
var s = FlowService();
Assert.Contains("myTask.NodeId == IpqcInspectionFlowConst.NodeInspect", s);
Assert.Contains("myTask.NodeId == IpqcInspectionFlowConst.NodeSupervisor", s);
Assert.Contains("myTask.NodeId == IpqcInspectionFlowConst.NodeDisposition", s);
Assert.Contains("output.CanSupervisorApprove = true;", s);
Assert.Contains("output.CanSupervisorReturn = true;", s);
Assert.Contains("output.CanSubmitDisposition = true;", s);
// 整批判定仍驱动网关,未被文案改动波及
Assert.Contains("var dispositionRequired = input.Qualified ? 0 : 1;", s);
}
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));
}
}