|
|
@@ -904,6 +904,85 @@ func TestUpdateQuotaCascade(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// TestUpdateQuotaClampMustNotCrossTree 调额级联**只压本树**:不得碰其它树里区间数值重叠的节点。
|
|
|
+//
|
|
|
+// # 这个用例钉的是台账 F25(P0)
|
|
|
+//
|
|
|
+// `ClampSubtreeQuota` 的 WHERE 原本只有 `left_val > ? AND right_val < ?` + `quota > ?` + `type`,
|
|
|
+// **没有 `root_user_id`**。而每棵树的 `left_val` 都从 1 起 ⇒ 多棵树根的区间在**数值上必然互相重叠**
|
|
|
+// (旧库实测:86 行 / 26 棵树 / 跨树重叠 325 对 = C(26,2) 满重叠)。
|
|
|
+// ⇒ 调 A 树的额度,会把 B 树里"区间数值落在 A 子树范围内、且额度更高"的节点一起压低,
|
|
|
+// 后续分成静默少算。根因是**照抄 dump**:legacy `sp_user_divide_into_update_quota` 本身就缺这个过滤。
|
|
|
+//
|
|
|
+// # 为什么必须用"两棵结构相同的树"来测
|
|
|
+//
|
|
|
+// 因为缺陷的触发条件就是"区间数值重叠"。本用例造 A(1,4)→B(2,3) 与 D(1,4)→E(2,3):
|
|
|
+// **E 的 (2,3) 与 B 的 (2,3) 数值完全相同**。若缺 root 过滤,`left>1 AND right<4` 会同时命中 B 和 E。
|
|
|
+// 下面有前置自检确保这个重叠真的成立 —— 否则用例会变成"测不到东西的绿"。
|
|
|
+//
|
|
|
+// # 与双跑的关系
|
|
|
+//
|
|
|
+// 这条缺陷**双跑抓不到**(legacy 与 target 同错 ⇒ equality ⇒ PASS,见 BACKLOG B1),
|
|
|
+// 所以它是**唯一能抓它的地方**。双跑的 DV-7 会以 INTENTIONAL-STRICT 形态登记两侧不同。
|
|
|
+func TestUpdateQuotaClampMustNotCrossTree(t *testing.T) {
|
|
|
+ db := newDivideServiceDB(t)
|
|
|
+ svc := NewDivideService(db, nil)
|
|
|
+ ctx := context.Background()
|
|
|
+ a, b := nodeID(0), nodeID(1)
|
|
|
+ d, e := nodeID(2), nodeID(3)
|
|
|
+ seedRoot(t, db, a, model.DivideTypeOneOff, "20.00")
|
|
|
+ seedRoot(t, db, d, model.DivideTypeOneOff, "20.00")
|
|
|
+ if err := svc.AddSubordinate(ctx, a, b); err != nil {
|
|
|
+ t.Fatalf("建链 A→B 失败: %v", err)
|
|
|
+ }
|
|
|
+ if err := svc.AddSubordinate(ctx, d, e); err != nil {
|
|
|
+ t.Fatalf("建链 D→E 失败: %v", err)
|
|
|
+ }
|
|
|
+ // 两个子节点都配 15.00(高于下面要压到的 10.00)
|
|
|
+ if err := svc.UpdateQuota(ctx, b, money.FromCents(1500)); err != nil {
|
|
|
+ t.Fatalf("设额度 B 失败: %v", err)
|
|
|
+ }
|
|
|
+ if err := svc.UpdateQuota(ctx, e, money.FromCents(1500)); err != nil {
|
|
|
+ t.Fatalf("设额度 E 失败: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---- 把两棵树的区间摆成**真实数据那种形态**:每棵树的 left 都从 1 起 ----
|
|
|
+ // 为什么必须手动摆:`AddSubordinate` 的右移会在同表内让后续树拿到递增的 left/right
|
|
|
+ // (实测第二棵树会变成 D(1,6)/E(4,5),与 B(2,3) 不重叠 ⇒ 缺陷根本触发不到)。
|
|
|
+ // 而旧库真实数据正是"26 棵树、每棵 left 都从 1 起、跨树重叠 325 对"。这里直接落库对齐真实形态。
|
|
|
+ if err := db.Exec("UPDATE t_user_divide_into SET left_val = 1, right_val = 4 WHERE user_id = ?", d).Error; err != nil {
|
|
|
+ t.Fatalf("摆 D 区间失败: %v", err)
|
|
|
+ }
|
|
|
+ if err := db.Exec("UPDATE t_user_divide_into SET left_val = 2, right_val = 3 WHERE user_id = ?", e).Error; err != nil {
|
|
|
+ t.Fatalf("摆 E 区间失败: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---- 前置自检:两棵树的子节点区间必须**数值重叠** ----
|
|
|
+ // 不成立 ⇒ 本用例根本触发不到 F25,跑出来的绿是假的。
|
|
|
+ bn, en := loadDivideNode(t, db, b), loadDivideNode(t, db, e)
|
|
|
+ if bn.LeftVal != en.LeftVal || bn.RightVal != en.RightVal {
|
|
|
+ t.Fatalf("前置不成立:B(%d,%d) 与 E(%d,%d) 区间不重叠,本用例测不到 F25",
|
|
|
+ bn.LeftVal, bn.RightVal, en.LeftVal, en.RightVal)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---- 调 A 的额度:只应压本树的 B ----
|
|
|
+ if err := svc.UpdateQuota(ctx, a, money.FromCents(1000)); err != nil {
|
|
|
+ t.Fatalf("UpdateQuota(A,10) 失败: %v", err)
|
|
|
+ }
|
|
|
+ if got := cents(t, loadDivideNode(t, db, b).Quota); got != 1000 {
|
|
|
+ t.Fatalf("本树 B 应被压到 1000 分(级联正常路径),实得 %d", got)
|
|
|
+ }
|
|
|
+ if got := cents(t, loadDivideNode(t, db, e).Quota); got != 1500 {
|
|
|
+ t.Fatalf("**F25 回归**:另一棵树 D→E 的节点 E 被误压,quota 应保持 1500 分,实得 %d 分"+
|
|
|
+ "(说明 ClampSubtreeQuota 丢了 root_user_id 过滤;E 与 B 区间数值都是 (%d,%d))",
|
|
|
+ got, bn.LeftVal, bn.RightVal)
|
|
|
+ }
|
|
|
+ // 另一棵树的根也不该被碰(它自己额度更高、且区间包含 B)
|
|
|
+ if got := cents(t, loadDivideNode(t, db, d).Quota); got != 2000 {
|
|
|
+ t.Fatalf("另一棵树的根 D 被误改,quota 应保持 2000 分,实得 %d", got)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// TestUpdateQuotaNoCascadeWhenOrigZero 原 quota=0 时不级联(audit A6,dump 763872 `IF (uQuota>0)`)。
|
|
|
//
|
|
|
// 语义:还没配置过额度的节点,被"设成某个值"时不该顺手压低子树(子树额度是独立配置的阶梯)。
|