Testing and gates
Every application in the monorepo has a unit runner, a linter, a production build and an end-to-end suite, and CI runs all of them on every push. This page lists the commands, says what each proves and what it cannot prove, and explains the rules the three e2e harnesses share. The unit runners skip type-checking on purpose, so the production builds are gates too, not packaging steps.
Every e2e command needs the test services up first:
npm run docker:up # postgres-test on 55433, redis-test on 56380, plus the dev pairnpx nx test apinpx nx lint apinpm run test:e2enpx nx test apiruns Jest onapps/api/src/**/*.spec.ts,apps/api/scripts/*.spec.tsand the rootscripts/*.spec.ts, perapps/api/jest.config.ts. It uses ts-jest withisolatedModules: true, so a type error does not fail a spec; type-checking lives innpx nx build api. The config setscollectCoverageFrombut nocoverageThreshold, andjest.preset.jsadds none, sonpm run test:covprints a report and nothing refuses a low number. The 80 percent floor per module is a review rule, not a config. One spec,apps/api/src/modules/geo/geo.service.spec.ts, is excluded bytestPathIgnorePatterns.npx nx lint apilintsapps/api/**/*.tsandtest/**/*.ts(apps/api/project.json).npm run lintruns it together with thecommonlibrary’s lint.npm run test:e2eisnx e2e api --forceExit: Supertest specs matching**/*.e2e-spec.tsundertest/, run in band against a real API. The target depends onbuild, sodist/apps/api/main.jsis fresh.test/e2e-global-setup.tspushes the schema to the test database with--accept-data-loss, re-applies the search index, runsprisma/seed.tsunder thedemostore fixture, backfills notification bodies and order history, flushes the test Redis, re-applies the two raw-SQL indexes, then spawns the built API on 53001 withAPI_GLOBAL_PREFIXempty andNODE_ENV=test. The API log istest/.e2e-api.log. The specs are HTTP clients; Jest never imports the Nest application.
npx nx test adminnpx nx lint adminnpx nx build admin --configuration=productionnpx nx e2e admin-e2enpx nx test adminruns Jest throughjest-preset-angularwithisolatedModules: true(apps/admin/jest.config.ts). A strict-template error, a wrong binding on a Zod-typed model, a missing input: none of these fail a unit spec. The Better Auth browser client is replaced byapps/admin/src/testing/better-auth-client.mock.tsin every spec.npx nx build admin --configuration=productionis the AOT build with the budgets fromapps/admin/project.json: initial bundle 750 kB warning and 900 kB error, any component style 4 kB and 8 kB, themainbundle 40 kB and 80 kB. It catches the template errors Jest misses and refuses a bundle that grew past the budget. Run it before every push that touches the admin; CI runs it as thebuildjob.npx nx e2e admin-e2eis the Playwright suite inapps/admin-e2e/playwright.config.ts.apps/admin-e2e/global-setup.tsstarts the test services, refuses a port 53001 held by a process it did not start (it kills only the pid recorded inapps/admin-e2e/.playwright-state/api.pid), drops and recreates thepublicschema of the test database, pushes the schema, re-applies the search index, runsprisma/seed.ts, builds the API with--skip-nx-cache, spawns it on 53001 under the/apiprefix, checks its identity, signs in the seeded superadmin and stores the session cookies for every spec. The admin itself runs throughapps/admin-e2e/serve-admin-e2e.mjson 53211 withreuseExistingServer: false, so a dev server on 53200 with its proxy to the dev API is never adopted. The API log isapps/admin-e2e/.playwright-state/api.log. Journey specs live underapps/admin-e2e/src/modules/and run in theauthed-desktopproject;authed-mobileand the browser matrix are opt-in with--project=<name>.ADMIN_DEV_PORTandE2E_API_PORTmove the two ports when the OS reserves them.
npm run test:services-e2e is a separate contract suite (apps/admin-services-e2e/playwright.config.ts) that targets http://localhost:53000, the dev API, and pushes the schema on whatever database .env names. It is not in CI and not in the list above.
Storefront
Section titled “Storefront”npx nx test storefrontnpx nx lint storefrontnpx nx build storefront --configuration=productionnpx nx e2e storefront-e2e --grep=<pattern>npm run test:storefront-ssr-smokenpx nx test storefrontruns Vitest throughapps/storefront/vite.config.ts. Coverage uses the v8 provider with no threshold in the config.npx nx build storefront --configuration=productionbuilds the Analog client and the Nitro server. No Playwright project runs this build, and the Nitro server code underapps/storefront/src/server/is bundled by Nitro’s own esbuild pass without the Angular compiler, so a value import of a decorated service there fails only here.scripts/__tests__/nitro-imports.spec.tsguards that import shape statically; the build is the proof.npx nx e2e storefront-e2eboots two servers fromapps/storefront-e2e/playwright.config.ts: the API on 53001 under/apiwithNODE_ENV=test, the test database,DISABLE_THROTTLE=1andAUTH_SKIP_EMAIL_VERIFICATION=1, and the Analog dev server on 53311.apps/storefront-e2e/src/global-setup.tsseeds throughscripts/seed-storefront-test.ts, flushes the test Redis, checks the API’s identity and then checks that the sitemap the API emits names this run’s host. SetREDIS_PORT=56380in the shell, as the CI job does. Filter with--grep=<pattern>; the whole suite is long.SKIP_SEED=1skips the seed while you iterate on a spec. Three projects:desktop-chromium-1440,mobile-chrome,mobile-safari.npm run test:storefront-ssr-smokeruns the Nx targetstorefront:verify-ssr-smoke, which depends on the production build.apps/storefront/tools/verify-ssr-smoke-runner.cjsbootsdist/apps/storefront/analog/server/index.mjson 53311 andapps/storefront/tools/verify-ssr-smoke.cjsreads the served HTML with plain GETs: a filtered catalogue URL, a search URL, a category deep link, and the product page as a control. It is the only gate on the production request path, where page loaders resolve over an internal fetch rather than HTTP. It needs a seeded test API; the default base ishttp://localhost:53001/api, andSTOREFRONT_API_URLmoves it. It refuses to run when 53311 already answers and exits 2 when the API does not answer/v1/health/identity. Exit 1 is a page rendered wrong, exit 2 a harness or fixture problem.
To hold a seeded test API for the smoke the way CI does:
npx nx build apinpx ts-node --transpile-only -r tsconfig-paths/register -P tsconfig.base.json scripts/ci/hold-test-api.ts# in a second shell, once it prints "hold-test-api: ready":STOREFRONT_API_URL=http://localhost:53001 npm run test:storefront-ssr-smokescripts/ci/hold-test-api.ts reuses the backend e2e global setup, so the held API has no /api prefix; the env var above points the smoke at the bare origin.
Repository scripts, the installer and the guards
Section titled “Repository scripts, the installer and the guards”npm run test:scriptsnpm run verify:no-client-refsnpx nx test setup-clinpx nx typecheck setup-clinode tools/setup/verify-standalone.mjsnpm run test:store-config-seednpm run test:fixture-catalogue-seednpm run test:scriptsruns Vitest onscripts/__tests__/**/*.spec.tsandscripts/*.spec.ts(scripts/vitest.config.ts). The specs pin the CI workflow against the tree (ci.spec.ts), the deploy scripts and systemd units (deploy-shared.spec.ts), the Dockerfiles (dockerfiles.spec.ts), the public docs pages (docs-site.spec.ts), the security headers each edge vhost may not repeat (edge-headers.spec.ts), the inline secret-scan allow markers (gitleaks-allows.spec.ts), the Nitro import rule, the release script, the storefront proxy lines in every vhost and the two install wrappers. The Nx project name isrepo-scripts, sonpx nx run-many -t testincludes it.npm run verify:no-client-refsscans every tracked and untracked-but-not-ignored text file for the tokens inscripts/verify-no-client-refs.tokens.jsonand fails on a hit outside its allowlist, or on an allowlist entry that matches nothing.--listprints the allowlisted hits,--summarycounts per directory. The token file is left out of the release tarball, so on a tarball the command reports that it runs in the repository only.- The setup CLI is the Nx project
setup-cli(tools/setup/project.json):testisnpm run test:setup,typecheckisnpm run typecheck:setup,buildisnpm run build:setup.node tools/setup/verify-standalone.mjscopies the stepper and the shared sources to a scratch directory without the workspacenode_modules, installs from the stepper’s own lockfile, builds and runs the bundle’s--version. - The two seed suites have no Nx project; CI runs them by their npm scripts after
nx run-many -t test.
What CI runs on a push
Section titled “What CI runs on a push”.github/workflows/ci.yml runs on every push and every pull request, one job per gate, with a read-only token, and deploys nothing. A newer push to the same ref cancels a run in progress. The jobs, by name:
lintrefuses a lint error in any project:nx run-many -t lintfor the applications, theneslint:lintfor the libraries, the stepper, the scripts and the prisma project.unitrefuses a failing unit spec in any project with atesttarget, then in the two seed suites.verify-no-client-refsrefuses a client name, currency or place anywhere in the tree.docs-generate-checkbuilds the API, regeneratesdocs/site/reference/from the code into a scratch directory and refuses any byte of difference from the committed tree, so a new route, permission, error code or env variable ships with its reference page or not at all.npm run docs:generateis the fix.secret-scanrefuses a secret in the tree or in any commit reachable from the ref, with gitleaks pinned by digest; it first plants two keys in a scratch copy and refuses to continue unless the scanner reports exactly those two.setup-standalonerefuses a stepper that cannot build from its own lockfile on a fresh clone.nginx-templatesrefuses a vhost template thatnginx -trejects, rendered for the server edge and for the local edge, and a compose template thatdocker compose configrejects, for all four profiles.buildrefuses a failingnx build api, an admin build over budget, a failing production storefront build, or a Lighthouse assertion failure on the admin’s public routes.backend-e2erefuses a failing Supertest spec against the dockerized test database.admin-e2erefuses a failing admin journey in chromium, firefox or webkit.storefront-e2erefuses a failing storefront journey in chromium or webkit.ssr-smokerefuses a production Nitro build that renders a filtered page wrong against a seeded API held on 53001 byscripts/ci/hold-test-api.ts.install-smokerefuses an installer that cannot install the local profile on the runner’s Docker fromci/answers.local.json, probes the installed store from outside the installer, then installs again and refuses a second run that does not end with the same closing lines.
One harness at a time on the test database
Section titled “One harness at a time on the test database”The backend suite, the admin suite and the storefront suite share one database (merchants_engine_test on 55433), one Redis (56380) and one API port (53001). Each global setup rewrites that database before its first spec: the admin setup drops the public schema, the backend setup pushes and reseeds, the storefront setup reseeds and flushes Redis. Two of them at once means one suite’s fixtures vanish under the other’s specs, with failures that name a missing row rather than the cause. Run one, let it finish, run the next.
The storefront port 53311 is shared the same way by five processes: the storefront e2e dev server, serve-production, verify-ssr-payloads, verify-ssr-smoke and Lighthouse. The smoke runner refuses a port that already answers instead of testing whatever is listening.
A suite whose teardown never ran leaves its API on 53001. The admin harness names that case in its refusal; stop the leftover process tree before the next run.
How a harness refuses a stale API
Section titled “How a harness refuses a stale API”/health is registered outside the global prefix and returns { status, timestamp }, so it answers 200 on a dev API, a production API and a stale build alike. The harnesses do not trust it. GET /v1/health/identity (apps/api/src/health/health.controller.ts) answers { data: { env } }, and adds the database name only when NODE_ENV is test. Because it sits under v1, it picks up the global prefix, so each harness reaches it through the same base URL its specs use.
scripts/e2e-identity-guard.ts exports assertTestTarget(apiBase, context), and all three harnesses call it before any spec writes. It never resolves on doubt: an unreachable URL, a 404 (an API too old to carry the route), any non-200, a non-JSON body, a missing env, an env other than test, or a database name that does not end in _test all abort the run with a message that names the port and the fix.
test/e2e-global-setup.tsprobes/v1/products?pageSize=1first, which is a 404 on a server started with the/apiprefix, then the identity, and only then reuses a running API.apps/admin-e2e/global-setup.tschecks the identity throughhttp://localhost:53001/apiafter/healthpasses and before it signs in.apps/storefront-e2e/src/global-setup.tschecks the identity, then reads/v1/sitemap-products.xmland refuses an API whose absolute URLs name a host this run is not serving.
There is no knob that points a suite at the dev stack. The overrides that exist move ports: E2E_API_PORT, ADMIN_DEV_PORT, STOREFRONT_E2E_API_PORT, STOREFRONT_DEV_PORT, STOREFRONT_SMOKE_PORT. test/e2e/health-identity.e2e-spec.ts pins the route at both addresses.
Rules the suites hold
Section titled “Rules the suites hold”- No unconditional
test.skip. The conditional formtest.skip(condition, reason)appears where a journey is scoped to a viewport or a tier, for example the desktop-only cases inapps/admin-e2e/src/modules/shell-identity.journey.spec.ts. A few storefront journeys are gated onSTOREFRONT_E2E_BUILT; nothing in.github/workflows/ci.ymlsets it, so those run only when you set it yourself. - No
page.route(), no MSW, no fixtures inside a journey spec. Every journey drives the real API on the seeded test database. Nothing in the lint config enforces this; a review does. - Every storefront journey attaches a console baseline from
apps/storefront-e2e/src/supportand asserts it clean after each test, runs its mutation and error paths under every locale of the active store fixture, and ends a mutation with a reload that asserts the state survived. - Snapshot baselines under the
*.spec.ts-snapshotsdirectories are committed. Update them with--update-snapshotsonly for an intended visual change, and read the diff images CI uploads on failure before you do.