04. Task Planning & Test Strategy — Vibe Coding Series

Chia nhỏ feature thành task đúng granularity cho AI agent, lên test strategy trước khi code để tránh 800 dòng code không test được.

Tôi từng giao cho AI một task: “Build toàn bộ auth module cho TaskFlow.” Hai tiếng sau, AI trả về 800 dòng code — JWT, bcrypt, middleware, refresh token, session management, tất cả trong một file. Code chạy được. Nhưng khi tôi muốn test từng phần, tôi không biết bắt đầu từ đâu. Khi tôi muốn sửa logic refresh token, tôi phải đọc qua 600 dòng code không liên quan. Khi tôi muốn thêm OAuth sau này, tôi phải refactor gần như toàn bộ.

Vấn đề không phải AI viết code xấu. Vấn đề là tôi giao task sai.


Bối cảnh

Ở bài 03, tôi đã xây design system cho TaskFlow — token, 5 atom component, và cập nhật AI_CONTEXT.md để AI có ngôn ngữ thiết kế nhất quán. Bài này bước vào Phase 3: trước khi AI viết một dòng code nào, chúng ta cần biết chính xác AI sẽ làm gì — và làm đến đâu.


Granularity đúng cho AI task

Task quá lớn

Khi task quá lớn, AI phải tự đưa ra hàng chục quyết định thiết kế mà không có input từ bạn. Kết quả là code phản ánh “best guess” của AI, không phải ý định của bạn. Và vì tất cả nằm trong một khối lớn, bạn không có checkpoint để review từng quyết định đó.

Dấu hiệu task quá lớn: mô tả task dùng từ “toàn bộ”, “module”, “system”, hoặc liệt kê hơn 3 chức năng.

Task quá nhỏ

Ngược lại, task quá nhỏ tạo ra overhead không cần thiết. “Viết function kiểm tra email có chứa ký tự @” là task quá nhỏ — bạn mất nhiều thời gian viết prompt hơn là AI viết code.

Dấu hiệu task quá nhỏ: task chỉ có 1–2 dòng code, không có logic thực sự, hoặc là sub-step của một operation đơn giản.

Sweet spot

1 task = 1 function hoặc 1 component có input/output rõ ràng.

Ví dụ tốt:

  • “Viết function hashPassword(plaintext: string): Promise<string> dùng bcrypt với salt rounds = 12”
  • “Viết function verifyToken(token: string): JWTPayload | null — return null nếu token invalid hoặc expired”
  • “Viết component LoginForm nhận prop onSubmit(email, password), có validation, loading state, và error message”

Mỗi task này có input rõ ràng, output rõ ràng, và có thể test độc lập.


Task breakdown với AI

Từ feature đến task list

Thay vì tự breakdown, tôi dùng AI như một công cụ để tạo task list — nhưng tôi là người review và quyết định cuối cùng.

Prompt tôi dùng:

Prompt:
"Feature: User authentication cho TaskFlow (email/password).
Requirements đã chốt:
- Đăng ký với email + password
- Đăng nhập, nhận JWT access token (15 phút) + refresh token (7 ngày)
- Refresh token endpoint
- Logout (invalidate refresh token)
- Middleware xác thực cho protected routes

Hãy breakdown thành task list với:
- Mỗi task: tên, mô tả ngắn, acceptance criteria (2–3 điểm)
- Đánh dấu dependency (task nào phải làm trước)
- Đánh dấu owner: AI hay Human"

AI trả về task list. Tôi review, điều chỉnh acceptance criteria cho rõ hơn, và đây là kết quả cuối cùng:

## Feature: User Authentication

### Task 1: Password hashing utilities
Owner: AI
Depends on: —
Acceptance criteria:
- hashPassword(plaintext) → bcrypt hash với salt 12
- verifyPassword(plaintext, hash) → boolean
- Unit test cover cả happy path và wrong password

### Task 2: JWT utilities
Owner: AI
Depends on: —
Acceptance criteria:
- generateAccessToken(userId) → JWT, expire 15 phút
- generateRefreshToken(userId) → JWT, expire 7 ngày
- verifyToken(token) → payload hoặc null (không throw)

### Task 3: User model & repository
Owner: AI
Depends on: —
Acceptance criteria:
- Schema: id, email (unique), passwordHash, createdAt
- createUser(email, passwordHash) → User
- findUserByEmail(email) → User | null

### Task 4: Refresh token store
Owner: AI
Depends on: Task 3
Acceptance criteria:
- saveRefreshToken(userId, token, expiresAt)
- findRefreshToken(token) → RefreshTokenRecord | null
- deleteRefreshToken(token)
- deleteAllUserTokens(userId)

### Task 5: Register endpoint
Owner: AI
Depends on: Task 1, Task 3
Acceptance criteria:
- POST /auth/register nhận { email, password }
- Validate email format, password min 8 ký tự
- Return 201 + { userId } hoặc 409 nếu email đã tồn tại

### Task 6: Login endpoint
Owner: AI
Depends on: Task 1, Task 2, Task 3, Task 4
Acceptance criteria:
- POST /auth/login nhận { email, password }
- Return 200 + { accessToken, refreshToken } hoặc 401
- Lưu refresh token vào store

### Task 7: Refresh token endpoint
Owner: AI
Depends on: Task 2, Task 4
Acceptance criteria:
- POST /auth/refresh nhận { refreshToken }
- Return 200 + { accessToken } hoặc 401 nếu token invalid/expired
- Không rotate refresh token (đơn giản hóa cho MVP)

### Task 8: Auth middleware
Owner: AI
Depends on: Task 2
Acceptance criteria:
- Middleware đọc Authorization header (Bearer token)
- Attach decoded payload vào req.user
- Return 401 nếu token missing hoặc invalid

Dependency graph

Nhìn vào task list trên, dependency graph rõ ràng:

Dependency graph của auth tasks — Task 1, 2, 3 song song; các task sau phụ thuộc vào chúng

Task 1, 2, 3 không có dependency — có thể làm song song. Task 4 cần Task 3. Task 5 cần Task 1 và 3. Và cứ thế.

Ownership: AI làm gì, Human làm gì

Trong 8 task trên, tất cả đều giao cho AI — nhưng có những loại task nên để Human làm:

  • Security-critical decisions: chọn thuật toán hash, token expiry policy, rate limiting strategy
  • Architecture decisions: cấu trúc database schema, cách tổ chức module
  • Integration với external service: OAuth provider, email service, payment gateway
  • Business logic phức tạp: pricing rule, permission matrix, workflow state machine

Với TaskFlow, tôi tự quyết định dùng bcrypt (không phải argon2), JWT (không phải session), và refresh token store trong database (không phải Redis) — rồi mới giao AI implement theo quyết định đó.


Test strategy trước khi code

TDD với AI

Thay vì để AI viết code rồi mới viết test, tôi viết test spec trước — rồi giao AI implement để test pass.

Lợi ích: test spec chính là acceptance criteria được formalize. AI có target rõ ràng để implement. Và tôi biết chính xác khi nào task “done”.

Property-based testing cho business logic

Với business logic như password hashing hay JWT, unit test thông thường chỉ cover vài case cụ thể. Property-based testing cho phép bạn phát biểu tính chất của function và test với hàng trăm input ngẫu nhiên.

Ví dụ: “Với bất kỳ password nào, verifyPassword(password, hashPassword(password)) phải luôn trả về true.” Đây là property — không phải test case cụ thể.

E2E test cho critical path

Với auth feature, critical path là: đăng ký → đăng nhập → dùng access token → refresh → logout. Tôi viết 1 E2E test cover toàn bộ flow này — không cần mock, test trực tiếp trên API.


Ví dụ thực chiến: Test spec cho 3 task đầu

Đây là test spec tôi viết trước khi giao AI implement Task 1, 2, và 8:

// auth/password.test.ts
describe("Password utilities", () => {
  describe("hashPassword", () => {
    it("returns a bcrypt hash (starts with $2b$)", async () => {
      const hash = await hashPassword("mypassword123");
      expect(hash).toMatch(/^\$2b\$/);
    });

    it("produces different hashes for the same password (salt)", async () => {
      const hash1 = await hashPassword("samepassword");
      const hash2 = await hashPassword("samepassword");
      expect(hash1).not.toBe(hash2);
    });
  });

  describe("verifyPassword", () => {
    it("returns true for correct password", async () => {
      const hash = await hashPassword("correct-password");
      expect(await verifyPassword("correct-password", hash)).toBe(true);
    });

    it("returns false for wrong password", async () => {
      const hash = await hashPassword("correct-password");
      expect(await verifyPassword("wrong-password", hash)).toBe(false);
    });
  });
});

// auth/jwt.test.ts
describe("JWT utilities", () => {
  describe("generateAccessToken + verifyToken", () => {
    it("verifyToken returns payload for valid token", () => {
      const token = generateAccessToken("user-123");
      const payload = verifyToken(token);
      expect(payload?.userId).toBe("user-123");
    });

    it("verifyToken returns null for tampered token", () => {
      const token = generateAccessToken("user-123");
      const tampered = token.slice(0, -5) + "XXXXX";
      expect(verifyToken(tampered)).toBeNull();
    });

    it("access token expires after 15 minutes", async () => {
      // Dùng fake timer để test expiry
      vi.useFakeTimers();
      const token = generateAccessToken("user-123");
      vi.advanceTimersByTime(16 * 60 * 1000); // 16 phút
      expect(verifyToken(token)).toBeNull();
      vi.useRealTimers();
    });
  });
});

// auth/middleware.test.ts
describe("Auth middleware", () => {
  it("attaches user payload to req.user for valid token", () => {
    const token = generateAccessToken("user-456");
    const req = mockRequest({ headers: { authorization: `Bearer ${token}` } });
    const res = mockResponse();
    const next = vi.fn();

    authMiddleware(req, res, next);

    expect(next).toHaveBeenCalled();
    expect(req.user?.userId).toBe("user-456");
  });

  it("returns 401 when Authorization header is missing", () => {
    const req = mockRequest({ headers: {} });
    const res = mockResponse();
    const next = vi.fn();

    authMiddleware(req, res, next);

    expect(res.status).toHaveBeenCalledWith(401);
    expect(next).not.toHaveBeenCalled();
  });
});

Với test spec này, tôi giao AI: “Implement hashPassword, verifyPassword, generateAccessToken, verifyToken, và authMiddleware để tất cả test trên pass.” AI có target rõ ràng, không cần đoán mò.


Những điều cần tránh

Task không có acceptance criteria rõ ràng. “Viết auth module” không phải acceptance criteria. “POST /auth/login trả về 200 + accessToken khi email/password đúng, 401 khi sai” mới là acceptance criteria. Không có criteria rõ ràng, AI sẽ tự quyết định “done” theo cách của nó — và thường không khớp với kỳ vọng của bạn.

Bỏ qua test strategy đến cuối mới viết. Viết test sau khi code xong là anti-pattern kinh điển — bạn sẽ vô tình viết test để fit code hiện tại thay vì test behavior thực sự. Với AI, vấn đề còn tệ hơn: AI sẽ viết test pass 100% vì nó biết code của mình làm gì. Test như vậy không có giá trị.


Tóm tắt

  • Sweet spot: 1 task = 1 function/component có input/output rõ ràng
  • Dùng AI để tạo task list, nhưng bạn review và quyết định acceptance criteria
  • Dependency graph giúp xác định thứ tự và cơ hội làm song song
  • Ownership rõ ràng: AI implement, Human quyết định architecture và security policy
  • Viết test spec trước khi code — test spec chính là acceptance criteria được formalize

Tiếp theo

Đã có task list, test spec, và dependency graph — giờ là lúc thực thi. Bài tiếp theo đi vào workflow thực tế: feed context cho AI agent đúng cách, xử lý context rot khi AI bắt đầu “quên”, và khi nào nên dùng multi-agent để chạy song song.

→ Bài 05: Thực thi với AI Agent

← Bài 03: Design System & Atomic Components