Bạn giao cho Claude Code một dự án chưa quen thuộc và nhận được câu trả lời: "Lịch sử được lưu trong trình duyệt, giữ 100 mục gần nhất và được khôi phục sau khi tải lại trang". Nghe đủ cụ thể để đưa thẳng vào README. Nhưng trước hết, hãy mở các nguồn mà câu trả lời dựa vào.

Ta sẽ xem xét một kết luận như vậy: chuyển yêu cầu thành các câu hỏi, đọc một đoạn mã ngắn và xác định hành vi cần kiểm tra khi chạy ứng dụng. Cuối cùng, bạn sẽ có một báo cáo ngắn cho biết điều gì đang hoạt động và điều gì vẫn cần kiểm tra.

Một yêu cầu thay vì tổng quan cả dự án

Haiku 50 là ứng dụng nhỏ tạo thơ ba dòng từ các từ khóa. Người dùng chọn ngôn ngữ, nhận bài thơ và xem lịch sử các lần tạo. Với phần phân tích này, chỉ cần một yêu cầu: trình duyệt phải lưu 100 lần tạo gần nhất và vẫn truy cập được sau khi tải lại trang.

Minh họa về Haiku 50. Phần dưới sẽ phân tích hành vi ứng dụng qua mã và các kịch bản.

Ở đây, Haiku 50 là ví dụ dùng để học. Mã và câu trả lời của agent được chuẩn bị để phân tích; hành vi dự kiến được suy ra từ mã. Bài viết không kiểm tra tệp nén gốc của dự án. Mọi thứ cần thiết đều có trong bài: bạn không cần tải ứng dụng xuống, lấy API key hay chạy chức năng tạo thơ.

Thay vì hỏi "hãy giải thích mọi thứ ở đây hoạt động thế nào", ta sẽ đặt ba câu hỏi. Lần tạo mới có được ghi vào bộ lưu trữ không? Ứng dụng có đọc dữ liệu đã lưu khi mở lại không? Điều gì xảy ra khi có hơn 100 mục?

Câu hỏi đầu tiên gửi cho Claude Code có thể như sau:

Chỉ khảo sát lịch sử tạo thơ. Chưa thay đổi mã.
Tìm luồng từ lúc nhận bài thơ mới đến khi lưu,
sau đó tìm luồng từ lúc mở trang đến khi hiển thị lịch sử.
Kiểm tra nơi giới hạn số mục được lưu.
Với mỗi kết luận, nêu tệp, số dòng và một đoạn trích ngắn.
Liệt kê riêng những điều vẫn cần chạy thử.

Câu hỏi này xác định phạm vi tìm kiếm rõ ràng. Nếu agent phát hiện lịch sử liên quan đến một tệp khác, agent có thể giải thích vì sao cần đọc tệp đó. Bạn cũng sẽ dễ kiểm tra một vài vị trí then chốt hơn là rà soát toàn bộ phần giải thích ứng dụng.

Đường dẫn tệp chưa thay thế việc đọc mã

Hãy xem tệp minh họa history.js. Sau khi tạo thơ thành công, giao diện gọi addToHistory(item), còn renderHistory chỉ hiển thị danh sách được truyền vào. Trong ví dụ này không có trình xử lý lịch sử nào khác.

const STORAGE_KEY = "haiku-history";
let history = [];

function addToHistory(item) {
  history.unshift(item);
  localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
  renderHistory(history.slice(0, 100));
}

function startHistory() {
  renderHistory(history);
}
startHistory();

Để hiểu đoạn mã này, chỉ cần phân biệt hai nơi lưu trữ. Mảng history nằm trong bộ nhớ của trang đang mở. localStorage lưu dữ liệu của trang web trong trình duyệt giữa các lần mở; ứng dụng phải tự ghi dữ liệu vào đó và đọc lại. Bộ lưu trữ này gắn với giao thức, host và cổng, vì vậy khi kiểm tra, hãy dùng cùng địa chỉ và hồ sơ trình duyệt. Hành vi này được mô tả trong tài liệu localStorage.

Giả sử agent trả lời:

Lịch sử lưu 100 lần tạo gần nhất và được khôi phục
sau khi tải lại trang. Căn cứ: history.js:4-13,
sử dụng localStorage và slice(0, 100).

Đã có đường dẫn và số dòng. Giờ hãy kiểm tra xem chúng có xác nhận được toàn bộ câu trả lời không. Trong tệp minh họa, dòng đầu tiên bắt đầu bằng const STORAGE_KEY; các dòng trống cũng được tính.

  • dòng 5, history.unshift(item), thêm mục mới vào đầu mảng;
  • dòng 6, lời gọi localStorage.setItem, ghi toàn bộ mảng vào bộ lưu trữ. Chuyển đổi sang JSON giúp lưu mảng dưới dạng chuỗi;
  • dòng 7, renderHistory(history.slice(0, 100)), gửi 100 phần tử đầu tiên lên màn hình. slice tạo mảng mới mà không rút ngắn mảng ban đầu, như mô tả trong tài liệu của phương thức này;
  • khi khởi động, dòng 2, let history = [], tạo mảng rỗng; hàm startHistory cùng lời gọi ở dòng 10-13 đưa mảng đó lên màn hình. Ở đây không có thao tác đọc dữ liệu từ bộ lưu trữ.

Đọc xong đoạn mã, ta có thể điều chỉnh kết luận: mã có ghi lịch sử và giới hạn phần hiển thị ở 100 mục. Trong phiên bản này không có thao tác khôi phục khi khởi động, cũng không thấy giới hạn số mục được lưu.

Trong dự án thực tế, có thể cần thêm đoạn mã khác vì việc tải dữ liệu đôi khi nằm ở module riêng. Khi đó, hãy hỏi agent: "Cho tôi xem mọi nơi đọc và ghi khóa này, cùng những nơi gọi các hàm đó". Với ví dụ ở đây, phạm vi đã rõ và ta có thể tiếp tục.

Kiểm tra hành vi sau khi tải lại trang

Đọc mã đã gợi ý nơi cần tìm sự khác biệt. Tiếp theo, hãy xác định điều kiện quan sát. Trang được mở trong hồ sơ trình duyệt thông thường, bằng một địa chỉ cố định; chức năng lưu được bật và lịch sử ban đầu trống. Kịch bản minh họa có một lần tạo thành công với nhãn dễ nhận biết A; mã không đổi giữa các bước.

Dưới đây là diễn biến dự kiến theo mã. Ta thêm một bước nữa: sau khi tải lại trang, tạo mục mới B.

BướcDự kiến trên màn hìnhDự kiến trong bộ lưu trữ dưới khóa haiku-history
Mở trang khi lịch sử trốngDanh sách trốngKhông có mục nào
Thêm lần tạo AMột mục AMảng chứa mục A
Tải lại cùng trangDanh sách trốngMảng vẫn còn mục A
Thêm lần tạo B sau khi tải lạiChỉ có mục BMảng chỉ có B; mục A bị ghi đè

Để quan sát trong ứng dụng thật, hãy mở công cụ dành cho nhà phát triển của trình duyệt và xem phần lưu trữ dữ liệu trang web. Tìm giá trị của khóa cụ thể rồi đối chiếu với mục vừa thêm. Chỉ nhìn danh sách trống trên màn hình thì chưa đủ.

Nói "sau khi tải lại, mọi thứ đã bị xóa" là không chính xác. Dữ liệu vẫn còn trong bộ lưu trữ, nhưng ứng dụng không đọc lại. Trong phiên bản này, luồng xử lý bị ngắt giữa chuỗi đã lưu và mảng được hiển thị.

Lần thêm tiếp theo cho thấy hậu quả của lỗi này. Sau khi tải lại, mảng trong bộ nhớ trống, nên chỉ có B được thêm vào; localStorage.setItem sẽ thay lịch sử đã lưu bằng mảng chỉ có một mục. Theo yêu cầu, cả B lẫn A phải còn; theo mã, chỉ B còn lại. Vấn đề ảnh hưởng cả phần hiển thị lẫn việc lưu giữ lịch sử.

Ta có thể ghi kết luận từ mã như sau: "Trong startHistory không có thao tác đọc lịch sử đã lưu; lần thêm tiếp theo sẽ ghi đè các mục cũ". Để xác nhận hành vi khi chạy ứng dụng, hãy thực hiện đủ bốn bước và sau mỗi bước đối chiếu danh sách trên màn hình với dữ liệu trong bộ lưu trữ.

Kiểm tra giới hạn 100 mục

Ta đã làm rõ việc khôi phục lịch sử. Yêu cầu còn một phần nữa: lưu 100 lần tạo gần nhất. Không thể kiểm tra giới hạn này chỉ bằng một mục.

Hãy tạo kịch bản riêng với lịch sử trống. Lần lượt truyền 101 mục đã chuẩn bị, đánh nhãn từ 1 đến 101, vào cùng hàm thêm mục; không tải lại trang giữa các lần thêm. Không cần gửi 101 yêu cầu đến bộ tạo thơ để kiểm tra lịch sử: ở đây ta chỉ quan tâm đến hành vi sau khi đã có mục hoàn chỉnh.

Mục mới được thêm vào đầu mảng. Vì thế sau lần thêm cuối, màn hình sẽ hiện nhãn từ 101 đến 2, tổng cộng 100 mục. Nhưng lệnh lưu nhận toàn bộ mảng: bộ lưu trữ sẽ chứa các nhãn từ 101 đến 1, tổng cộng 101 mục. Cần kiểm tra cả số lượng lẫn thành phần. Nếu không, bạn có thể bỏ sót việc ứng dụng giữ lại các mục cũ nhất thay vì những mục mới nhất.

Đây là chỗ câu trả lời của agent khái quát quá mức. Trong mã quả thật có số 100, nhưng nó chỉ áp dụng cho phần hiển thị. Kết luận chính xác theo mã là: "Giao diện hiển thị 100 mục gần nhất; trong kịch bản này, bộ lưu trữ chứa 101 mục, vì vậy yêu cầu về số lượng lịch sử được lưu chưa được đáp ứng".

Kịch bản kiểm tra phần nào của ứng dụng?

Các mục đã chuẩn bị giúp kiểm tra riêng phần lịch sử. Chúng không cho biết dịch vụ tạo thơ có hoạt động hay không, chất lượng thơ ra sao hoặc ngôn ngữ đã chọn có được truyền đi không. Những yêu cầu đó cần kịch bản riêng.

Soạn báo cáo ngắn

Bạn nên lưu kết quả khảo sát cạnh dự án để ngày mai không phải đọc lại cuộc hội thoại. Trong ví dụ này, ta đặt tên tệp là HANDOFF_REVIEW.md. Tên chỉ là ví dụ; điều quan trọng là ghi chép đủ để có thể lần lại quá trình kiểm tra.

Dưới đây là báo cáo sau khi đọc mã minh họa. Mục hành vi dự kiến gợi ý điều cần quan sát khi chạy ứng dụng:

Phạm vi: lịch sử tạo thơ, phiên bản history.js minh họa trong bài viết.
Cách khảo sát: đọc đoạn mã minh họa.
Chưa chạy thử trên trình duyệt.

Yêu cầu: lưu 100 lần tạo gần nhất trong trình duyệt
và hiển thị lại sau khi tải lại trang.

Theo mã:
- history.js:6, localStorage.setItem, ghi toàn bộ mảng;
- history.js:7, renderHistory(history.slice(0, 100)),
  chỉ giới hạn phần hiển thị;
- history.js:10-13, startHistory và lời gọi hàm,
  hiển thị dữ liệu mà không đọc dữ liệu đã lưu.

Hành vi dự kiến theo mã:
- sau khi thêm, mục A xuất hiện trên màn hình và trong bộ lưu trữ;
- sau khi tải lại, A vẫn còn trong bộ lưu trữ, danh sách trên màn hình trống;
- thêm B sau khi tải lại sẽ thay thế lịch sử đã lưu:
  chỉ còn B thay vì cả B và A;
- trong kịch bản riêng, sau 101 lần thêm từ trạng thái trống,
  màn hình có 100 mục (101..2), bộ lưu trữ có 101 mục (101..1).

Khác biệt theo mã: không khôi phục khi khởi động,
nên lần thêm tiếp theo sẽ ghi đè lịch sử cũ;
lịch sử lưu trữ không giới hạn ở 100 mục gần nhất.

Chưa kiểm tra: hoạt động của bộ tạo thơ, lỗi khi lưu,
thao tác đồng thời trong nhiều thẻ trình duyệt.

Bước tiếp theo: chạy thử cả hai kịch bản và ghi lại quan sát.
Sau đó sửa việc đọc dữ liệu khi khởi động và giới hạn mảng cần lưu,
rồi chạy lại đúng các kịch bản đó.

Trong báo cáo của bạn, hãy thay "phiên bản minh họa" bằng phiên bản dự án đã kiểm tra, chẳng hạn commit và trạng thái có thay đổi chưa commit hay không. Sau khi chạy thử, thêm mục riêng "Quan sát thực tế" và đính kèm kết quả hoặc ghi lại các thao tác đã thực hiện. So sánh chúng với hành vi dự kiến; nếu không chạy được, hãy để trống mục này kèm ghi chú rằng chưa chạy. Ở đây, bài viết chứa toàn bộ dữ liệu đầu vào; với dự án lớn, chỉ dẫn số dòng là chưa đủ để lặp lại việc kiểm tra.

Báo cáo như vậy giúp giao cho agent yêu cầu tiếp theo. Agent đã có hai điểm sai khác cụ thể thay vì lời nhờ "làm cho lịch sử đáng tin cậy hơn". Đồng thời, bạn cũng thấy kết luận nào còn quá sớm.

Tự kiểm tra kết luận

Ta thêm nút xóa vào cùng phiên bản minh họa. Trình xử lý của nút như sau:

function clearHistory() {
  history = [];
  renderHistory(history);
}

Trước khi nhấn nút, danh sách và bộ lưu trữ đều có mục A. Sau khi nhấn, danh sách trống. Agent kết luận: "Nút này xóa hoàn toàn lịch sử đã lưu".

Kết luận nào được xác nhận: lịch sử biến mất khỏi màn hình, mục đã bị xóa khỏi localStorage hay cả hai? Tiếp theo nên kiểm tra điều gì? Hãy thử tự trả lời trước khi xem phần phân tích.

Phân tích

Ta xác nhận được danh sách đã biến mất khỏi màn hình. Mã cũng cho thấy mảng trong bộ nhớ được đặt lại thành rỗng. Tuy nhiên, trình xử lý không thay đổi localStorage; theo điều kiện đã nêu, renderHistory chỉ hiển thị danh sách, nên mục A đã lưu vẫn còn.

Bước kiểm tra tiếp theo là đọc giá trị của khóa haiku-history ngay sau khi nhấn nút. Sau khi bổ sung chức năng khôi phục lịch sử, bạn cũng nên tải lại trang để đảm bảo các mục đã xóa không xuất hiện trở lại. Với phiên bản ban đầu, chỉ tải lại trang sẽ không giúp ích: ngay cả khi chưa xóa, ứng dụng vẫn hiển thị danh sách trống lúc khởi động. Đây là một chi tiết dễ làm sai lệch kết quả kiểm tra.

Cách giao nhiệm vụ kiểu này cho Claude Code

Trong dự án thực tế, hãy bắt đầu bằng một nhận định ảnh hưởng đến quyết định tiếp theo của bạn. Chẳng hạn, có thể tiếp tục dùng lịch sử hay cần sửa chức năng khôi phục trước? Hãy yêu cầu agent trả lời theo định dạng giúp bạn mở nguồn và kiểm tra kết luận dễ dàng:

Kiểm tra nhận định: [hành vi cụ thể của ứng dụng].
Trước tiên, tìm các tệp liên quan và đọc những phần cần thiết.
Chưa thay đổi mã.

Hãy trả về:
1. Điều được mã xác nhận: tệp, số dòng, đoạn trích ngắn.
2. Kịch bản dùng để kiểm tra hành vi khi chạy ứng dụng.
3. Những gì đã thực sự thực hiện và quan sát được.
4. Kết luận chính xác và những điểm chưa rõ.

Nếu không thể chạy ứng dụng, hãy nói thẳng.
Đừng mô tả kịch bản đề xuất như thể đã kiểm tra.

Sau khi nhận câu trả lời, hãy mở các nguồn làm căn cứ cho kết luận chính, đối chiếu quan sát với yêu cầu rồi mới ghi kết quả vào ghi chú hoặc tài liệu. Nếu nguồn hiện có chưa đủ, bước tiếp theo đã rõ: tìm nơi đọc lịch sử, kiểm tra giá trị của khóa và lặp lại kịch bản với 101 mục. Nhờ vậy, quá trình khảo sát tiến lên từng bước nhỏ có thể kiểm chứng.

Với câu hỏi đơn giản về tên hàm, chỉ cần dẫn chiếu đến vị trí trong mã. Nếu câu trả lời đề cập đến việc lưu dữ liệu, khôi phục hoặc giới hạn kích thước, hãy theo dõi hành vi đó đến cùng. Phạm vi kiểm tra phụ thuộc vào điều bạn định khẳng định.