| group | files | needs | runs |
|---|---|---|---|
| unit | *.test.ts, *.spec.ts |
nothing | build phase, pnpm test |
| integration | *.itest.ts |
the stand from tests/prepare-tests.sh |
pnpm integration |
| bench | *.bench.ts |
nothing (some are env-gated) | pnpm bench, by hand |
*.itest.ts and *.bench.ts do not match a package's default testMatch, so a rename is
all it takes to move a file between groups - no config and no package.json field lists them.
pnpm test # All tests, from the repository root
pnpm run test # A single package, from inside its directory
pnpm test --to @hcengineering/core # Scope to a package and its dependenciesJest is configured at the root (jest.config.js); pnpm jest runs the shared runner
directly. The group runs without Docker; if a unit test needs a service, it is in the
wrong group.
pnpm integration # builds, then runs; needs a running Docker, and nothing else
pnpm integration -t 'name' # jest flags pass throughThe build comes first because workspace dependencies resolve to their lib/; on a built tree it is a cache hit. pnpm api-test does the same for api-tests/ (plus common/scripts/docker-api.sh for the stand images; jest args narrow the api-tests and skip the backup-tests), and pnpm fulltest runs unit, integration and both api suites in a row - everything but Playwright. All three live in common/scripts/test-suite.sh.
pnpm api-test # api-tests, then backup-tests, stand up and down
pnpm api-test src/__tests__/rest.test.ts # one api-tests file
API_STAND=keep pnpm api-test # leave the stand running afterwardsEach suite starts the services it needs itself, through @hcengineering/test-containers
(testcontainers): postgresUrl(), elasticUrl(), kafkaBrokers(), minioConfig(). One container
per kind per jest worker, started on first use, removed when the process ends - a suite that needs
only postgres never starts elasticsearch. tests/prepare-tests.sh is for the Playwright stand now;
the integration group does not use it.
An address already in the environment wins and no container is started for it, so a prepared stand
still works: DB_URL, ELASTIC_URL, QUEUE_CONFIG, MINIO_ENDPOINT.
Nine packages have *.itest.ts files: postgres, elastic, minio, s3, kafka,
pod-fulltext, account, pod-ai-bot, pod-telegram-bot. Two suites still skip themselves:
s3 wants a real S3 (S3_ENDPOINT), and the pod-ai-bot LLM suites want a local model server
(AI_BOT_E2E=1). kafka and pod-fulltext carry testIsolated: they get a jest of their own, run one at a time by pnpm test and side by side by pnpm coverage.
Never part of a CI phase. jest packages run through their own jest.config.js with the
*.bench.ts matcher and a 600s timeout; packages/ui runs its own bench script
(vitest bench).
pnpm coverage # unit only
pnpm coverage --integration # unit + integration, starts its own containers
pnpm coverage --allow-failures # report even when a test failed (exit 0)
pnpm coverage --server # server code only: what is untested, worst firstPrints a per-package table worst-first and one total, and writes into coverage/:
| file | for |
|---|---|
coverage-final.json |
merged istanbul, input to any other reporter |
lcov.info |
Codecov, SonarQube, editors |
cobertura-coverage.xml |
GitLab's artifacts:reports:coverage_report |
html/index.html |
reading it by hand |
The last line is Coverage: NN.NN% of statements, which is what GitLab's coverage: regex
reads. A failing test fails the command unless --allow-failures is passed.
Two runners feed it: jest for everything with a jest.config.js, vitest (istanbul provider) for
packages/ui. Packages that jest cannot run together - desktop and presentation declare
their own projects, which jest will not nest - get their own invocation; flattened into the
shared run they lose their preset and babel then fails to parse TypeScript.
Two things the number does not include, both reported as separate lines:
- packages with a
src/and no test at all - they are in neither report, so they cannot be told apart from 0% any other way .svelteoutsidepackages/ui, which no runner mounts
api-tests/api (26 files, 301 cases), api-tests/backup and the Playwright suites drive a live stand, so the server code they exercise runs inside containers where istanbul cannot see it. V8 can: each pod is started with NODE_V8_COVERAGE and drops a profile of its bundle when it exits, and the pods' external sourcemaps map that back onto src/.
common/scripts/docker-api.sh # images from the checkout
cd api-tests/api && STAND_COVERAGE=true pnpm run api-test && cd ../..
pnpm coverage:stand # writes coverage-stand.json
pnpm coverage --integration --stand # folds it into the whole-repo numberTwo rules, both of which silently produce nothing when broken:
- Let the pods exit on their own. V8 writes the profile as the process exits;
kill, and a stop ordownthat runs past its grace period, lose everything that pod collected. The api-tests stand gives them 60s. - The images must be built from the checkout. The profile carries ranges of the bundle in the image, and the sourcemap used to decode it is the one on disk - a stale image maps its ranges onto whatever lines that file has now.
docker-compose.coverage.yaml mounts one directory per image name, which is how
stand-coverage.js finds each pod's bundle. Adding a pod to the collection is one more block there.
pnpm coverage --integration --server replaces the per-package table with three lists over
foundations/net, foundations/server, pods, server, server-plugins and services
(-assets and model-* packages are left out - they are declarations):
- Server packages with no coverage report at all. No test ever runs there, so they are in no
istanbul report; size is counted from
src/in lines, biggest gap first. - Server files at 0%, inside packages that do run tests. These are in the report only because
of
--collectCoverageFrom; no test ever reached them. - Server packages by coverage, worst first, plus a SERVER total over the packages that have a report.
UI tests run against a full Docker stand, not against sources - build the images first. Full setup (version-override variant, AI bot @llm suite, allure, debug/codegen) is in tests/readme.md.
pnpm install --frozen-lockfile
pnpm docker:build
cd tests
./prepare-pg.sh # create test containers and set up the test database
cd sanity
pnpm run uitestprepare-cockroach.sh is the CockroachDB variant of the same stand. After changing application code, rebuild (pnpm docker:build) and re-run ./prepare-pg.sh - Playwright tests exercise the built bundle, not the source tree.
scripts/persona-stand.sh brings up a stand for persona UX runs (skill persona-test in foundation-tasks) beside the sanity one: compose project persona, overlays tests/docker-compose.mail.yaml and tests/docker-compose.persona.yaml, which publish only nginx on 8084 and mailpit on 8025 (sign-up codes go through the mail service in queue mode, as on prod). Russian branding (tests/branding-persona.json), prod themes, DISABLED_FEATURES and rating, no seeded accounts. From the prod chart (CHART=, default ../fusion-deployment/charts/fusion) it takes the landing, docs and legal images, files/plan-config.yaml and the landing routes, which go into tests/.persona/nginx-landing.conf through the include /etc/nginx/extra/*.conf hook in tests/nginx.conf. LiveKit on the host is shared with the sanity stand.
tests/prepare-secure.sh brings up the secure stand beside the sanity one: compose project secure, overlays tests/docker-compose.mail.yaml and tests/docker-compose.secure.yaml. Only nginx on https://localhost:8443 (tests/nginx-ssl.conf, certificate in tests/.secure/, from mkcert when installed, otherwise self-signed) and mailpit on 8026 are published. The account service runs with the production sign-up checks: no ALLOW_SKIP_OTP_PASSWORD_SIGNUP, no ADMIN_OTP_DEV_CODE, WORKSPACE_LIMIT_PER_USER=10; the front uses USE_OTP=true. No seeded accounts - sign up with the code from mailpit.
Flags (-g "<title>", --workers=1 for love/meetings), tracing, flake diagnosis (analyze_failures.js) and the whole-run profiling harness (do-test.sh) are in the "Sanity tests (Playwright)" section of AGENTS.md.
api-tests/- API (api-tests/api) and backup (api-tests/backup) tests against a stand of their own, without the web UI. The stand is a testcontainers compose environment that the jest run brings up itself (api-tests/api/src/stand.ts) and seeds throughdev/test-base/run.sh api seed. Run withcd api-tests/api && pnpm run api-testandcd api-tests/backup && pnpm run backup-test, aftercommon/scripts/docker-api.shfor the images.API_STANDpicks what a run does with the stand: unset - start it unless it is up, take down only a stand this run started;keep- leave it running (a second run reuses it without re-seeding);stop- take it down at the end either way;external- touch nothing.STAND_COVERAGE=trueaddsdocker-compose.coverage.yaml. Its ports sit 100 above the ws-tests ones (nginx 8183, postgres 5533, redpanda 19193, minio 9102, elastic 9301, mailpit 8126/1126), so it runs beside the sanity and dev stands.ws-tests/- the workspace-admin Playwright suite (ws-tests/sanity).ws-tests/prepare.shsets up its stand.qms-tests/- controlled-documents (QMS) suite, its own stand (./prepare-qms.sh, same ports astests/- run one at a time). UI tests:cd qms-tests/sanity && pnpm run uitest.
All server-side checks run in one job, ci_test.sh, right after pnpm bundle:
common/scripts/docker-api.shbuilds the images of the services inapi-tests/docker-compose.yamlfrom the bundles on disk. No web UI:frontis built with an emptydist/, since the api-tests only need its/config.json, and the webpack bundle is most of a full image build.api-tests/apiruns withAPI_STAND=keepand brings the stand up with the coverage overlay,api-tests/backupreuses it withAPI_STAND=stopand takes it down (logs go toapi-tests/logs/), thenpnpm coverage:standturns the pods' V8 profiles intocoverage-stand.json.pnpm coverage --integration --standruns the unit and integration groups under istanbul, folds the stand run in and is the gate for both jest groups. The integration group and the vitest run go side by side, and packages withtestIsolatedrun alongside the shared jest - each jest process starts its own containers - unlessQUEUE_CONFIGpoints at a shared stand.
STAND_COVERAGE=true is set for the api-tests; rest.test.ts's find avg timing check returns early under it, since pods under NODE_V8_COVERAGE answer 2-3x slower.
- GitHub - the
buildjob in.github/workflows/main.yml(bundle, thenci_test.sh);docker-buildanddist-buildhang off it. The job summary carries the TOTAL/STAND lines and the per-package table,coverage/goes up as thecoverageartifact. - GitLab - the
testjob in.gitlab-ci.yml, which runsci_test.shon a shell runner under the per-host stand lock (.lock_stand). The job reads the percentage off the last line with itscoverage:regex and publishescobertura-coverage.xmlas the merge-request coverage report.DOCKER_REGISTRYis cleared for the stand: the images are local and tagged without one.
The Playwright suites (tests/sanity, qms-tests, ws-tests/sanity) stay in their own uitest:* jobs in GitLab and uitest-* jobs in GitHub, on full images including the web UI.
wait-elastic.sh takes its host from ELASTIC_HOST (default localhost) and now exits non-zero
when elasticsearch never answers, instead of reporting a healthy stand.
- AGENTS.md - "Sanity tests (Playwright)" section: run flags, tracing, flake diagnosis, whole-run profiling.
- tests/readme.md - full Playwright setup, AI bot tests, allure.
- memory/sanity-flaky-tests.md - recurring flake causes and fixes.
- memory/sanity_love_wall_time.md - where the love lane spends time.
- memory/sanity_run_comparability.md - what makes two runs comparable.
- memory/sanity_shared_workspace.md - shared workspace per worker.
- memory/test-phase-single-jest.md - unit test phase internals.
- memory/test-run-telemetry.md - sanity run telemetry tooling.
- memory/ui-component-tests-vitest.md - Component tests in packages/ui (vitest)