So sánh Cursor và Copilot với Claude Code cho side project
So sánh Cursor và Copilot với Claude Code trên một việc thật của side project, kèm giá cập nhật ngày 5/11/2026 và cách chọn theo kiểu làm việc của bạn.
So sánh Cursor và Copilot với Claude Code thì chọn cái nào? Theo mình, điều đáng so là bạn muốn giao cho AI bao nhiêu việc. Copilot rẻ nhất để bắt đầu, Cursor là trình soạn code xây quanh AI, còn Claude Code tự chạy lệnh và test. Cùng mình thử cả ba hướng trên một việc cụ thể nhé!
Side project đang dừng ở đâu?
Series này làm một app sổ chi tiêu qua tin nhắn. Bạn nhắn cà phê 35k thì app ghi một khoản chi 35.000đ với ghi chú "cà phê". Bạn nhắn /tong thì app trả lời tổng chi của tháng.
Trái tim của app là một hàm tên parseExpense. Hàm nhận một tin nhắn và trả về hai thứ, là ghi chú và số tiền. Nếu không hiểu tin nhắn, hàm trả về null, và app sẽ không ghi gì cả.
Ở bài trước, mình viết hàm này cùng AI, dựa trên một spec ngắn có bảng ví dụ. Hàm qua cả 11 test. Nhưng khi thử thêm vài tin nhắn mà bảng chưa có, mình gặp kết quả này:
"cà phê 35k" → {"note":"cà phê","amount":35000}
"cà phê 35 nghìn" → {"note":"cà phê nghìn","amount":35}
"bánh mì 20 ngàn" → {"note":"bánh mì ngàn","amount":20}
"học phí 2 triệu" → {"note":"học phí triệu","amount":2}Dòng đầu đúng. Ba dòng sau đều sai theo cùng một kiểu: hàm chỉ hiểu chữ k và tr viết liền sau số, nên khi người dùng viết chữ "nghìn", "ngàn" hay "triệu", hàm lấy con số trơn làm số tiền và đẩy chữ đó sang phần ghi chú. Một học phí 2 triệu bị ghi thành 2 đồng.
Bài này làm hai việc. Việc thứ nhất là sửa lỗi trên. Việc thứ hai là làm lệnh /tong. Mình dùng đúng hai việc này để so các công cụ AI, vì chúng đủ nhỏ để làm trong một buổi tối và đủ thật để thấy khác biệt.
Ba công cụ khác nhau ở chỗ nào?
Theo mình, so model nào mạnh hơn ít quan trọng với side project, vì cả ba công cụ đều cho dùng những model hàng đầu. Điểm khác thật sự là công cụ nằm ở đâu, và bạn giao cho nó bao nhiêu quyền.
Mức thứ nhất là gợi ý khi gõ. Bạn viết code, công cụ đoán và gợi ý phần tiếp theo, có khi một dòng, có khi cả hàm, bạn bấm Tab để nhận. Đây là việc mà GitHub Copilot nổi tiếng từ đầu, và Cursor cũng có tính năng tương tự. Bạn vẫn là người viết, AI chỉ gõ nhanh hơn giúp bạn.
Mức thứ hai là agent trong trình soạn code. Bạn mô tả việc cần làm trong khung chat, công cụ tự đọc nhiều file, sửa nhiều chỗ, rồi hiện các thay đổi để bạn duyệt ngay trong editor. Copilot có agent mode trong VS Code và các IDE khác. Cursor là một trình soạn code riêng, được xây quanh đúng kiểu làm việc này.
Mức thứ ba là agent tự chạy lệnh. Ngoài đọc và sửa file, công cụ còn tự chạy lệnh trong terminal như chạy test, đọc lỗi, sửa tiếp, cho tới khi xong rồi mới báo lại. Claude Code làm việc theo cách này, chạy trong terminal, trong VS Code hoặc trên web. Copilot cũng có một "coding agent" chạy trên đám mây, nhận việc và mở pull request trên GitHub.
Đi từ mức một lên mức ba, bạn giao cho AI nhiều việc hơn và nhận về những thay đổi lớn hơn. Đổi lại, bạn phải kiểm tra kỹ hơn, vì bạn không còn nhìn thấy từng dòng lúc nó được viết ra.
Giá và gói miễn phí, cập nhật ngày 5/11/2026
Các con số dưới đây mình đọc trực tiếp trên trang giá chính thức của từng hãng vào ngày 5/11/2026. Giá công cụ AI thay đổi rất thường xuyên, nên nếu bạn đọc bài vào lúc khác, hãy bấm vào link để xem lại.
Với GitHub Copilot, gói Free giá 0 USD, có 2.000 lượt gợi ý code mỗi tháng và một lượng chat, agent có giới hạn. Gói Pro giá 10 USD mỗi tháng, gợi ý không giới hạn, có thêm coding agent trên đám mây và review code. Sinh viên đã xác minh có gói Copilot Student riêng.
Với Cursor, có một thay đổi lớn mà bạn cần biết.
Từ ngày 12/10/2026, Cursor được gộp vào The X Subscription, dùng chung một lượng sử dụng với Grok. Gói Free có một lượng nhỏ để thử agent. Gói Plus giá 8 USD mỗi tháng, nhưng trang giá ghi rõ gói này chỉ dùng được model Grok. Muốn dùng model của hãng khác trong Cursor thì phải từ gói Premium giá 30 USD, và phần dùng model đó tính theo giá API. Người đang dùng gói Cursor cũ được giữ nguyên gói và giá.
Với Claude Code, gói Free của Claude không bao gồm Claude Code. Gói Pro giá 20 USD mỗi tháng, hoặc 17 USD mỗi tháng nếu trả theo năm, đã bao gồm Claude Code. Các gói Max 100 USD và Ultra 200 USD dành cho người dùng nhiều.
Chuẩn bị project để làm theo
Bạn cần Node.js 22.18 trở lên, vì từ bản này Node chạy thẳng được file TypeScript và có sẵn bộ test node:test, không phải cài thêm gì. Project chỉ có bốn file:
so-chi-tieu/
src/
parse-expense.ts # đọc một tin nhắn → { note, amount } hoặc null
parse-expense.test.ts # test cho hàm trên
so-chi-tieu.ts # xử lý tin nhắn: ghi khoản chi, hoặc trả lời /tong
so-chi-tieu.test.ts # test cho lệnh /tongHai file test bạn tự viết ở hai bước tiếp theo, trước khi nhờ AI. Hai file code là thứ công cụ AI sẽ viết. Phần cuối bài có đầy đủ code mà mình nhận được, để bạn so với kết quả của mình.
Bước 1: viết ra kết quả mong muốn, mỗi trường hợp một lý do
Trước khi mở công cụ AI nào, mình viết ra từng tin nhắn và kết quả mình muốn nhận. Làm việc này trước là để chính mình quyết định, chứ không để AI đoán. Mỗi dòng đi kèm một lý do:
cà phê 35 nghìnthì ghi 35.000đ, ghi chú "cà phê". Đây là lỗi cần sửa.bánh mì 20 ngànthì ghi 20.000đ, vì nhiều người, nhất là ở miền Nam, viết "ngàn" thay cho "nghìn".học phí 2 triệuthì ghi 2.000.000đ. Chữ "triệu" là lỗi cùng loại với "nghìn".35kthì không ghi gì, vì tin nhắn có số tiền mà không có ghi chú, nên không biết tiền tiêu vào việc gì. Trường hợp này đã có test từ bài trước, nhưng chưa ai ghi lý do, nên mình viết lại ở đây.laptop 1 triệu 2thì không ghi gì. Câu này mơ hồ, vì có thể là 1,2 triệu, cũng có thể là "1 triệu" cộng với "2 cái". Thay vì để app đoán, mình quyết định chưa hỗ trợ và nhắc người dùng nhắn1tr2, cách viết mà app đã hiểu từ bài trước.
Dòng cuối cùng là chỗ mình muốn bạn chú ý. Đó là một quyết định về sản phẩm, và nếu không viết ra trước, công cụ AI nào cũng sẽ tự chọn giúp bạn một đáp án.
Bốn trường hợp mới này, cộng với 11 trường hợp cũ từ bài trước, trở thành file test. Mỗi trường hợp là một test riêng:
import assert from "node:assert/strict";
import { test } from "node:test";
import { parseExpense } from "./parse-expense.ts";
// [tin nhắn, ghi chú mong muốn, số tiền mong muốn]
const ok: [string, string, number][] = [
// 7 trường hợp từ bài trước
["cà phê 35k", "cà phê", 35000],
["cơm trưa 45.000", "cơm trưa", 45000],
["xăng 50000", "xăng", 50000],
["tiền nhà 3tr", "tiền nhà", 3000000],
["điện thoại 1tr2", "điện thoại", 1200000],
["trà sữa 32K", "trà sữa", 32000],
["35k cà phê", "cà phê", 35000],
// mới trong bài này
["cà phê 35 nghìn", "cà phê", 35000],
["bánh mì 20 ngàn", "bánh mì", 20000],
["học phí 2 triệu", "học phí", 2000000],
];
for (const [message, note, amount] of ok) {
test(`đọc được "${message}"`, () => {
assert.deepEqual(parseExpense(message), { note, amount });
});
}
// Những tin nhắn app phải trả lời "không hiểu" (hàm trả về null)
for (const message of ["cà phê", "cà phê 0k", "cà phê 35k 40k", "35k", "laptop 1 triệu 2"]) {
test(`không hiểu "${message}"`, () => {
assert.equal(parseExpense(message), null);
});
}Nếu bạn còn giữ hàm của bài trước, hãy chạy file test này với hàm cũ. Bạn sẽ thấy đúng ba test thất bại:
not ok 8 - đọc được "cà phê 35 nghìn"
not ok 9 - đọc được "bánh mì 20 ngàn"
not ok 10 - đọc được "học phí 2 triệu"
# pass 12
# fail 3Ba test thất bại khớp đúng ba lỗi ở đầu bài. Bước này trông thừa, nhưng nó cho bạn biết các test mới thật sự kiểm tra được lỗi. Một test chưa bao giờ đỏ thì bạn không chắc nó bắt được gì.
Bước 2: quy tắc cho lệnh /tong
Lệnh /tong được xử lý trong hàm handleMessage(text, now, entries). Hàm nhận tin nhắn, thời điểm nhắn và danh sách các khoản chi đã ghi. Nếu tin nhắn là /tong, hàm cộng các khoản trong tháng hiện tại và trả về một câu như Tháng 11/2026: 3 khoản, tổng 1.280.000đ. Nếu không phải, hàm gọi parseExpense để ghi khoản chi mới.
Nghe đơn giản, nhưng có một câu hỏi mà mình suýt bỏ qua: "tháng hiện tại" là tháng theo giờ nào? App sẽ chạy trên máy chủ, mà máy chủ của Vercel hay GitHub Actions đều dùng giờ UTC, chậm hơn giờ Việt Nam 7 tiếng.
Bạn có thể tự thấy vấn đề bằng một lệnh. Lệnh dưới đây tạo thời điểm 6:30 sáng ngày 1/12 theo giờ Việt Nam, rồi hỏi "đây là tháng mấy" bằng cách thông thường nhất là getMonth(). Mình chạy hai lần, lần đầu với máy đặt giờ UTC như máy chủ, lần sau với giờ Việt Nam như máy của bạn:
TZ=UTC node -e 'const d = new Date("2026-12-01T06:30+07:00"); console.log(d.getMonth() + 1)'
TZ=Asia/Ho_Chi_Minh node -e 'const d = new Date("2026-12-01T06:30+07:00"); console.log(d.getMonth() + 1)'11
12Cùng một thời điểm, máy chủ trả lời tháng 11, còn máy của bạn trả lời tháng 12. getMonth() dùng múi giờ của máy đang chạy code, và lúc 6:30 sáng 1/12 ở Việt Nam thì ở UTC vẫn là 23:30 ngày 30/11. Nếu dùng getMonth(), ly cà phê sáng đầu tháng sẽ bị cộng vào tháng trước, và mọi test chạy trên máy bạn vẫn xanh. Lỗi chỉ lộ ra sau khi deploy.
Vì vậy, quy tắc mình viết ra là tính tháng theo giờ Việt Nam, bất kể máy chạy ở đâu, kèm các trường hợp cụ thể:
- Ghi 3 khoản trong tháng 11 rồi nhắn
/tong, kết quả là đúng 3 khoản với tổng đúng. - Tháng chưa có khoản nào thì trả lời
Tháng 11/2026 chưa có khoản chi nào., không trả về "0đ" gây hiểu lầm. - Khoản chi lúc 23:59 ngày 30/11 giờ Việt Nam vẫn thuộc tháng 11.
- Khoản chi lúc 6:30 sáng 1/12 giờ Việt Nam thuộc tháng 12. Đây là trường hợp
getMonth()làm sai trên máy chủ. - Tin nhắn không hiểu thì không được ghi gì vào danh sách.
import assert from "node:assert/strict";
import { test } from "node:test";
import { handleMessage, type Entry } from "./so-chi-tieu.ts";
// Viết thời điểm theo giờ Việt Nam (+07:00) cho dễ đọc.
const vn = (iso: string) => new Date(`${iso}+07:00`);
test("ghi ba khoản rồi /tong ra đúng tổng tháng", () => {
const entries: Entry[] = [];
handleMessage("cà phê 35k", vn("2026-11-03T08:00"), entries);
handleMessage("cơm trưa 45.000", vn("2026-11-03T12:00"), entries);
handleMessage("điện thoại 1tr2", vn("2026-11-04T20:00"), entries);
assert.equal(handleMessage("/tong", vn("2026-11-05T09:00"), entries), "Tháng 11/2026: 3 khoản, tổng 1.280.000đ");
});
test("tháng chưa có khoản nào", () => {
assert.equal(handleMessage("/tong", vn("2026-11-05T09:00"), []), "Tháng 11/2026 chưa có khoản chi nào.");
});
test("23:59 ngày 30/11 giờ VN vẫn thuộc tháng 11", () => {
const entries: Entry[] = [];
handleMessage("trà sữa 32k", vn("2026-11-30T23:59"), entries);
assert.equal(handleMessage("/tong", vn("2026-11-30T23:59"), entries), "Tháng 11/2026: 1 khoản, tổng 32.000đ");
});
test("6:30 sáng 1/12 giờ VN (còn là 30/11 theo UTC) thuộc tháng 12", () => {
const entries: Entry[] = [];
handleMessage("trà sữa 32k", vn("2026-11-30T20:00"), entries);
handleMessage("bánh mì 20 ngàn", vn("2026-12-01T06:30"), entries);
assert.equal(handleMessage("/tong", vn("2026-12-01T07:00"), entries), "Tháng 12/2026: 1 khoản, tổng 20.000đ");
});
test("tin nhắn không hiểu thì không lưu gì", () => {
const entries: Entry[] = [];
const reply = handleMessage("cà phê", vn("2026-11-05T09:00"), entries);
assert.match(reply, /chưa hiểu/);
assert.equal(entries.length, 0);
});Test thứ tư có hai khoản chi, một khoản tối 30/11 và một khoản sáng 1/12. Kết quả mong đợi là tháng 12 chỉ có đúng một khoản 20.000đ. Nếu code dùng getMonth() và test chạy với TZ=UTC, kết quả sẽ thành tháng 11 với hai khoản, và test đỏ ngay.
Bước 3: một prompt dùng chung cho cả ba công cụ
Đến đây mình mới viết prompt. Prompt này dùng được cho cả ba công cụ, vì nó chỉ nói ba điều: việc cần làm, nơi tìm thông tin, và cách chứng minh là xong:
Trong src/ có hai file test: parse-expense.test.ts và so-chi-tieu.test.ts. Đừng sửa test.
1. Sửa src/parse-expense.ts để hiểu thêm "nghìn", "ngàn", "triệu" đứng sau số,
đúng như các trường hợp trong parse-expense.test.ts.
2. Viết src/so-chi-tieu.ts có hàm handleMessage(text, now, entries) và kiểu Entry,
tính tháng theo giờ Việt Nam (Asia/Ho_Chi_Minh). Không dùng thư viện ngoài.
3. Chạy cả hai file test với TZ=UTC và dán kết quả.
Nếu có chỗ chưa rõ, hỏi lại trước khi viết code.Câu "đừng sửa test" quan trọng hơn bạn nghĩ. Một agent được giao "làm cho test qua" có thể chọn con đường ngắn nhất, là sửa luôn test. Còn TZ=UTC là để test chạy trên máy bạn giống hệt trên máy chủ, đúng cái bẫy ở bước 2.
Kết quả khi làm bằng Claude Code
Code dưới đây được làm bằng Claude Code với đúng prompt ở trên. Đây là hàm đọc số tiền sau khi sửa:
export type Expense = { note: string; amount: number };
const MAX_AMOUNT = 1_000_000_000;
// Một "token" số tiền đứng riêng: 35k · 32K · 3tr · 1tr2 · 45.000 · 50000 · 35 nghìn · 20 ngàn · 2 triệu
const AMOUNT = /(?<![\p{L}\d])(\d{1,3}(?:[.,]\d{3})+|\d+)(?:(k|tr)(\d)?| (nghìn|ngàn|triệu))?(?![\p{L}\d])/giu;
const UNIT: Record<string, number> = { k: 1_000, nghìn: 1_000, ngàn: 1_000, tr: 1_000_000, triệu: 1_000_000 };
function toAmount(digits: string, unit: string | undefined, decimal: string | undefined): number {
const base = Number(digits.replace(/[.,]/g, ""));
if (!unit) return base;
const multiplier = UNIT[unit.toLowerCase()];
// "1tr2": chữ số sau "tr" là hàng trăm nghìn.
if (decimal && multiplier === 1_000_000) return base * multiplier + Number(decimal) * 100_000;
if (decimal) return Number.NaN; // "35k2" chưa được hỗ trợ
return base * multiplier;
}
export function parseExpense(input: string): Expense | null {
// Tin nhắn gõ trên điện thoại có thể ở dạng dấu tổ hợp (NFD); đưa về một dạng để so khớp "nghìn".
const message = input.normalize("NFC");
const matches = [...message.matchAll(AMOUNT)];
if (matches.length !== 1) return null; // thiếu số tiền, hoặc có nhiều hơn một
const [token, digits, shortUnit, decimal, wordUnit] = matches[0];
const amount = toAmount(digits, shortUnit ?? wordUnit, decimal);
if (!Number.isInteger(amount) || amount <= 0 || amount > MAX_AMOUNT) return null;
const note = message.replace(token, " ").replace(/\s+/g, " ").trim();
if (!note) return null;
return { note, amount };
}Thay đổi chính nằm ở biểu thức chính quy AMOUNT. Sau con số, giờ có hai khả năng: hoặc là k, tr viết liền (kèm một chữ số lẻ như 1tr2), hoặc là một dấu cách rồi tới chữ "nghìn", "ngàn", "triệu". Bảng UNIT đổi đơn vị thành hệ số nhân. Còn laptop 1 triệu 2 trả về null nhờ dòng matches.length !== 1, vì hàm tìm thấy hai số tiền là "1 triệu" và "2".
Và đây là file xử lý tin nhắn:
import { parseExpense } from "./parse-expense.ts";
export type Entry = { note: string; amount: number; at: Date };
const VN_TIME = "Asia/Ho_Chi_Minh";
const HUONG_DAN = 'Mình chưa hiểu. Nhắn kiểu "cà phê 35k" để ghi một khoản chi, hoặc /tong để xem tổng tháng này.';
// Tháng/năm theo giờ Việt Nam, kể cả khi máy chủ chạy theo giờ UTC.
function thangVN(date: Date): string {
const parts = new Intl.DateTimeFormat("en-GB", { timeZone: VN_TIME, month: "2-digit", year: "numeric" }).formatToParts(date);
const month = parts.find((p) => p.type === "month")!.value;
const year = parts.find((p) => p.type === "year")!.value;
return `${month}/${year}`;
}
const vnd = (n: number) => `${n.toLocaleString("vi-VN")}đ`;
export function tongThang(entries: Entry[], now: Date): string {
const thang = thangVN(now);
const trongThang = entries.filter((entry) => thangVN(entry.at) === thang);
if (trongThang.length === 0) return `Tháng ${thang} chưa có khoản chi nào.`;
const tong = trongThang.reduce((sum, entry) => sum + entry.amount, 0);
return `Tháng ${thang}: ${trongThang.length} khoản, tổng ${vnd(tong)}`;
}
export function handleMessage(text: string, now: Date, entries: Entry[]): string {
if (text.trim().toLowerCase() === "/tong") return tongThang(entries, now);
const expense = parseExpense(text);
if (!expense) return HUONG_DAN;
entries.push({ ...expense, at: now });
return `Đã ghi: ${expense.note}, ${vnd(expense.amount)}`;
}Hàm thangVN thay cho getMonth(). Nó hỏi Intl.DateTimeFormat rằng "thời điểm này, nhìn từ múi giờ Asia/Ho_Chi_Minh, là tháng mấy năm nào", nên kết quả không phụ thuộc vào múi giờ của máy chủ. Hai khoản chi cùng tháng khi chuỗi "11/2026" của chúng giống nhau.
Chạy hai file test với TZ=UTC:
ok 1 - ghi ba khoản rồi /tong ra đúng tổng tháng
ok 2 - tháng chưa có khoản nào
ok 3 - 23:59 ngày 30/11 giờ VN vẫn thuộc tháng 11
ok 4 - 6:30 sáng 1/12 giờ VN (còn là 30/11 theo UTC) thuộc tháng 12
ok 5 - tin nhắn không hiểu thì không lưu gì
# pass 5
# fail 0File parse-expense.test.ts cũng qua hết 15 trên 15 test, gồm 11 test cũ và 4 test mới.
Test xanh rồi, vẫn còn một lỗi
Phần mình thấy đáng kể nhất lại đến sau khi test đã xanh. Theo thói quen từ bài trước, mình gọi parseExpense với vài tin nhắn không có trong file test, để xem hàm xử lý những cách viết chưa ai nghĩ tới:
"cà phê 35 Nghìn" → {"note":"cà phê","amount":35000}
"gạo 35kg" → null
"cà phê 35k2" → null
"cà phê 35 nghìn" → {"note":"cà phê nghìn","amount":35}Ba dòng đầu ổn. Chữ "Nghìn" viết hoa vẫn được hiểu. gạo 35kg không bị nhầm thành 35.000đ, vì "kg" là đơn vị khối lượng chứ không phải tiền. 35k2 bị từ chối, vì cách viết này chưa có nghĩa rõ ràng.
Dòng cuối thì sai. Giữa "35" và "nghìn" có hai dấu cách, kiểu gõ thừa rất hay gặp trên điện thoại, và nó tái hiện đúng lỗi 35 đồng mà cả bài đang sửa. Nhìn lại biểu thức chính quy ở trên, bạn sẽ thấy phần | (nghìn|ngàn|triệu) chỉ chấp nhận đúng một dấu cách.
Lỗi này không phải do công cụ AI làm ẩu. Khi viết danh sách kết quả mong muốn ở bước 1, mình không nghĩ tới chuyện gõ thừa dấu cách, nên không có test nào cho nó, và AI chỉ làm đúng những gì test yêu cầu. Cách sửa vẫn theo đúng thứ tự cũ. Mình thêm trường hợp ["cà phê 35 nghìn", "cà phê", 35000] vào file test, chạy để thấy nó đỏ, rồi đổi dấu cách trong biểu thức thành \s+ (một hoặc nhiều khoảng trắng). Đây là dòng AMOUNT sau khi sửa, bạn thay dòng tương ứng trong parse-expense.ts ở trên:
const AMOUNT = /(?<![\p{L}\d])(\d{1,3}(?:[.,]\d{3})+|\d+)(?:(k|tr)(\d)?|\s+(nghìn|ngàn|triệu))?(?![\p{L}\d])/giu;Sau đó cả 16 test đọc số tiền và 5 test của /tong đều qua.
Công cụ nào cũng làm theo test của bạn, kể cả khi test còn thiếu, nên việc thử thêm ngoài test vẫn là việc của bạn.
Cùng việc đó trong Copilot hay Cursor sẽ khác thế nào?
Mình nói trước cho rõ là mình không chạy thử Copilot và Cursor trên việc này trong cùng điều kiện, nên mình không chấm điểm hay so tốc độ. Một bài so sánh công bằng cần cùng model, cùng prompt, chạy nhiều lần, và đó là một thí nghiệm lớn hơn nhiều so với một buổi tối. Điều mình nói được là quy trình sẽ khác nhau thế nào.
Nếu bạn dùng gợi ý khi gõ của Copilot hoặc Tab của Cursor, bạn sẽ tự mở so-chi-tieu.ts, gõ tên hàm thangVN, và nhận gợi ý từng đoạn. Bạn đọc từng dòng trước khi nhận, nên hiểu code rất kỹ, nhưng bạn phải tự biết rằng cần tính theo múi giờ. Nếu bạn không nghĩ ra điều đó, gợi ý rất có thể sẽ dùng getMonth(), vì đó là cách phổ biến nhất. May là test số 4 sẽ đỏ khi chạy với TZ=UTC.
Nếu bạn dùng agent mode của Copilot hoặc agent của Cursor, bạn dán prompt chung vào khung chat. Agent đọc test, sửa hai file, và bạn duyệt từng thay đổi ngay trong editor. Bạn vẫn nên tự chạy test, hoặc cho phép agent chạy giúp nếu công cụ hỗ trợ.
Nếu bạn dùng Claude Code, bạn gõ prompt vào terminal, và nó tự chạy test, đọc kết quả, sửa tiếp cho tới khi qua. Bạn đọc diff cuối cùng. Cách này nhanh nhất khi việc có nhiều bước, nhưng cũng dễ khiến bạn chỉ lướt diff rồi bấm đồng ý nhất.
Vậy nên chọn công cụ nào cho side project?
Đây là gợi ý của mình, dựa trên kiểu làm việc chứ không dựa trên bảng xếp hạng.
Nếu bạn còn là sinh viên hoặc đang học, hãy bắt đầu với Copilot Free hoặc gói Student. Gợi ý khi gõ giúp bạn nhanh hơn mà vẫn phải tự viết, tự hiểu từng dòng, điều quan trọng nhất ở giai đoạn này.
Nếu bạn muốn AI làm cả một thay đổi nhiều file nhưng vẫn duyệt từng chỗ trong editor quen thuộc, agent mode của Copilot Pro hoặc Cursor là lựa chọn tự nhiên. Với Cursor, bạn nhớ đọc kỹ gói mới, vì gói Plus chỉ có model Grok.
Nếu side project của bạn đã có test như app sổ chi tiêu, và bạn quen làm việc với terminal, Claude Code tận dụng được điều đó tốt nhất, vì nó tự chạy test thay bạn. Đổi lại, nó không có gói miễn phí.
Theo mình, đừng trả tiền cho hai công cụ cùng lúc khi mới bắt đầu. Hãy chọn một, dùng trọn một tuần trên cùng một việc thật, với cùng hai file test và prompt chung như ở trên. Cuối tuần, bạn tự đếm xem mình mất bao lâu, test qua bao nhiêu, và phải sửa lại bao nhiêu lần. Con số của chính bạn đáng tin hơn mọi bài so sánh, kể cả bài này.
Test xanh rồi, nhưng bạn có giải thích được code không?
Nhìn lại hàm thangVN ở trên. Nó chạy đúng, qua cả ca ranh giới tháng. Nhưng nếu có người hỏi formatToParts trả về cái gì, vì sao lại dùng "en-GB" chứ không phải "vi-VN", hay dấu ! sau find(...) có nguy hiểm không, bạn có trả lời được không?
Đây là cái bẫy lớn nhất khi làm cùng AI: code chạy được và test xanh, nhưng người chịu trách nhiệm về code lại không hiểu nó. Bài một workflow dùng AI mà vẫn giữ được tư duy có nói về tinh thần chung. Còn bài viết tiếp theo của series sẽ đi vào những mẫu prompt cụ thể mình dùng hằng ngày, trong đó có mẫu bắt AI giải thích code của chính nó cho tới khi bạn hiểu. Bạn theo dõi cả series ở Làm side project cùng AI, và nếu đang ôn phỏng vấn thì câu hỏi "bạn chọn công cụ AI dựa trên tiêu chí nào" có trong ngân hàng câu hỏi phỏng vấn.