Appearance
Testing
Run everything from the repo root unless a step says otherwise.
The gate
bash
npm run verify # backend tests and PHPStan, lint, web tests, type checks, web buildBefore calling a change done, also run the checks for the part you touched.
Backend
| Command | Does |
|---|---|
npm run backend:test | composer test: clears config, then php artisan test (Pest runs the PHPUnit-style tests too) |
cd apps/backend-laravel && php artisan test --filter=Name | One test class or method |
cd apps/backend-laravel && composer test:pest | Pest directly |
npm run backend:analyse | PHPStan with Larastan at level 5 over app, routes and database/seeders |
Tests live in apps/backend-laravel/tests: Feature (about 90 files, one per module) and Unit. Pest.php applies the shared TestCase to the Feature folder.
phpunit.xml sets the environment: SQLite in memory, array cache and sessions, QUEUE_CONNECTION=sync, MAIL_MAILER=array, broadcasting off and EXTENSIBILITY_RETHROW=true. Rethrow means an exception inside a plugin listener fails the test instead of being logged and skipped. So tests run on SQLite while development uses Postgres and the demo uses MySQL. Write portable SQL, and give composite indexes and foreign keys explicit short names (under 64 characters) so MySQL accepts them.
What a good module test covers, following the reference module (git show 40a3a41 --stat):
- The happy path for each endpoint.
- Permission denied (403) for a user without the key.
- Tenant isolation: a record in another organization is a 404, and a foreign id in the body fails validation.
- Validation errors and edge cases.
- Workflow rules, such as an invalid status move or an edit to a locked record.
- Idempotency on a retry, and audit records on mutations.
AuthorizationAndValidationTestchecks that every route has permission coverage and thatTenantResourcePolicycovers every tenant controller, so a new route without a permission fails it.
Demo seeders are skipped under tests (app()->runningUnitTests()). Prove seeders separately:
bash
cd apps/backend-laravel
DB_CONNECTION=sqlite DB_DATABASE=/path/to/scratch.sqlite php artisan migrate:fresh --seedPostgres and MySQL
The default run is SQLite, which hides engine differences: LIKE case, uuid columns that reject non-uuid text, boolean columns, partial indexes, and a caught unique violation aborting a whole Postgres transaction. Run the suite on Postgres before a release or after touching queries or migrations:
bash
docker compose -f infra/local-dev/docker-compose.yml up -d postgres
docker compose -f infra/local-dev/docker-compose.yml exec postgres createdb -U rangefront rangefront_waste_test
cd apps/backend-laravel && composer test:pgsqlcomposer test:pgsql sets DB_CONNECTION=pgsql and the compose credentials through the environment, so phpunit.xml and the SQLite default stay as they are. It uses its own rangefront_waste_test database (and _test_N copies for parallel workers), because the tests rebuild the schema. Never point it at your development database. Under emulation on Apple Silicon the run is slow, so php artisan test --parallel --processes=6 with the same variables is faster.
Postgres runs a whole migrate:fresh in one transaction group, so the compose file raises max_locks_per_transaction to 256. A Postgres you start some other way needs that too, or parallel runs fail with "out of shared memory".
CI runs SQLite only. The public demo runs on MySQL 8 (see DEMO-APP.md); the suite has not been run there, but migrations, the rollback and the seeders have, and must keep working:
bash
docker run -d --name rf-mysql -e MYSQL_ROOT_PASSWORD=rf -e MYSQL_DATABASE=rf_test -p 33099:3306 mysql:8.0
cd apps/backend-laravel
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=33099 DB_DATABASE=rf_test DB_USERNAME=root DB_PASSWORD=rf php artisan migrate:fresh --seedUse a throwaway database for both. migrate:fresh drops every table.
Fakes for outside services
Tests use Http::fake() for Stripe, Twilio, Anthropic, providers and council endpoints, and Mail::fake() or the array mailer for email. Don't let a test call a real service.
Web console
| Command | Does |
|---|---|
npm --workspace apps/web-aurelia run lint | ESLint on src, test, e2e, and Stylelint on the CSS |
npm --workspace apps/web-aurelia run test | Vitest, once (test:watch for watch mode). It runs lint first, through pretest |
npm run web:build | Production build. Run it before claiming a web change is done |
npm run web:e2e | Playwright against a dev server, with demo login switched off |
Vitest specs are in apps/web-aurelia/test, with setup.ts for the DOM. Specs cover the main states of many pages: empty, loading, error, permission denied and the happy path. The Playwright spec is e2e/operator-workflows.spec.ts.
Field app
bash
cd apps/mobile-expo
npx tsc --noEmit # type check
npm run lintReal-device behaviour (photo markup flattening, push registration, biometric unlock) is on the manual list in TODO.md. Expo is version-sensitive, so read apps/mobile-expo/AGENTS.md first.
Contracts
If you change API routes or response shapes, regenerate the contract and commit it:
bash
npm run contracts:generatePlugins and themes
Plugin tests live beside the core ones in tests/Feature/Extensibility. The bundled plugins are covered there, and are a good template. See extending.
Before you commit
npm run backend:testandnpm run backend:analysefor backend changes.- Web lint, test and build for console changes.
npx tsc --noEmitfor field app changes.- Update
TODO.mdwhen a module moves from planned to built.