package web import ( "context" "math" "testing" ) // D2 回归:留档序列化失败必须把错误上抛,让整个合并事务回滚。 // 以前是只 log 后 return nil —— 调用方以为写成功了,合并照常提交, // 这一行的原始列就永久缺失(下次跨备份比对认不出 UUID,导出也还原不回去)。 func TestWriteRawRowFailsLoudlyOnMarshalError(t *testing.T) { f := newLoginFixture(t) ctx := context.Background() var ds int64 if err := f.pg.QueryRowContext(ctx, `SELECT id FROM datasets ORDER BY id LIMIT 1`).Scan(&ds); err != nil { t.Skipf("库里没有数据集:%v", err) } const pk = 987654321 t.Cleanup(func() { ctx := context.Background() for _, e := range []string{"ZVISIT", "ZLOCATION"} { f.pg.ExecContext(ctx, `DELETE FROM entity_raw WHERE dataset_id=$1 AND entity=$2 AND src_pk=$3`, ds, e, pk) } }) // NaN 是 json.Marshal 明确拒绝的值,拿来触发真实的序列化失败 err := writeRawRow(ctx, f.pg, ds, "ZVISIT", pk, map[string]any{"ZBAD": math.NaN()}) if err == nil { t.Fatal("序列化失败必须返回错误;返回 nil 等于让留档静默丢失") } var n int if err := f.pg.QueryRowContext(ctx, `SELECT count(*) FROM entity_raw WHERE dataset_id=$1 AND entity='ZVISIT' AND src_pk=$2`, ds, pk).Scan(&n); err != nil { t.Fatal(err) } if n != 0 { t.Errorf("失败时不应留下 entity_raw 行,实际 %d 行", n) } // 正常值必须写得进去(否则上面的断言可能只是「这张表根本写不动」) if err := writeRawRow(ctx, f.pg, ds, "ZLOCATION", pk, map[string]any{"ZNAME": "回归测试"}); err != nil { t.Fatalf("正常留档不应失败: %v", err) } var got string if err := f.pg.QueryRowContext(ctx, `SELECT raw->>'ZNAME' FROM entity_raw WHERE dataset_id=$1 AND entity='ZLOCATION' AND src_pk=$2`, ds, pk).Scan(&got); err != nil { t.Fatalf("读取刚写入的留档失败: %v", err) } if got != "回归测试" { t.Errorf("留档内容对不上:%q", got) } }