Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ that app's `AGENTS.md`.
- Use Vitest for unit and integration tests; name test files `*.test.ts[x]` and place under or near source code or in `__tests__`.
- Reset databases before any suite that mutates data (`pnpm reset-test-db` or `pnpm -C packages/db reset-test-db`).
- Prefer fixtures in `apps/map/tests` or `packages/*/__mocks__` instead of live service calls.
- How coverage is measured and why thresholds are set the way they are (Vitest 4's whole-`src` denominator, shared `coverageInclude`/`coverageExclude`): [`docs/testing.md`](docs/testing.md).

### Driving auth-bounded flows in local dev

Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"prettier": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-tsconfig-paths": "catalog:",
"vitest": "catalog:"
},
Expand Down
15 changes: 6 additions & 9 deletions apps/api/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { coverageExclude } from "@acme/vitest-config";
import { coverageExclude, coverageInclude } from "@acme/vitest-config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vitest/config";
Expand All @@ -14,17 +14,14 @@ export default defineConfig({
provider: "v8",
reporter: ["text", "json", "html"],
reportsDirectory: "./coverage",
// Exclude bootstrap/config files that aren't unit-testable (Sentry init,
// Next config, instrumentation, styling config). They otherwise sit in the
// denominator at 0% and make every edit to them break the global
// thresholds. Shared list keeps vitest's defaults plus the bootstrap globs.
include: coverageInclude,
exclude: coverageExclude,
thresholds: {
autoUpdate: true,
statements: 65.41,
branches: 88.88,
functions: 50,
lines: 65.41,
statements: 90.32,
branches: 90.9,
functions: 77.77,
lines: 90.32,
},
},
exclude: [
Expand Down
1 change: 1 addition & 0 deletions apps/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"tailwindcss": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vitest": "catalog:"
},
"prettier": "@acme/prettier-config"
Expand Down
6 changes: 3 additions & 3 deletions apps/auth/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ export default defineConfig({
include: ["src/lib/phone.ts"],
thresholds: {
autoUpdate: true,
statements: 86.95,
branches: 72.72,
statements: 91.66,
branches: 80,
functions: 100,
lines: 86.95,
lines: 90.9,
},
},
},
Expand Down
1 change: 1 addition & 0 deletions apps/map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"prettier": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-tsconfig-paths": "catalog:",
"vitest": "catalog:",
"vitest-canvas-mock": "catalog:"
Expand Down
9 changes: 4 additions & 5 deletions apps/map/vitest.config.ts
Comment thread
BigGillyStyle marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { coverageExclude } from "@acme/vitest-config";
import { coverageExclude, coverageInclude } from "@acme/vitest-config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vitest/config";
Expand All @@ -14,14 +14,13 @@ export default defineConfig({
provider: "v8",
reporter: ["text", "json", "html"],
reportsDirectory: "./coverage",
// Exclude non-testable bootstrap/config files (defensive; map uses static
// thresholds, so this only keeps the denominator consistent with other apps).
include: coverageInclude,
exclude: coverageExclude,
thresholds: {
autoUpdate: false,
statements: 1.8,
branches: 27.23,
functions: 17.15,
branches: 4,
functions: 7,
lines: 1.8,
},
},
Expand Down
1 change: 1 addition & 0 deletions apps/me/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"tailwindcss": "catalog:",
"tailwindcss-animate": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vitest": "catalog:"
}
}
13 changes: 6 additions & 7 deletions apps/me/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { coverageExclude } from "@acme/vitest-config";
import { coverageExclude, coverageInclude } from "@acme/vitest-config";
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
Expand All @@ -13,15 +13,14 @@ export default defineConfig({
provider: "v8",
reporter: ["text", "json", "html"],
reportsDirectory: "./coverage",
// Exclude non-testable bootstrap/config files so they don't sit in the
// coverage denominator at 0% and break the autoUpdate thresholds on edit.
include: coverageInclude,
exclude: coverageExclude,
thresholds: {
autoUpdate: true,
statements: 26.08,
branches: 82.89,
functions: 48.61,
lines: 26.08,
statements: 30.76,
branches: 33.9,
functions: 17.14,
lines: 31.69,
},
},
setupFiles: ["./vitest.setup.ts"],
Expand Down
69 changes: 69 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Testing & Coverage

How unit/integration tests and coverage are set up across the monorepo. The
per-app `vitest.config.ts` files are intentionally declarative β€” the rationale
behind the coverage settings lives here so it stays in one place instead of
drifting across config comments.

## Running tests

- All packages: `pnpm test`
- A single app/package: `pnpm -C apps/map test` (append `--run` for a one-shot,
non-watch run)
- CI runs these under the `test-coverage` check (one of the five required checks
enforced by the `dev` branch ruleset β€” see [AGENTS.md](../AGENTS.md)).

Use Vitest for unit and integration tests. Name test files `*.test.ts[x]` and
place them next to the source or under `__tests__`. Reset databases before any
suite that mutates data (`pnpm reset-test-db`).

## Coverage measurement (Vitest 4)

Vitest 4's v8 coverage provider removed the `coverage.all` option and, by
default, only measures files that a test actually imported. Left unset, an
untested file disappears from the denominator entirely, so coverage silently
answers "how much of what we imported is tested" instead of "how much of the app
is tested."

To preserve the v3 behaviour β€” untested files stay in the denominator β€” every
config sets `coverage.include` explicitly to the whole `src` tree
(`src/**/*.{ts,tsx}`). Apps that use the shared tooling import this rather than
hardcoding the glob:

```ts
import { coverageExclude, coverageInclude } from "@acme/vitest-config";
```

- **`coverageInclude`** β€” the whole-`src` glob (keeps untested files counted).
- **`coverageExclude`** β€” Vitest's built-in excludes plus non-testable
bootstrap/config files (Sentry init, `next.config.*`, `instrumentation*`,
Tailwind/PostCSS config, `middleware.*`). Those files would otherwise sit in
the denominator at 0% and break thresholds on every edit.

Both constants, and the exact glob lists, are defined and documented in
[`tooling/vitest/coverage.ts`](../tooling/vitest/coverage.ts) (the
`@acme/vitest-config` package). That file is the single source of truth β€” update
the globs there, not in individual app configs.

## Thresholds

Coverage thresholds live in each app's `vitest.config.ts` under
`test.coverage.thresholds`. Two modes are in use:

- **`autoUpdate: true`** (e.g. `apps/api`, `apps/me`) β€” Vitest ratchets the
thresholds up automatically as coverage improves, guarding against
regressions without manual bookkeeping.
- **Static floors** (`apps/map`) β€” fixed minimums that only change by hand.

Note that Vitest 4's AST-aware v8 remapping counts branches and functions more
granularly than v3, so whole-`src` branch/function coverage measures lower than
it did before the upgrade. Static floors were lowered accordingly to sit just
under the v4 baseline while still catching regressions. The numbers in each
config are the source of truth β€” this doc deliberately doesn't repeat them.

## Driving auth-bounded flows

Tests and QA flows that require sign-in go through `apps/auth`'s email-based MFA
against a local mail backend (no real inbox). See the
[Testing Guidelines in AGENTS.md](../AGENTS.md) and
[`docs/QA_LOCAL_AUTH.md`](QA_LOCAL_AUTH.md).
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-tsconfig-paths": "catalog:",
"vitest": "catalog:"
},
Expand Down
18 changes: 10 additions & 8 deletions packages/api/src/lib/cascade-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
import { vi } from "vitest";

vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: vi.fn().mockResolvedValue({
success: true,
limit: 10,
remaining: 9,
reset: Date.now() + 60000,
}),
})),
MemoryRatelimiter: vi.fn(function () {
return {
limit: vi.fn().mockResolvedValue({
success: true,
limit: 10,
remaining: 9,
reset: Date.now() + 60000,
}),
};
}),
}));

import { and, eq, gte, schema } from "@acme/db";
Expand Down
18 changes: 10 additions & 8 deletions packages/api/src/lib/first-event-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
import { vi } from "vitest";

vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: vi.fn().mockResolvedValue({
success: true,
limit: 10,
remaining: 9,
reset: Date.now() + 60000,
}),
})),
MemoryRatelimiter: vi.fn(function () {
return {
limit: vi.fn().mockResolvedValue({
success: true,
limit: 10,
remaining: 9,
reset: Date.now() + 60000,
}),
};
}),
}));

vi.mock("@acme/mail", async (importOriginal) => {
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/lib/webhook-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { vi } from "vitest";
// Mock the rate limiter before any imports
const mockLimit = vi.hoisted(() => vi.fn());
vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: mockLimit,
})),
MemoryRatelimiter: vi.fn(function () {
return { limit: mockLimit };
}),
}));

// Mock notifyWebhooks to capture webhook calls
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/router/api-key.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { vi } from "vitest";
const mockLimit = vi.hoisted(() => vi.fn());

vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: mockLimit,
})),
MemoryRatelimiter: vi.fn(function () {
return { limit: mockLimit };
}),
}));

import { eq, schema } from "@acme/db";
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/router/attendance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { vi } from "vitest";
const mockLimit = vi.hoisted(() => vi.fn());

vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: mockLimit,
})),
MemoryRatelimiter: vi.fn(function () {
return { limit: mockLimit };
}),
}));

import { and, eq, schema } from "@acme/db";
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/router/event-instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { vi } from "vitest";
const mockLimit = vi.hoisted(() => vi.fn());

vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: mockLimit,
})),
MemoryRatelimiter: vi.fn(function () {
return { limit: mockLimit };
}),
}));

import { eq, schema } from "@acme/db";
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/router/event-tag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { vi } from "vitest";
const mockLimit = vi.hoisted(() => vi.fn());

vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: mockLimit,
})),
MemoryRatelimiter: vi.fn(function () {
return { limit: mockLimit };
}),
}));

import { eq, schema } from "@acme/db";
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/router/event-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { vi } from "vitest";
const mockLimit = vi.hoisted(() => vi.fn());

vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: mockLimit,
})),
MemoryRatelimiter: vi.fn(function () {
return { limit: mockLimit };
}),
}));

import { eq, schema } from "@acme/db";
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/router/event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { vi } from "vitest";
const mockLimit = vi.hoisted(() => vi.fn());

vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: mockLimit,
})),
MemoryRatelimiter: vi.fn(function () {
return { limit: mockLimit };
}),
}));

import { eq, schema } from "@acme/db";
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/router/location.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { vi } from "vitest";
const mockLimit = vi.hoisted(() => vi.fn());

vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: mockLimit,
})),
MemoryRatelimiter: vi.fn(function () {
return { limit: mockLimit };
}),
}));

import { eq, schema } from "@acme/db";
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/router/me/me.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import { vi } from "vitest";
const mockLimit = vi.hoisted(() => vi.fn());

vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: mockLimit,
})),
MemoryRatelimiter: vi.fn(function () {
return { limit: mockLimit };
}),
}));

import type { Session } from "@acme/auth";
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/router/org-chart/org-chart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { vi } from "vitest";
const mockLimit = vi.hoisted(() => vi.fn());

vi.mock("@orpc/experimental-ratelimit/memory", () => ({
MemoryRatelimiter: vi.fn().mockImplementation(() => ({
limit: mockLimit,
})),
MemoryRatelimiter: vi.fn(function () {
return { limit: mockLimit };
}),
}));

import { schema } from "@acme/db";
Expand Down
Loading