green builds

The 10 critical-path tests that catch 80% of embarrassing bugs

A concrete starter set of ten end-to-end tests covering the paths most likely to break in public, auth, payments, the core AI flow, and the quiet killers.

You don’t need a thousand tests. You need the ten that cover the paths whose failure is visible, expensive, or both. Coverage percentage is a vanity metric at startup scale; what matters is that the flows a customer (or your board) would notice can’t break silently. Here’s how to pick them and a Playwright sketch for each.

How to pick a critical path

Score each candidate flow on two axes: how visible is the failure (does a customer hit it immediately?) and how costly (lost money, lost trust, lost data). Test the top-right quadrant first. Almost every product’s list rhymes with the ten below.

The ten

1. Sign-up / onboarding. The first thing a new user does. If it breaks, nobody becomes a customer.

test('@smoke new user can sign up and reach the app', async ({ page }) => {
  await page.goto('/signup');
  await page.getByLabel('Email').fill(uniqueEmail());
  await page.getByLabel('Password').fill('correct-horse-battery');
  await page.getByRole('button', { name: 'Create account' }).click();
  await expect(
    page.getByRole('heading', { name: /welcome|dashboard/i })
  ).toBeVisible();
});

2. Log in / log out. Auth is the gate to everything. Test both directions and a wrong password.

3. The core action, your product’s one verb. Whatever the user came to do (send the message, run the query, generate the thing). For an AI product this is the LLM flow:

test('@smoke core AI flow returns a grounded answer', async ({ page }) => {
  await page.goto('/app');
  await page.getByRole('textbox').fill('What was my last invoice?');
  await page.getByRole('button', { name: 'Ask' }).click();
  await expect(page.getByTestId('answer')).toContainText(/invoice/i);
  await expect(page.getByTestId('answer')).not.toContainText(
    /error|undefined/i
  );
});

4. Payment / checkout. If you take money, a broken checkout is a direct revenue leak. Use the provider’s test mode.

5. A data-loss path. Anything that deletes or overwrites user data. Test that it confirms, and that cancel actually cancels.

6. Permissions boundary. A free user can’t reach a paid feature; user A can’t see user B’s data. This is also a security test.

test('a free user cannot open a pro-only page', async ({ page }) => {
  await loginAs(page, 'free-user');
  await page.goto('/pro/reports');
  await expect(page.getByText(/upgrade to pro/i)).toBeVisible();
});

7. The empty state. New accounts with no data are where layouts explode. Demo data hides this; test the zero case.

8. The error path. Submit the form with bad input and assert a graceful message, not a stack trace, not a silent no-op.

9. Search / filter / the “find my thing” flow. If users can’t find their data, the product feels broken even when it isn’t.

10. The integration that matters. The one third-party your product leans on (the model API, the payment webhook). Mock it, then test the failure mode: what does the user see when it’s down?

Make them a blocking gate

Ten tests are worthless if they don’t stop a bad merge. Tag them @smoke, run them on every PR, and block on red, see the no-flake CI setup. Keep them stable: web-first assertions, isolated data, no fixed waits.

When to expand past ten

Add a test every time something embarrassing escapes, the incident becomes a permanent regression test, and your suite grows along the exact contour of your real failures. That’s how you get to real coverage without ever chasing a coverage number.


These ten are a weekend of work and they eliminate most of the bugs that make a startup look amateur. If you want them built for your stack, plus the AI-testing layer and CI gates around them, that’s the QA Foundation Sprint.

One bad regression away from a lost week.

Book a 20-minute intro call. I’ll tell you honestly whether I can help and what the right next step is: audit, sprint, or nothing yet.