Testing
Unit tests, real PostgreSQL bot flows, and browser tests of the GitHub Pages export.
Tests use bun:test with Sinon, configured in
apps/discord/bunfig.toml. Test files live in apps/discord/tests/, mirroring
src/, and use the .test.ts suffix.
Running tests
bun run test # bot unit suite (excludes @fluffboost/motion)
bun run test:coverage # with a coverage reportRun just the bot's suite from its package:
bun --filter @fluffboost/discord testbun run brand:verify separately checks the animated brand exports in
apps/motion; it needs ffmpeg and ffprobe on your PATH.
The unit-test scripts run with --isolate and preload tests/preload.ts to
mock application settings before imports. They do not require .env or depend on the order test files
run. Individual suites can override that fixture. The E2E script does not use
this preload and exercises the real environment loader.
Always use these scripts. A bare bun test at the repository root also picks
up the bot E2E suite and the Playwright specs, and skips the preload fixture.
To run one file:
cd apps/discord
NODE_ENV=test bun test --isolate --preload ./tests/preload.ts tests/utils/timezones.test.tsPatterns
- Prefer injected dependencies. Modules such as
sendMotivationCore.ts,setActivityCore.tsandpremiumReconciliationCore.tstake their database, logger and helpers as arguments, so tests pass stubs directly. mock.module()is per file. The scripts pass--isolate, so each test file gets a fresh module registry and the preload fixture; a mock never reaches another file and needs no restore hook. Inside one file it persists across tests and replaces the whole module, so when you mock a shared module spread its real exports and replace only what you need (tests/commands/permissionsMock.tsshows the pattern). A partial mock makes other imports fail withExport named ... not found. Without--isolate(a barebun test) mocks leak between files, which is one more reason to use the scripts.- Fresh copies. To evaluate a module again under different mocks in the
same file, import it with a query suffix, such as
import("../../src/events/commandRegistry.js?env=production"). - Time control — schedule-sensitive tests use
sinon.useFakeTimers()to pindayjs()to a fixed moment. - Shared fakes —
tests/helpers.tsprovides factories (mockLogger,mockDb,mockInteraction,mockClient,mockEnv, and friends) so each test stays short. - Routers — tests for
/admin,/ownerand/setupswap entries in the exported route Maps instead of mocking subcommand modules.
Unit tests isolate commands and jobs behind these fakes and never touch a real database, Redis, or the Discord API.
Bot end-to-end tests
apps/discord/e2e/ exercises the actual interaction router, command handlers,
Drizzle queries, schema migrations, embeds, premium policy, and delivery worker
with real PostgreSQL. Discord transport stays local: these tests never log
in to Discord, create entitlements, or post to a server. Redis and BullMQ job
dispatch are outside this suite; it invokes the production job handler directly.
The suite covers channel setup, administrator permissions, Premium activation and expiry, concurrent SQL delivery claims, retry after a transient send failure, and real Discord.js channel resolution when a guild is absent from a shard's cache. A separate scheduler regression checks the repeated autumn DST hour.
Start a disposable database in another terminal:
docker run --rm --name fluffboost-e2e-db \
-e POSTGRES_USER=ci -e POSTGRES_PASSWORD=ci -e POSTGRES_DB=fluffboost_e2e \
-p 127.0.0.1:55439:5432 postgres:18-alpineThen run:
E2E_DATABASE_URL=postgres://ci:ci@127.0.0.1:55439/fluffboost_e2e \
E2E_DATABASE_DISPOSABLE=true \
bun run --filter=@fluffboost/discord test:e2eEach run creates a unique schema, applies the repository migrations inside it,
and drops only that schema on completion. The suite requires the exact database
name fluffboost_e2e, a loopback host, and the explicit
E2E_DATABASE_DISPOSABLE=true operator assertion; it never falls back to
DATABASE_URL. The assertion means you have confirmed the target database is
disposable. Host and name checks cannot identify the server behind a local port
forward, so never point a tunnel at production. Use a disposable test database
and a role scoped to that database; stop the container afterward with
docker stop fluffboost-e2e-db.
Browser end-to-end tests
Playwright builds apps/docs with the deployed /fluffboost base path and
serves the static out/ directory with no route fallback. Desktop Chromium and
a Pixel 7 viewport sweep every page in the generated sitemap for broken local
links, missing assets, client errors, horizontal overflow, a single <main>, and
per-page canonical and og: metadata. They also cover static search (including
the retry after a failed index load and focus return), the branded 404 page, a
keyboard Tab walk (skip link first, focus always visible and never covered),
the mobile docs drawer, and an axe-core WCAG 2.2 AA scan of every page (with
reduced motion) and of the visible mobile invite bar. Mobile emulation does not replace testing on a real
phone.
bun run --filter=@fluffboost/docs test:e2e:install
bun run --filter=@fluffboost/docs test:e2ePreview the static export
bun run --filter=@fluffboost/docs preview builds with the /fluffboost base
path and serves it at http://127.0.0.1:4173/fluffboost/.
bun run --filter=@fluffboost/docs start serves an existing out/ directory;
set NEXT_PUBLIC_BASE_PATH to match the build it serves.
To run both suites, set E2E_DATABASE_URL as above and run bun run test:e2e.
Browser failure screenshots and traces are written to the ignored
apps/docs/test-results/ directory, with an HTML report in
apps/docs/playwright-report/.
In CI
GitHub Actions runs on every push and PR to main and dev:
| Job | What it checks |
|---|---|
| Test & Typecheck | bun run test:coverage, schema drift, ESLint, brand:verify, TypeScript |
| Security Audit | bun audit |
| Bot E2E (PostgreSQL) | The bot E2E suite against a fresh PostgreSQL 18 service |
| Docs Build & Browser E2E | Contrast check, then Playwright against the exact static export |
| Docker Build Test | Builds the image and runs its migrations twice (the second is a no-op) |
The coverage report and the browser report and failure traces are uploaded as build artifacts.