04. Task Planning & Test Strategy — Vibe Coding Series

Break features into right-sized tasks for AI agents and define your test strategy upfront — before you end up with 800 lines of untestable code.

I once gave AI a single task: “Build the entire auth module for TaskFlow.” Two hours later, AI returned 800 lines of code — JWT, bcrypt, middleware, refresh tokens, session management, all in one file. The code ran. But when I wanted to test individual pieces, I didn’t know where to start. When I wanted to fix the refresh token logic, I had to read through 600 unrelated lines. When I wanted to add OAuth later, I had to refactor almost everything.

The problem wasn’t that AI wrote bad code. The problem was that I gave it the wrong task.


Context

In article 03, I built a design system for TaskFlow — tokens, 5 atom components, and an updated AI_CONTEXT.md to give AI a consistent design language. This article moves into Phase 3: before AI writes a single line of code, we need to know exactly what AI will do — and where it stops.


Getting task granularity right

Tasks that are too large

When a task is too large, AI has to make dozens of design decisions without your input. The result reflects AI’s “best guess,” not your intent. And because everything is in one big block, you have no checkpoints to review each of those decisions.

Signs a task is too large: the description uses words like “entire,” “module,” “system,” or lists more than 3 functions.

Tasks that are too small

On the other end, tasks that are too small create unnecessary overhead. “Write a function that checks if an email contains @” is too small — you’ll spend more time writing the prompt than AI spends writing the code.

Signs a task is too small: it’s only 1–2 lines of code, has no real logic, or is a sub-step of a simple operation.

The sweet spot

1 task = 1 function or 1 component with clear input and output.

Good examples:

  • “Write a hashPassword(plaintext: string): Promise<string> function using bcrypt with salt rounds = 12”
  • “Write a verifyToken(token: string): JWTPayload | null function — return null if the token is invalid or expired”
  • “Write a LoginForm component that accepts an onSubmit(email, password) prop, with validation, loading state, and error message”

Each of these has clear input, clear output, and can be tested independently.


Task breakdown with AI

From feature to task list

Instead of breaking things down myself, I use AI as a tool to generate the task list — but I’m the one who reviews and makes the final call.

The prompt I use:

Prompt:
"Feature: User authentication for TaskFlow (email/password).
Finalized requirements:
- Register with email + password
- Login, receive JWT access token (15 min) + refresh token (7 days)
- Refresh token endpoint
- Logout (invalidate refresh token)
- Auth middleware for protected routes

Break this into a task list with:
- Each task: name, short description, acceptance criteria (2–3 points)
- Mark dependencies (which tasks must come first)
- Mark owner: AI or Human"

AI returns a task list. I review it, sharpen the acceptance criteria, and here’s the final result:

## Feature: User Authentication

### Task 1: Password hashing utilities
Owner: AI
Depends on: —
Acceptance criteria:
- hashPassword(plaintext) → bcrypt hash with salt 12
- verifyPassword(plaintext, hash) → boolean
- Unit tests cover happy path and wrong password

### Task 2: JWT utilities
Owner: AI
Depends on: —
Acceptance criteria:
- generateAccessToken(userId) → JWT, expires in 15 min
- generateRefreshToken(userId) → JWT, expires in 7 days
- verifyToken(token) → payload or null (no 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 accepts { email, password }
- Validate email format, password min 8 chars
- Return 201 + { userId } or 409 if email already exists

### Task 6: Login endpoint
Owner: AI
Depends on: Task 1, Task 2, Task 3, Task 4
Acceptance criteria:
- POST /auth/login accepts { email, password }
- Return 200 + { accessToken, refreshToken } or 401
- Save refresh token to store

### Task 7: Refresh token endpoint
Owner: AI
Depends on: Task 2, Task 4
Acceptance criteria:
- POST /auth/refresh accepts { refreshToken }
- Return 200 + { accessToken } or 401 if token invalid/expired
- No refresh token rotation (simplified for MVP)

### Task 8: Auth middleware
Owner: AI
Depends on: Task 2
Acceptance criteria:
- Middleware reads Authorization header (Bearer token)
- Attaches decoded payload to req.user
- Returns 401 if token is missing or invalid

Dependency graph

Looking at the task list above, the dependency graph is clear:

Auth task dependency graph — Tasks 1, 2, 3 are parallel; downstream tasks depend on them

Tasks 1, 2, and 3 have no dependencies — they can be done in parallel. Task 4 needs Task 3. Task 5 needs Tasks 1 and 3. And so on.

Ownership: what AI does, what humans do

All 8 tasks above are assigned to AI — but there are types of tasks that should stay with humans:

  • Security-critical decisions: choosing the hashing algorithm, token expiry policy, rate limiting strategy
  • Architecture decisions: database schema structure, module organization
  • External service integration: OAuth providers, email services, payment gateways
  • Complex business logic: pricing rules, permission matrices, workflow state machines

For TaskFlow, I decided to use bcrypt (not argon2), JWT (not sessions), and a database-backed refresh token store (not Redis) — then handed AI the implementation based on those decisions.


Test strategy before writing code

TDD with AI

Instead of letting AI write code and then writing tests, I write the test spec first — then hand it to AI to implement until the tests pass.

The benefit: the test spec is the acceptance criteria, formalized. AI has a clear target to implement against. And I know exactly when a task is “done.”

Property-based testing for business logic

For business logic like password hashing or JWT, regular unit tests only cover a few specific cases. Property-based testing lets you state properties of a function and test them against hundreds of random inputs.

Example: “For any password, verifyPassword(password, hashPassword(password)) must always return true.” That’s a property — not a specific test case.

E2E tests for the critical path

For the auth feature, the critical path is: register → login → use access token → refresh → logout. I write 1 E2E test covering this entire flow — no mocks, testing directly against the API.


Real-world example: Test specs for the first 3 tasks

Here are the test specs I wrote before handing Tasks 1, 2, and 8 to AI:

// 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 () => {
      vi.useFakeTimers();
      const token = generateAccessToken("user-123");
      vi.advanceTimersByTime(16 * 60 * 1000); // 16 minutes
      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();
  });
});

With this test spec, I hand AI: “Implement hashPassword, verifyPassword, generateAccessToken, verifyToken, and authMiddleware so all the tests above pass.” AI has a clear target — no guesswork.


Pitfalls

Tasks without clear acceptance criteria. “Write an auth module” is not acceptance criteria. “POST /auth/login returns 200 + accessToken when email/password is correct, 401 when wrong” is. Without clear criteria, AI will decide when it’s “done” on its own terms — and that rarely matches your expectations.

Skipping test strategy until the end. Writing tests after the code is done is a classic anti-pattern — you’ll unconsciously write tests that fit the existing code rather than testing actual behavior. With AI, it’s even worse: AI will write tests that pass 100% because it knows exactly what its own code does. Those tests have no real value.


Summary

  • Sweet spot: 1 task = 1 function/component with clear input and output
  • Use AI to generate the task list, but you review and own the acceptance criteria
  • A dependency graph helps determine order and opportunities for parallel work
  • Clear ownership: AI implements, humans decide architecture and security policy
  • Write test specs before code — the test spec is the acceptance criteria, formalized

What’s Next

You’ve got a task list, test specs, and a dependency graph — now it’s time to execute. The next article covers the actual workflow: feeding context to AI agents correctly, handling context rot when AI starts to “forget,” and when to use multi-agent setups to run tasks in parallel.

→ Article 05: Execution with AI Agents

← Article 03: Design System & Atomic Components