---
title: "Tests, Tests, Tests: TSC, Linting, Vitest, Playwright and CI/CD"
description: "Protect your project with type checks, linting, unit and end-to-end tests, wired into CI/CD"
type: "lesson"
locale: "en"
course: "Quality, Security and the Agent-First Business"
number: "5.1"
canonical: "https://agenticschool.dev/courses/quality-security-agent-first/tests-tests-tests-tsc-linting-vitest-playwright-and-ci-cd"
datePublished: "2026-06-12"
dateModified: "2026-06-12"
---

# Tests, Tests, Tests: TSC, Linting, Vitest, Playwright and CI/CD

- Course: Quality, Security and the Agent-First Business
- Lesson: 5.1
- Duration: 28 min
- Level: technik
- Status: published
- Canonical URL: https://agenticschool.dev/courses/quality-security-agent-first/tests-tests-tests-tsc-linting-vitest-playwright-and-ci-cd
- Locale: en

> Protect your project with type checks, linting, unit and end-to-end tests, wired into CI/CD

## Summary

Agents move fast, which makes a safety net essential. This lesson covers the full quality stack: TypeScript type checking, linting, Vitest for unit tests, Playwright for end-to-end tests, a pre-push hook and a GitHub Actions pipeline, so regressions are caught before they ship instead of after a customer finds them.

## What you learn

- Why agents make tests more important, not less, and the four-layer quality gate
- Writing Vitest unit tests and Playwright end-to-end tests that survive refactors
- Running the whole suite automatically with a pre-push hook and GitHub Actions

## Summary

There is a myth that AI writes the code so you no longer need tests. The opposite is true. An agent will happily rewrite a working function at 2am because you asked for an unrelated change, and it has no memory of why the old behaviour mattered. Tests are how you tell future agents what must not break. This lesson builds the four-layer quality gate - types, lint, unit, end-to-end - and wires it into a pre-push hook and a CI pipeline so quality is enforced by the machine, not by your discipline.

## What you will learn

You will learn what each layer of the quality stack catches, how to write a Vitest unit test and a Playwright end-to-end test that keep working after a refactor, how to block a bad push with a Git hook, and how to run the whole suite automatically on every push with GitHub Actions. By the end you can hand an agent free rein on a repo and trust the gate to catch its mistakes.

## Prerequisites

A working TypeScript project from earlier courses and the hooks lesson from Course 2, since CI/CD is just the team-wide version of a pre-push gate. You do not need to be a test expert. The point of this lesson is to make testing a habit your agent does for you, not a discipline you have to remember.

## The problem

When you drive an agent hard, it touches a lot of code fast. A change to one file silently breaks another. The agent reports success because the file it edited looks right, and you only discover the regression when a page is blank in production. Manual testing does not scale to agent speed - you cannot click through every flow after every change. Without an automated gate, every agent session is a small gamble with your live product. The fix is to make "is it still working?" a question the machine answers in seconds.

## The four-layer quality gate

Each layer catches a different class of bug, and they get slower and more thorough as you go down. Run the fast ones constantly and the slow ones before you ship. Together they form a net dense enough that fast, agent-driven changes stay safe.

- Type check (tsc --noEmit): catches shape errors - a function called with the wrong arguments, a property that does not exist, a null you forgot to handle. Instant and free.
- Lint (ESLint): catches bug-prone patterns and style drift - unused variables, missing awaits, accidental console.logs. Keeps agent-written code consistent with yours.
- Unit tests (Vitest): check that your logic is actually correct - a price calculation, a validation rule, a date formatter. Fast, run on every save.
- End-to-end tests (Playwright): drive a real browser through whole user journeys - sign up, add to cart, check out. Slow but they prove the real thing works.

## Writing a Vitest unit test

Unit tests are small and fast. You give a function an input and assert the output. The trick that makes them survive agent refactors: test behaviour, not implementation. Assert what the function should produce for a user, never the private steps it takes to get there. Then the agent can rewrite the internals freely and the test still guards the contract.

```typescript
import { describe, it, expect } from 'vitest'
import { yearlyPricePerMonth } from './pricing'

describe('yearlyPricePerMonth', () => {
  it('shows the discounted monthly figure for a yearly plan', () => {
    // Monthly is 17% above yearly, so yearly-per-month is monthly / 1.17.
    expect(yearlyPricePerMonth({ monthly: 117 })).toBe(100)
  })

  it('never returns a negative price', () => {
    expect(yearlyPricePerMonth({ monthly: 0 })).toBeGreaterThanOrEqual(0)
  })
})
```
A Vitest test that pins behaviour, not implementation - run it with bun run test

When you ask an agent to add a feature, add a line to your spec: "write a Vitest test for it". Now the agent leaves behind a tripwire that protects the feature from the next agent, including a future version of itself.

## Writing a Playwright end-to-end test

Playwright launches a real browser, visits your app and clicks through it like a user. It catches the bugs unit tests cannot: a broken route, a button that does nothing, a form that never submits. Write end-to-end tests for your money paths first - the handful of flows where a break costs you a customer. Select elements by what the user sees or by stable test ids, never by brittle CSS, so a redesign does not break every test.

```typescript
import { test, expect } from '@playwright/test'

test('a visitor can reach the pricing page from the hero CTA', async ({
  page,
}) => {
  await page.goto('/')
  await page.getByRole('link', { name: 'See pricing' }).click()
  await expect(page).toHaveURL(/\/pricing/)
  await expect(
    page.getByRole('heading', { name: 'Pricing' }),
  ).toBeVisible()
})
```
A Playwright test for a real user journey - run it with bun run test:e2e

## The pre-push hook: your local gate

The first place to enforce the gate is your own machine, the moment before code leaves it. A Git pre-push hook runs your checks and refuses the push if any fail, so broken code never reaches the repo. This is the same idea you met in Course 2, now pointed at the full suite. Keep the local hook fast - types, lint and unit tests - and let the slower end-to-end suite run in CI.

```bash
#!/usr/bin/env sh
# .husky/pre-push - blocks the push if any check fails

bun run typecheck || exit 1
bun run lint || exit 1
bun run test || exit 1

echo "All checks passed - pushing."
```
A pre-push hook that runs the fast layers before any code leaves your machine

A hook you can skip with --no-verify is a suggestion, not a gate. The hook is your fast feedback; CI is the gate that nobody can bypass. You want both.

## GitHub Actions: the gate nobody can skip

Continuous integration runs your whole suite on GitHub's servers on every push and pull request, regardless of what anyone ran locally. A workflow file in .github/workflows tells GitHub what to do. The example below installs dependencies, then runs all four layers including the end-to-end tests. Set your branch to require this check to pass before anything merges, and a regression simply cannot reach your main branch.

```yaml
name: Quality gate

on:
  push:
    branches: [main]
  pull_request:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install --frozen-lockfile
      - run: bun run typecheck
      - run: bun run lint
      - run: bun run test
      - run: bunx playwright install --with-deps
      - run: bun run test:e2e
```
.github/workflows/quality.yml - the full gate, run automatically on every push

## Typical mistakes

The big ones: writing zero tests because "the agent already checked it" (it cannot see your live app); testing implementation details so every refactor turns the suite red and you start ignoring it; making the local hook so slow you bypass it with --no-verify; and having CI but not requiring it to pass before merge, so it becomes decorative. Test behaviour, keep the local gate fast, and make CI mandatory.

## Business ROI

A regression that reaches a customer costs you trust, a support ticket and an emergency fix, often at the worst possible time. A test that catches it costs the few seconds it takes to run. Once the gate exists you can let agents work far more aggressively, because the cost of a mistake drops from "broke production" to "the pipeline went red, fix it before merge". That is the real unlock: tests are not overhead on agentic development, they are what makes aggressive agentic development safe.

## Checklist

You are ready to move on when every one of these is true for a real project, not just in theory.

- You can explain what each of the four layers catches that the others miss.
- At least one Vitest test and one Playwright test pass in your project.
- A pre-push hook runs the fast layers and blocks a failing push.
- A GitHub Actions workflow runs the full suite and is required before merge.

## Resources

Bookmark the official Vitest and Playwright docs for matchers and selectors, and the GitHub Actions docs for workflow syntax, since these evolve. Add "write a test for this" to your standard spec-sheet constraints so every agent task leaves a tripwire behind. The hooks lesson in Course 2 is worth a reread now that you are wiring the full suite in.

## Your task

Add one Vitest test and one Playwright test to a real project, then add the pre-push hook and the GitHub Actions workflow above. Deliberately break something an agent might break - rename a route, change a function's output - and confirm the gate goes red. That red is the whole point: it caught the mistake before a user did.

## Next lesson

Quality covered, the next lesson hardens security: rate limits on every public endpoint, CSP headers, encrypting stored credentials, and the pre-public audit that stops a secret leaking when you flip a repo public.

## Transcript

This lesson is a written, text-first guide. Agents move fast, which makes a safety net essential. This lesson covers the full quality stack: TypeScript type checking, linting, Vitest for unit tests, Playwright for end-to-end tests, a pre-push hook and a GitHub Actions pipeline, so regressions are caught before they ship instead of after a customer finds them. You will protect your project with type checks, linting, unit and end-to-end tests, wired into ci/cd. Work through the sections in order, try the task at the end in a real project, and move on once it works for you. There is no video required - everything you need is in the written steps above.
