Kwisple
Browse the docs

The platform repository › Working on it

Testing and checks

Every check the platform repository has - typecheck, lint, the i18n and action guards, unit and database tests per package, end-to-end tests, the backup drill, Lighthouse, the docs content tests and the release dry run - and what CI runs.

The repository guards itself with a handful of commands. Most of them are fast, all of them run from the root, and CI runs the same ones on every push. This page lists them, says what each one catches, and how the tests are organised so you know where a new test goes.

At a glance

CommandWhat it checksIn CI
pnpm lintBiome format and lint (biome check .)yes
pnpm check:i18ncatalogues in sync, keys exist, no hardcoded Dutchyes
pnpm check:actionsevery workspace server action checks a permissionyes
pnpm typechecktsc in every workspace, tests includedyes
pnpm testunit and database tests in every workspaceyes (after pnpm db:migrate)
pnpm buildproduction buildsyes
pnpm e2ePlaywright against a running dashboardno, run locally
pnpm backup:drillbackup and restore with the production scripts, locallyno
pnpm lighthouse <url…>Lighthouse scores of running sitesno
pnpm release:drythe npm packages build and pack as they would publishno

pnpm typecheck

turbo run typecheck runs each workspace's typecheck script (tsc -p tsconfig.json, no emit). The tsconfigs include the tests ("include": ["src", "test"] in the packages, **/*.ts in the Next apps), so a test that no longer matches the code fails here even before it runs. The message catalogue types make a missing translation key a type error as well.

pnpm lint

biome check . with biome.json: formatting (two spaces, double quotes, semicolons, trailing commas, line width 100), organised imports and lint rules, among them the a11y rule noLabelWithoutControl as an error. pnpm lint:fix writes what can be fixed. CI fails on anything Biome would change.

pnpm check:i18n

node tools/check-i18n.mjs reads apps/dashboard/messages/*.json and the sources in apps/dashboard/src with the TypeScript parser. It fails on keys used through getT, useT or getTranslator that do not exist, on keys or {variables} that differ between nl.json and the other catalogues, on unbalanced braces, and on Dutch-looking copy left in JSX text, label-like props, toasts, returned errors or sentences. // i18n-ignore: <reason> on the line (or the line above) and // i18n-ignore-file are the escape hatches. Options: --dir to point it at another app, --json for machine-readable output. Details in Languages and design system.

pnpm check:actions

node tools/check-action-permissions.mjs reads every exported async function in apps/dashboard/src/lib/actions/*.ts. An action whose body calls requireWorkspace(, await setup(, await setupDeploy( or await setupWebhooks( must also contain a recognised permission check (can(ctx, "…"), canTrash(ctx, …), the site setup helpers with a key, ctx.isStaff / ctx.canManage / ctx.staffRole / ctx.agencyRole, or findWorkspaceApp(ctx, …)), or the script names it and exits 1. See Tenancy and permissions.

pnpm test

turbo run test runs the test script of every workspace that has one:

WorkspaceScriptNotes
@kwisple/corevitest runpure
@kwisple/cmsvitest runpure; includes the English labels of the standard schema
@kwisple/contentvitest runthe engine against Postgres (test/engine.test.ts, query.test.ts, …)
@kwisple/analyticsvitest runpure, agents, benchmark, and db.test.ts (rollup against Postgres)
@kwisple/brandvitest runpure
@kwisple/uivitest runlocale, png
@kwisple/sdkvitest runclient, CLI, Astro, apps, staging, SEO; fetch is injected
create-kwisple-sitevitest runscaffolding
@kwisple/agentvitest runthe agent service
@kwisple/docsvitest runthe developer documentation content
@kwisple/dashboardtsx --test test/*.test.ts && vitest runtwo runners, below

@kwisple/web, @kwisple/db, @kwisple/jobs, @kwisple/social and @kwisple/storage have no test script; their code is covered through the packages and the dashboard tests that use it. Run one workspace with a filter:

pnpm --filter @kwisple/content test
pnpm --filter @kwisple/dashboard test
pnpm --filter @kwisple/dashboard exec vitest run test/access     # one folder

The dashboard's two runners

apps/dashboard/package.json runs tsx --test test/*.test.ts first, then vitest run.

RunnerFiles
node:test through tsxthe top-level files test/*.test.ts: billing, dns-records, jobs-assets, oauth, pure, signature, webhooks
Vitest (apps/dashboard/vitest.config.ts)the folders test/access, agent, billing, brand, campaigns, cli, help, hosting, i18n, media, platform, preflight, requests, sites, social, studio, trash, website-review

The Vitest config resolves @/ to src/, uses the node environment, a 30 s test and 60 s hook timeout, and fileParallelism: false (database tests share one Postgres). A new folder under test/ must be added to include in vitest.config.ts, or it never runs. New tests go into a Vitest folder; the node:test files are the older ones.

Database-backed tests

Tests that need Postgres read the root .env themselves and use the real database with their own fixtures:

import { config } from "dotenv";
config({ path: new URL("../../../../.env", import.meta.url).pathname });

const { db, sql } = await import("@kwisple/db");
const reachable = await db.execute(sql`select 1`).then(() => true).catch(() => false);

const tag = shortId();                       // unique per run
const ids = { agency: newId("agency"), ws: newId("workspace"), owner: `usr_a_${tag}_o` };
const slug = `access-test-${tag}`;

beforeAll(async () => { if (!reachable) return; /* insert agency, workspace, users … */ });
afterAll(async () => { if (!reachable) return; /* delete workspace, agency, users */ });

describe.skipIf(!reachable)("access policy resolution", () => { /* … */ });

The rules they follow:

  • Skip when Postgres is unreachable. The dashboard's database tests probe with select 1 and wrap their suites in describe.skipIf(!reachable), so pnpm test on a machine without pnpm infra:up still passes the pure tests. The @kwisple/content engine tests have no such skip: run them with the database up.
  • Own fixtures, tagged. Every run creates its own agency, workspace and users with ids and slugs that contain a shortId() tag, so tests never touch seed or real data and two runs never collide.
  • Clean up in afterAll. Delete the workspace (its tenant rows go with it through on delete cascade), then the agency and the users.
  • Dynamic imports after dotenv. Modules that read DATABASE_URL at import are imported with await import(...) after the .env is loaded.

CI starts a Postgres 17 service, runs pnpm db:migrate, then pnpm test, so database tests run there for real.

pnpm e2e

Playwright tests of the core flows in e2e/:

pnpm build && pnpm preview     # or pnpm dev, in another terminal
pnpm e2e

pnpm e2e type-checks e2e/ (tsc -p e2e/tsconfig.json) and runs playwright test -c e2e/playwright.config.ts:

  • Target: http://localhost:4100, or E2E_BASE_URL. Global setup refuses to start when no dashboard answers on /login.
  • Fixtures: global setup runs apps/dashboard/scripts/e2e-fixtures.ts setup, which creates a throw-away agency with an owner, a client with a custom-code website and a client member, and writes them to e2e/.fixtures.json. Teardown removes all of it. The script refuses to run with NODE_ENV=production.
  • E2E_KEEP=1 skips the teardown, so you can sign in as the fixture accounts and look around after a failure.
  • Projects: desktop (Desktop Chrome, everything except phone) and phone (iPhone 13 viewport, phone.spec.ts), one worker, no retries, nl-BE and Europe/Brussels, trace and screenshot on failure in e2e/.results.
  • Specs (e2e/tests/): auth, campaign, help, phone, requests, social, solo, website. Selectors use the message catalogue (msg("auth.form.email") in e2e/fixtures.ts), so a copy change does not break them.

Run it before a release and after changes to login, navigation or a core flow.

pnpm backup:drill

tools/backup-drill.sh rehearses the production backup locally with the same image and scripts as production (infra/backup). It needs pnpm infra:up, reads .env without printing it, and:

  1. builds the backup image;
  2. creates a scratch database restore_check and a throw-away bucket on the local RustFS;
  3. runs backup.sh (database dump and file copy) into that bucket;
  4. runs restore-check.sh, which restores the newest dump into restore_check;
  5. compares agency, workspace, user, entry and asset counts and the file count between live and restored;
  6. removes the bucket, the scratch database and the image again.

It exits 1 when the numbers differ. Run it after changes to infra/backup or big schema changes.

pnpm lighthouse

tools/lighthouse/run.mjs audits running production servers (it builds nothing):

pnpm lighthouse http://<slug>.sites.localhost:4101/ http://localhost:4102/
pnpm lighthouse --preset mobile --runs 5 --min perf=90 <url…>
pnpm lighthouse --config tools/lighthouse/urls.example.json

Each URL is audited --runs times (default 3) per preset (mobile and desktop by default); the median run by performance is kept and fails below the thresholds (defaults perf 95, a11y 100, best practices 100, SEO 100). Lighthouse itself is pinned in the script. --out sets the report folder, --chrome the browser binary.

Content tests: the handleiding and these docs

Both documentation sets are tested like code:

TestRuns inFails when
apps/dashboard/test/help/manual.test.tspnpm --filter @kwisple/dashboard testa Hulp page is missing from docs/handleiding/README.md, or a link between pages points at a page that does not exist
apps/docs/test/content.test.tspnpm --filter @kwisple/docs testa developer docs page lacks title, description, section, order or updated, its H1 differs from its title, it is not in docs/developers/README.md exactly once, a link points at a page or heading that does not exist, or it still contains a "to verify" marker left by a draft

pnpm release:dry and pnpm release:pack

tools/release-dry.mjs handles the three packages meant for npm: @kwisple/cms, @kwisple/sdk and create-kwisple-site.

  • pnpm release:dry builds each package, stages it the way pnpm publish would (with publishConfig applied, workspace:* replaced by real versions, devDependencies dropped) and runs npm pack --dry-run. Nothing is published. It fails when a package is private.
  • pnpm release:pack <dir> writes the real tarballs to <dir> instead. A site repository installs them from its own vendor/ folder until the packages are on npm, so repack after an SDK or CMS change that a site needs.

CI

.github/workflows/ci.yml runs on every push to main and on pull requests, on Ubuntu with Node 22 and pnpm, with a Postgres 17 service and placeholder environment values:

pnpm install --frozen-lockfile
pnpm lint
pnpm check:i18n
pnpm check:actions
pnpm typecheck
pnpm db:migrate
pnpm test
pnpm build

Storage is not running in CI, so tests must not depend on a reachable bucket.

Before you commit

Run what CI runs, fastest first:

pnpm lint
pnpm check:i18n
pnpm check:actions
pnpm typecheck
pnpm test          # with pnpm infra:up, so database tests run instead of skipping

Add, depending on what you touched:

You changedAlso run
a schema file in packages/dbpnpm db:generate (commit the migration), pnpm db:migrate
login, navigation or a core flowpnpm build && pnpm preview, then pnpm e2e
a screen, button or flowupdate docs/handleiding; the manual test checks the index and links
docs/developerspnpm --filter @kwisple/docs test
packages/sdk or packages/cmspnpm release:dry; pnpm release:pack <dir> for vendoring sites
infra/backuppnpm backup:drill
a hosted block or site renderingpnpm lighthouse on a production build

Last updated 2026-10-30