Просмотр исходного кода

test(进件): Task 3 修复轮 1 - 标签清单改成切开比集合 + 补两键优先级的现状钉子

评审判 A=符合 / B=通过,两条 Low 都成立,同一个根:断言没咬住它声称咬住的东西。
本轮只改测试文件,生产代码一行未动。

F1:26 个必填标签里「商户签约负责人」是「商户签约负责人电话 / 邮箱」的前缀,
逐条 contains 之下它恒真。实测(不是推断):把 production 里 contact_name 那条标签改成
「签约负责人」——一个会到租户眼前的改动——旧断言下本类 57 条全绿。改成把文案「:」之后的
清单按「、」切回来、断言数量与集合都相等(⇒ 多重集相等):少一个 / 多一个 / 串一个 /
一条是另一条的前缀,四种都会红。同一根的第三处(类别为空那两个派生标签)一并切成同一判据。
连带的假话一并订正:数组 javadoc 原写「断言用的是 contains,所以顺序不影响结果」——
换成集合相等后那句不成立,改写成「排列不参与断言」,并把切分判据的两个前提写进去。

F2:取号的优先级(先读 merchant_no、空白才回落 shoubei_merchant_no)此前没有任何场景
咬得住——三个子场景里没有「两键都非空且不同值」这一态,把服务端那两行对调全绿。
补第四个场景钉住现状;javadoc 写明这是**未决语义下的现状钉子**(spec §6.2(c):两键本该是
同义别名、正常只有一个有值;真冲突时哪个算数没有依据,联调之前不猜),并写明语义裁定后
要改的两处(服务端那两行的先后 + 本用例的期望值)。

变异实测(两向;读数与还原指纹见 task-3-fix1-report.md):
  M-A1 改 production 的 contact_name 标签文案  旧断言 57 绿 → 新断言 1 红(失败信息精确点出那一条)
  M-A2 删掉期望数组里那一项                    1 红(期望 25 / 实际 26)
  M-B  期望数组前两项换序(无害)              仍绿,且 javap 证明变异体进了被执行的 class
  M-C  服务端取号两行对调                      1 红,且恰好是新的第 4 个子场景(其余 56 条不动)

测试:全量 538 → 538(条数不变 —— 本轮只加强断言与补场景数据,不新增用例),
0 fail / 0 error / 0 skipped,BUILD SUCCESS。
alphaH 3 недель назад
Родитель
Сommit
623fb68520

+ 65 - 9
java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingServiceTest.java

@@ -22,6 +22,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 
 
 import java.nio.charset.StandardCharsets;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.List;
@@ -1555,10 +1556,23 @@ class LsyOnboardingServiceTest {
         assertEquals(400, ex.getCode());
         assertEquals(400, ex.getCode());
         assertTrue(ex.getMessage().contains("共 26 项"),
         assertTrue(ex.getMessage().contains("共 26 项"),
                 "小微商户的必填集是 18 + 8 = 26,实际: " + ex.getMessage());
                 "小微商户的必填集是 18 + 8 = 26,实际: " + ex.getMessage());
+        // ★ 把「:」之后的标签清单按「、」**切回来**再比(数量与集合都相等 ⇒ 多重集相等),
+        //   不是逐条 contains:后者挡不住「有一条标签正好是另一条的前缀」——「商户签约负责人」
+        //   会被「商户签约负责人电话 / 邮箱」这两条自己带出来。修复轮 1 的 F1 实测:把 production
+        //   里 contact_name 那条标签改成「签约负责人」,旧断言下本类 57 条**全绿**。
         for (String label : MERCHANT_MICRO_MISSING_LABELS) {
         for (String label : MERCHANT_MICRO_MISSING_LABELS) {
-            assertTrue(ex.getMessage().contains(label),
-                    "文案缺中文标签「" + label + "」,实际: " + ex.getMessage());
+            assertFalse(label.contains("、"),
+                    "夹具里的标签本身含顿号 ⇒ 按顿号切分清单这个判据不成立,先换切分依据:" + label);
         }
         }
+        List<String> actualLabels = labelsIn(ex.getMessage());
+        Set<String> expectedLabels = new LinkedHashSet<>(Arrays.asList(MERCHANT_MICRO_MISSING_LABELS));
+        assertEquals(MERCHANT_MICRO_MISSING_LABELS.length, expectedLabels.size(),
+                "夹具数组自身有重复项 ⇒ 下面的比较不再是「逐字相同」,先修夹具");
+        assertEquals(MERCHANT_MICRO_MISSING_LABELS.length, actualLabels.size(),
+                "文案里的标签个数必须恰好是 " + MERCHANT_MICRO_MISSING_LABELS.length
+                        + "(少一个 / 多一个都红),实际: " + actualLabels);
+        assertEquals(expectedLabels, new LinkedHashSet<>(actualLabels),
+                "标签清单必须与期望的 26 个逐字相同(顺序不参与断言),实际: " + actualLabels);
         for (String key : new String[]{"merchant_name", "merchant_mcc_code", "account_idcard_num",
         for (String key : new String[]{"merchant_name", "merchant_mcc_code", "account_idcard_num",
                 "fSettle_account_type", "picture_idcard_hander", "mcht_Landuse"}) {
                 "fSettle_account_type", "picture_idcard_hander", "mcht_Landuse"}) {
             assertFalse(ex.getMessage().contains(key),
             assertFalse(ex.getMessage().contains(key),
@@ -1571,8 +1585,12 @@ class LsyOnboardingServiceTest {
         assertEquals(400, noCategory.getCode());
         assertEquals(400, noCategory.getCode());
         assertTrue(noCategory.getMessage().contains("共 2 项"),
         assertTrue(noCategory.getMessage().contains("共 2 项"),
                 "类别为空时只有两个派生字段填不出来,实际: " + noCategory.getMessage());
                 "类别为空时只有两个派生字段填不出来,实际: " + noCategory.getMessage());
-        assertTrue(noCategory.getMessage().contains("商户类型"), noCategory.getMessage());
-        assertTrue(noCategory.getMessage().contains("账户类型"), noCategory.getMessage());
+        // 同一根的第二处:这里原本也是 contains ⇒ 同样切成「按顿号切开比集合」。
+        List<String> derivedLabels = labelsIn(noCategory.getMessage());
+        assertEquals(2, derivedLabels.size(),
+                "类别为空时文案里只该有这两个标签,实际: " + derivedLabels);
+        assertEquals(Set.of("商户类型", "账户类型"), new LinkedHashSet<>(derivedLabels),
+                "类别为空时文案里只该有这两个标签(顺序不参与断言),实际: " + derivedLabels);
         assertFalse(noCategory.getMessage().contains("商户名称"),
         assertFalse(noCategory.getMessage().contains("商户名称"),
                 "18 个无条件字段全填了,不该报它们中的任何一个:" + noCategory.getMessage());
                 "18 个无条件字段全填了,不该报它们中的任何一个:" + noCategory.getMessage());
 
 
@@ -1583,8 +1601,17 @@ class LsyOnboardingServiceTest {
      * 小微商户那一类的 26 个必填字段的**中文标签**,顺序照 {@code LsyMerchantRequiredFields}
      * 小微商户那一类的 26 个必填字段的**中文标签**,顺序照 {@code LsyMerchantRequiredFields}
      * 的声明顺序(18 个无条件在前、小微独有的 8 个在后)。
      * 的声明顺序(18 个无条件在前、小微独有的 8 个在后)。
      *
      *
-     * <p>断言用的是 {@code contains},所以顺序不影响结果 —— 排成这个顺序只为读起来
-     * 能跟矩阵那张表逐行对照。
+     * <p>★ 断言是把文案里「:」之后的清单**按「、」切回来**再比(数量与集合都相等 ⇒ 多重集相等),
+     * 所以这个数组的**排列不参与断言** —— 排成矩阵声明顺序只为读起来能跟那张表逐行对照。
+     *
+     * <p>★ 修复轮 1(F1)之前这里用的是逐条 {@code contains},那是**恒真**的:
+     * 数组里「商户签约负责人」是「商户签约负责人电话 / 邮箱」的前缀,改掉前者那条标签文案,
+     * 后两条自己就把前缀带出来了。切分之后「少一个 / 多一个 / 串一个 / 一条是另一条的前缀」
+     * 才会红 —— 这正是一条是另一条前缀时 {@code contains} 唯一抓不到的情形。
+     *
+     * <p>★ 切分这个判据的两个前提(改本数组或改文案构造时要一起看):① 标签里不含顿号「、」
+     * (用例里有断言,含了就红);② 文案的构造是「进件必填项缺失(共 N 项):」+
+     * {@code String.join("、", labels)}({@code LsyOnboardingService} 必填缺失那一支)。
      */
      */
     private static final String[] MERCHANT_MICRO_MISSING_LABELS = {
     private static final String[] MERCHANT_MICRO_MISSING_LABELS = {
             "商户名称",
             "商户名称",
@@ -1671,13 +1698,22 @@ class LsyOnboardingServiceTest {
      * {@code merchant_no}(spec §6.2(c):只有真跑一次才知道哪个是准的)⇒ 两个都读,
      * {@code merchant_no}(spec §6.2(c):只有真跑一次才知道哪个是准的)⇒ 两个都读,
      * 谁给值就用谁。
      * 谁给值就用谁。
      *
      *
-     * <p>三个子场景:
+     * <p>四个子场景:
      * <ol>
      * <ol>
      *   <li>只有示例那个键({@code merchant_no});</li>
      *   <li>只有示例那个键({@code merchant_no});</li>
      *   <li>只有参数表那个键({@code shoubei_merchant_no});</li>
      *   <li>只有参数表那个键({@code shoubei_merchant_no});</li>
-     *   <li>示例那个键给了**空串**(与没给一样不可用 —— 判据是空白,不是「键在不在」)⇒ 仍要取到参数表那个键。</li>
+     *   <li>示例那个键给了**空串**(与没给一样不可用 —— 判据是空白,不是「键在不在」)⇒ 仍要取到参数表那个键;</li>
+     *   <li>★ **两个键都在、都是非空、且值不同** ⇒ 取到的是 {@code merchant_no} 那个。</li>
      * </ol>
      * </ol>
      *
      *
+     * <p>★★ 第 4 个子场景是**未决语义下的现状钉子**,不是设计决策:spec §6.2(c) 的立场是两个键
+     * **本该是同义别名**(返回参数表写一个、返回示例写另一个),正常只会有一个有值;
+     * 真出现「两个都有值且不同」时哪个算数**当前没有依据** —— 联调跑一次之前不猜。
+     * 所以这里把**现状**(服务端 {@code createMerchant} 先读 {@code merchant_no}、空白才回落
+     * {@code shoubei_merchant_no})钉住:将来任何人改动那个优先级,这条断言都会红。
+     * <b>语义裁定之后要改两处</b>:① 服务端取号那两行的先后;② 本用例第 4 个子场景的期望值
+     * 与这段 javadoc(钉子本身就是为了让后来人一眼看到该去哪改)。
+     *
      * <p>★ 这里读的两个键名是**上游的**,属于内部代码,不触 GC 1(GC 1 只管网页面 / 错误消息 /
      * <p>★ 这里读的两个键名是**上游的**,属于内部代码,不触 GC 1(GC 1 只管网页面 / 错误消息 /
      * 应答字段名)—— 不要为了「避讳」把它们从解析代码里去掉,那会直接把取号打断。
      * 应答字段名)—— 不要为了「避讳」把它们从解析代码里去掉,那会直接把取号打断。
      */
      */
@@ -1688,7 +1724,10 @@ class LsyOnboardingServiceTest {
                 {ok(",\"merchant_no\":\"851901070001\""), "851901070001", "返回示例的键"},
                 {ok(",\"merchant_no\":\"851901070001\""), "851901070001", "返回示例的键"},
                 {ok(",\"shoubei_merchant_no\":\"851901070002\""), "851901070002", "返回参数表的键"},
                 {ok(",\"shoubei_merchant_no\":\"851901070002\""), "851901070002", "返回参数表的键"},
                 {ok(",\"merchant_no\":\"\",\"shoubei_merchant_no\":\"851901070003\""), "851901070003",
                 {ok(",\"merchant_no\":\"\",\"shoubei_merchant_no\":\"851901070003\""), "851901070003",
-                        "示例那个键给了空串 ⇒ 与没给一样,仍要取到参数表那个键"}};
+                        "示例那个键给了空串 ⇒ 与没给一样,仍要取到参数表那个键"},
+                {ok(",\"merchant_no\":\"851901070004\",\"shoubei_merchant_no\":\"851901070005\""),
+                        "851901070004",
+                        "★ 两个键都在且值不同 ⇒ 取示例那个键(现状钉子,语义待联调裁定,见 javadoc)"}};
         for (String[] c : cases) {
         for (String[] c : cases) {
             stubUpstream(c[0]);
             stubUpstream(c[0]);
             assertEquals(c[1], service.createMerchant(merchantDto()), c[2]);
             assertEquals(c[1], service.createMerchant(merchantDto()), c[2]);
@@ -1863,4 +1902,21 @@ class LsyOnboardingServiceTest {
     private static List<String> messages(List<ILoggingEvent> events) {
     private static List<String> messages(List<ILoggingEvent> events) {
         return events.stream().map(ILoggingEvent::getFormattedMessage).toList();
         return events.stream().map(ILoggingEvent::getFormattedMessage).toList();
     }
     }
+
+    /**
+     * 把必填缺失文案里「:」之后的标签清单**切回来**({@code "……(共 N 项):甲、乙、丙"} ⇒ {@code [甲,乙,丙]})。
+     *
+     * <p>为什么不是逐条 {@code contains}:{@code contains} 挡不住前缀关系 —— 期望清单里
+     * 「商户签约负责人」恰好是「商户签约负责人电话」的前缀,于是改掉前者文案(甚至删掉它那一项)
+     * 断言照样绿(修复轮 1 的 F1,实测过:改 production 那条标签,本类 57 条全绿)。
+     * 切开来比集合,每个标签才各自独立地被钉住。
+     *
+     * <p>用**第一个**全角冒号切:前缀「进件必填项缺失(共 N 项):」里只有这一个冒号,
+     * 标签里若出现冒号也只会在它之后。
+     */
+    private static List<String> labelsIn(String message) {
+        int colon = message.indexOf(':');
+        assertTrue(colon >= 0, "必填缺失的文案里没有全角冒号「:」,切不出标签清单:" + message);
+        return List.of(message.substring(colon + 1).split("、", -1));
+    }
 }
 }