English
Testing
All tests run with Vitest. Unit tests need nothing but Node.js; integration tests of the server need a real PostgreSQL, and a few need an S3-compatible object store. No test calls a real external API: no email provider, no third-party service, no language model.
Vitest projects
The root vitest.config.ts declares one Vitest project per package, so that one command runs everything and a flag selects a part:
| Project | Files | Environment |
|---|---|---|
workflow, api-types, credentials, connectors, mirror, nodes, engine, tools, server | packages/<name>/src/**/*.test.ts | Node.js. Hook timeout of 60 seconds, for the integration suites that prepare a database. |
ui | packages/ui/src/**/*.test.ts | jsdom, with the Vue plugin. |
docs | apps/docs/scripts/**/*.test.ts | Node.js. Tests of the documentation generator. |
Test files sit next to the code they test, with the .test.ts suffix. Server tests that need PostgreSQL are named *.integration.test.ts; they match the same pattern and belong to the server project.
Run the tests
From the repository root:
sh
pnpm test # every project
pnpm test --project nodes # one project
pnpm test --project server --project engine # several projects
pnpm test mail-flag # files whose path contains "mail-flag"
pnpm test --project nodes -t "mail.flag" # tests whose name matches
pnpm test:watch --project workflow # watch mode
pnpm test --coverage # coverage report (text and HTML)pnpm test runs vitest run, so every Vitest option is available after it. Some packages depend on the compiled output of others: run pnpm build once after cloning and after changing a package that others import.
The PostgreSQL test database
Integration tests run only when TEST_DATABASE_URL is set. Without it they are skipped with an explicit message, never silently green. Start a disposable PostgreSQL 16 on a port that does not collide with the development stack, then point the variable at it:
sh
docker run -d --rm --name test-pg \
-e POSTGRES_PASSWORD=test -p 55432:5432 postgres:16-alpine
TEST_DATABASE_URL=postgres://postgres:test@localhost:55432/postgres pnpm test --project serverTEST_DATABASE_URL points at a server, not at the database the tests use. Each integration test file gets its own database on that server, named by the file (isolatedDatabaseUrl in packages/server/src/db/testing.ts):
- the database is created if it is missing and never dropped;
- at the start of the file, all its tables are emptied, except the table that records applied migrations;
createTestDatabasethen applies the product's real migrations with the product's runner, so tests check the schema that ships.
Databases therefore survive between runs, and the second run skips the migrations that are already applied. Removing the container removes everything. Never point TEST_DATABASE_URL at the database of your development stack.
The variable is a switch of the test harness only: no shipped binary reads it. Test code may read it directly, although product code reads the environment only through the configuration module.
Object storage in tests
The S3 adapter suite runs only when TEST_S3_ENDPOINT is set. The run-attachment suite uses MinIO when the variable is set and an in-memory store otherwise. To run them against MinIO:
sh
docker run -d --rm --name test-minio \
-e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin \
-p 59000:9000 minio/minio:latest server /data
TEST_S3_ENDPOINT=http://localhost:59000 pnpm test --project serverThe suites create their bucket themselves. Set both variables to run the server project like CI does.
Parallelism and --maxWorkers
Vitest runs test files in parallel workers, and all integration files share the same PostgreSQL server. When several people or several test runs use the same test server, or on a small machine, limit the number of workers:
sh
pnpm test --project server --maxWorkers=4A hook that times out (Hook timed out) in an integration file is almost always contention on the shared database, not the test itself. Look at the harness in packages/server/src/db/testing.ts before looking at the test.
Node tests
Each node of the catalog has a test file next to it in packages/nodes/src/catalog/. The helpers of packages/nodes/src/testing/context.ts build a complete context without any server:
| Helper | Role |
|---|---|
runNode(definition, input) | Applies the node's defaults like the engine, then calls execute. |
createContext(input) | Builds the context only, for finer tests. |
bind(TOKEN, double) | Wires a service into the context. |
createMailService, createHttpService, createAttachmentService, createLlmService, createProfileService | Doubles that record calls and return scripted results or a scripted failure (failWith). |
emailFixture(patch) | The triggering email, provided by default; pass email: undefined for a run without email. |
createSignal() | A signal you can abort, to test cancellation. |
The default context uses the idempotency key step-42, the run { id: 'exec-42', workflowId: 'wf-42' }, simulated: false and the French locale; each can be overridden.
Besides each node's own tests, packages/nodes/src/catalog/catalog.test.ts checks the whole catalog: the exact list of node keys and their effects, complete French and English metadata, explicit palette category, search aliases, labelled parameters and options, valid defaults, output ports computed without throwing, input ports according to the node's nature, and an execute that always returns a promise. Adding a node means updating its inventory. The i18n guards are in packages/nodes/src/i18n/.
sh
pnpm test --project nodesInterface tests
Interface tests run in the ui project, in jsdom, with @vue/test-utils to mount components. The i18n guards of the interface are tests too: packages/ui/src/i18n/hardcoded.test.ts (no hard-coded text) and packages/ui/src/i18n/i18n.test.ts (same keys in French and English). Run them after any interface change:
sh
pnpm test --project uiDocumentation tests
The documentation generator has its own tests, in the docs project:
sh
pnpm test --project docsThey test the generator's functions. The completeness of the documentation (a source for every node, a guide for every connection, valid frontmatter, no forbidden word) is checked by the generator itself when it runs:
sh
pnpm --filter @manko/docs generateIt reads the compiled packages, so run pnpm build first.
AI in tests
Tests never call a real language model.
- Node tests use
createLlmService, a double that records the request (the prompt is what most AI node tests check) and returns the text or JSON the test decided. - The server's LLM module is tested against an in-memory provider client and key store (
packages/server/src/llm/testing.ts), including errors, quotas, JSON repair and costs. - In a test run of a workflow, the LLM service is replaced by a simulated one: no network call, no usage recorded, and a deterministic output (a fixed text, or the minimal object that satisfies the requested JSON schema).
The only code that calls a real model is the mailbox analyzer's benchmark, outside the test suite. It uses OpenAI only, needs OPENAI_API_KEY, and stops without it; --dry-run checks its fixtures without any call:
sh
pnpm bench:analyzer --dry-run
OPENAI_API_KEY=… pnpm bench:analyzerIt also uses the disposable test PostgreSQL (TEST_DATABASE_URL, by default the one on port 55432). Do not add tests or tools that call Anthropic's API.
End-to-end tests
The repository has no browser end-to-end suite. The closest check is the CI smoke test: it builds and starts the full container stack with docker compose up, then requires /healthz and /readyz to answer.
What CI requires
CI sets TEST_DATABASE_URL and TEST_S3_ENDPOINT against real PostgreSQL 16 and MinIO services, runs pnpm test with a JSON report, then fails if:
- any test was skipped: in CI every dependency is present, so a skip means a harness variable is miswired;
- the number of tests is below a floor (
MIN_EXPECTED_TESTS), which catches a whole suite disappearing from a project's file pattern.
Every night, a separate job runs the full suite three times in a row on the same database and reports, for each pass, the failed files and the hooks that timed out. A file that fails in only one of the three passes is unstable.
Rules for writing tests
- No real external API: use doubles of services, fake connectors, or a local HTTP server on port 0.
- Deterministic tests: inject the
Clock(from@manko/engine) instead of reading the current time in business code, use fixed seeds, and wait on conditions rather than sleeping. - A fixed bug comes with a test that reproduces it.
- A suite that needs a real dependency uses
describe.skipIf(!enabled)with a suite name that says why it is skipped, never a silent skip.