Refactor không có triệu chứng — Claude sai bạn không thấy. Ba hàng rào: test, commit nguyên tử, click tay.
Refactor là task Claude nguy hiểm nhất. Bug có triệu chứng — nút không phản ứng, giá trị undefined, stack trace — nên bạn biết được fix của Claude có đúng không. Refactor thì không. "Vẫn chạy" có thể nghĩa là "test vẫn pass" trong khi hành vi đã âm thầm thay đổi, bạn không nhận ra, và một tuần sau production nổ.
Gần đây tôi làm một refactor khá lớn với Claude ở how2claude: chuyển thanh toán crypto x402 từ PaymentHandler + FacilitatorClient tự viết (139 dòng) sang gem x402-rails, đồng thời tách phần map field của Purchase.create! / Subscription.create! — bị lặp ở hai controller — thành class method của model. Một commit, 4 file thay đổi, 2 xoá, 2 thêm.
Prompt của tôi là một từ: "refactor."
Có thể ngắn như vậy vì xung quanh có hàng rào bảo vệ.
Đến lúc branch này tới đó, có 221 test. Tất cả đường đi quan trọng của payment flow đều được phủ.
Hành động mặc định của Claude trước refactor không phải "xem test trước." Nên tôi bảo nó chạy bin/rails test trước, xác nhận xanh, rồi mới chạm vào gì.
Sau refactor, chạy lại. Vẫn xanh. Cái đó không nghĩa là không có regression — nghĩa là hành vi đã biết không bị hỏng.
Nếu codepath của bạn không có test phủ, bảo Claude viết một test tối thiểu khoá hành vi hiện tại. Chạy nó, commit. Rồi mới refactor. Nếu không, cái nó đang làm không phải refactor, mà là rewrite — và bạn không có cách nào xác minh tính tương đương.
Refactor này thực ra là hai việc:
Purchase / Subscription: controller → class method của modelPhía frontend có việc thứ ba: viết lại luồng ký bên JS bằng viem + x402-fetch.
Tôi bảo Claude tách theo biên tự nhiên: backend + model extraction một commit (9f3e239), frontend commit riêng (93746d8). Mỗi commit có mô tả đầy đủ, danh sách file thay đổi, và lý do đổi.
Lợi ích:
- Diff dễ đọc. Một commit, một việc.
- Độ hạt rollback có kiểm soát. Nếu production gặp bug frontend, git revert 93746d8 chỉ revert frontend, backend giữ nguyên.
- Sự chú ý của Claude cũng tập trung. Một commit một việc — sự chú ý của nó cũng chỉ phủ việc đó.
Refactor xong, tôi bảo Claude dừng lại và đưa git diff --staged cho tôi xem. Không chạy test, không chạy app, đọc diff trước đã.
Các tín hiệu tôi quét:
app/services/x402/payment_handler.rb bị xoá sạch — OK, đó chính là mục đích của việc chuyển sang gem. Nhưng nếu nó xoá thứ tôi không bảo động tới, tôi dừng lại và hỏi.Purchase.create!(wallet_address: verify_result["payer"], ...) → Purchase.record_x402!(payment:, settlement:) giờ đọc payment[:payer]. Nguồn đã đổi (request.env của gem vs giá trị trả về của client cũ), nhưng các field phải ánh xạ một-một.Bẫy 1: Stimulus controllers của gem âm thầm không load
Gem x402-rails đi kèm Stimulus controllers riêng. Claude viết code, test xanh hết. Tôi click tay nút thanh toán — không phản ứng.
Lý do: config/importmap.rb có pin cho @hotwired/stimulus trỏ tới một file vendor không tồn tại, và importmap âm thầm bỏ pin đó. Controllers của gem không bao giờ được load. Test không bắt được, vì bin/rails test không thực thi JS.
Bẫy 2: YAML parse 0x... thành integer
wallet_address: 0x833589... — không có dấu ngoặc. YAML thấy tiền tố 0x, đọc thành số nguyên hệ 16. Facilitator nhận một non-string và từ chối. Claude không dừng lại nghĩ về quy tắc parse YAML khi viết config.
Cả hai bẫy đều được bắt vì tôi click nút thật. Test pass không phải là feature hoạt động. Xác minh tay sau refactor không thể bỏ qua.
Refactor là tình huống "để Claude cầm lái" rủi ro cao nhất. Hàng rào không phải cho Claude. Chúng cho bạn — để khi Claude sai, bạn bắt được trong 5 phút thay vì khi production đang cháy.