Skip to content

Testing

Terminal window
npx motions test

Runs every motions/*/tests/**/*.test.ts with the tenant’s own Vitest, which must be in its devDependencies. It passes when there are no tests. It doesn’t run tests elsewhere, such as a custom source’s sources/*/index.test.ts: run those with npx vitest run sources.

Tests run in Node, with the step code as it’ll run in a batch: the same harness, the same frozen clock and seeded randomness, and source calls answered by the same matcher as fixture mode.

Runs one step on one company, or a batch step on several. Its options and result are in the SDK reference; how to use it is in Test a motion.

Option Default
company a company keyed co_1, named after its key, with every core field null
fixtures the tenant’s fixtures/sources/, found by walking up from the working directory to tenant.yaml
crm an empty CRM
rules {}
calendar {}
now the real time

The CRM in a test filters the way the test CRM does: tax_id ignoring punctuation, tax_root by prefix, domain ignoring scheme and www., name as a case-insensitive substring. Proposals are recorded but not checked against records, so proposals[].before is null.

A fixture is a recorded answer to a source call. The same files serve three purposes:

  1. Tests: runStep answers every source call from them, built-in and custom.
  2. Fixture mode: a tenant with sources: { fixtures: true } answers built-in calls from them in real batches.
  3. Documentation: they show what each provider returns for your cases.
[
{ "op": "rdap.lookup", "request": { "domain": "aurorametais.example" }, "data": { "registrant": "METALURGICA AURORA LTDA" } },
{ "op": "rdap.lookup", "request": { "domain": "nuvemclara.example" }, "data": null, "note": "nobody registered it" },
{
"op": "fullenrich.enrich_bulk_get",
"request": {},
"sequence": [{ "data": { "status": "pending" } }, { "data": { "status": "done", "contacts": [] } }]
}
]

Matching:

  • Files in fixtures/sources/ are read in name order, entries in file order. The first entry whose op matches and whose request is a subset of the call’s request wins.
  • A subset means every key the fixture gives is in the call with an equal value, recursively. Arrays must be equal. {} matches anything.
  • No match fails the call with fixture_missing: “no fixture matches <op> <request>; add one to fixtures/sources”.

Answers:

  • data: returned as the call’s result. null reads as “not found”.
  • error: { "code": "rate_limited", "message": "…" } fails the call with that class. Codes: not_found, bad_input, not_allowed, no_credit, budget, rate_limited, transient, config.
  • sequence: one answer per identical call, in turn; the last repeats. In a batch, calls are counted per batch. In tests, pass the same fixtureCalls map to several runStep calls to walk through it.

In fixture mode, calls cost nothing and aren’t cached, so editing a fixture takes effect on the next run.

Fixture mode on Fixture mode off
Built-in sources answered from fixtures, free call the provider, with a key, and spend
Custom sources call their real API call their real API
Source keys not used Settings → Source keys
Caching none per operation’s cache time

Turn it on or off in tenant.yaml and push. motions check and push print which it is and how many fixtures there are.

A batch can run on a simulated clock that only moves when an approver advances it. Waits, retries, deadlines, checkpoint cutoffs and calendar instants then happen in order, as fast as the steps run, so a week of a motion plays out in minutes.

It’s only available on a tenant in fixture mode on the test CRM, and is started when opening the batch through the API:

POST /api/tenants/<tenant>/motions/<motion>/batches
Authorization: Bearer <your API token>
Content-Type: application/json
{ "list": "first-batch", "label": "week rehearsal", "clock": { "start": "2026-11-09T08:00:00-03:00" } }

The batch screen then shows Advance the simulated clock, to the next thing due or to a time you choose.

Motions is built by Pruma. These docs describe what the platform does today; Limitations lists what it doesn't do yet. Open the app