Contributing
Testing
Vyre's tests use the built-in node:test runner and nothing else. Unit tests sit beside their code as *.test.js; tests that cross modules live in test/. Every test gets its own throwaway VYRE_HOME, and none may touch your real ~/.vyre, your real transcripts or your screen. This page covers how the suite is laid out, the helpers that keep it off your machine, and how to run a part of it.
What npm test runs
npm test is node --test over these globs, from package.json:
core/**/*.test.js
test/**/*.test.js
apps/app/**/*.test.js
modules/**/*.test.js
local/*/*.test.js
local/*/lib/*.test.js
A new test file anywhere under those paths is picked up without registering it.
Two lifecycle scripts wrap the run. pretest and posttest run test/tmp-guard.mjs, the leak guard. It fails the run when a test left something behind:
- a folder under this checkout's scratch folder (below) that appeared during the run;
- a new
vyre-*,vy-*,vssh-*orcomputerd-*entry left bare in$TMPDIR. Only entries that did not exist before the run and changed after it began count, so another worktree's old leftovers do not fail yours.vyre-presence-<uid>, the Touch ID helper's build cache, is meant to stay and is never counted.
For a leaked test home, the guard names the test file and the test that made it.
The scratch folder
test/scratch.mjs exports SCRATCH: the one folder under $TMPDIR that tests in this checkout may create directories in. Its name is vt- plus six hex characters hashed from the checkout's path, so two worktrees running the suite at once never see each other's folders. It is short on purpose: a test's vyred puts its unix socket under its VYRE_HOME, and macOS limits a socket path to about 100 bytes.
Make temp folders under SCRATCH, never bare in os.tmpdir(), and remove them in t.after. A module that saves on a timer (a 500 ms debounce, say) must save or stop before the test removes its folder, or the timer makes the folder again after the test ends.
test/scratch.mjs also sets VYRE_TMPDIR to SCRATCH, for this process and every vyre or vyred a test spawns. Product code that makes its own temp folders (backup staging in core/names/backup.js, image thumbnails in core/files/index.js) puts them under VYRE_TMPDIR when it is set, so those stay inside SCRATCH too.
tempHome writes one line per home to <SCRATCH>.homes (the folder, the test file, the test name). The file sits beside SCRATCH, not in it, so it survives the home's removal; the leak guard reads it to name the test behind a leak.
If this happens
tmp-guard fails with a folder you did not make
A sibling worktree running an older suite at the same moment can leave a bare
vyre-*entry that lands inside your run's window. The guard prints the entry's name; if no test in your checkout makes a folder with that prefix, rerunnpm testonce the other run is done.
Helpers
test/helpers.js is the only way a test gets a home:
| Helper | What it does |
|---|---|
tempHome(t) | Makes a folder under SCRATCH, sets VYRE_HOME to it for the test, and restores it after. Throws if the folder would be the real ~/.vyre. After the test it stops any vyred the test started in that home, then removes the folder. |
writeModule(root, name, manifest, source) | Writes a module folder (module.json and index.js) for loader and contract tests. |
present | A presence verifier that always finds a person, for testing what a tool does after approval. Presence's own tests use the real one. |
upPresent(home) | Starts vyred in a child process for a temp home with present as its verifier (test/fixtures/vyred-present.js, which refuses any home outside the temp folder). |
import { test } from "node:test";
import assert from "node:assert/strict";
import { tempHome } from "../../test/helpers.js";
test("a project keeps its name", async t => {
const home = tempHome(t);
// ... start what you need against `home` ...
});
Fakes for outside services
No test runs the real claude, ssh, docker or a browser. Each outside program is reached through an environment variable that a test points at a fake:
| Variable | Replaces | Read in |
|---|---|---|
VYRE_CLAUDE_BIN | claude | core/switchboard, core/onboard |
VYRE_SSH_BIN | ssh | core/cli/ssh.js |
VYRE_OPEN_BIN | the command that opens a browser | vyre up, vyre box |
VYRE_HANDS_BIN | the macOS accessibility helper | local/hands-mac |
The fakes themselves:
core/switchboard/testing/fake-claude.js: aclaudethat speaksstream-json, so the Switchboard and agents run end to end against a realvyred.core/computers/driver/fake.js: an in-memory Docker driver that enforces the Engine's state rules (no pausing a stopped container), so a pool bug fails here as it would on the box.test/journey/: the install journey rig.rig.jsbuilds a fresh Mac and a fresh Linux server as two temp homes on this machine, with fakesshanddockerand a port forward. Thevyredon each side,vyre box add,vyre up, the installer and the onboarding page are real.test/journey.test.jsdrives it.test/fixtures/corpus.js: a small fictional corpus (alex, Juniper Studio, Northwind Bakery, the agents juno and kit), written as real Claude Code transcripts for the transcripts adapter and Recall.
The rule from the spec still holds: anything that talks to Claude Code or a network is also exercised for real once before it merges. A fake proves the logic, not the integration.
The suite also runs on Linux. Where Node has no Argon2id (before 24.7), the vault tests' cheap password key uses scrypt at its test floor instead (TEST_KDF in core/vault/testing.js), and the tests of the real Lumen helpers skip off macOS.
Dialogs
A test must never raise anything on the screen: a Touch ID prompt, a keychain prompt, a browser tab, Lumen. dialogsAllowed() in core/config/dialogs.js is the one check, and the code that could raise one asks it first. It returns false:
- when
VYRE_NO_DIALOGS=1, always; - under
node --test(it seesNODE_TEST_CONTEXT, which child processes inherit), unlessVYRE_TEST_DIALOGS=1is set by a person at the machine running one test on purpose; - when
VYRE_HOMEis anything but~/.vyre, unlessVYRE_ALLOW_DIALOGS=1.
A refused dialog returns the error code no_dialog. Set VYRE_NO_DIALOGS=1 in any script that runs a real vyred unattended.
Run part of the suite
node --test core/vault/*.test.js # one area
node --test test/journey.test.js # the install journey
node --test --test-name-pattern "pass" core/vault/*.test.js # tests whose name matches
node --test test/docs-check.test.js test/docs-build.test.js # the docs tooling
Run from the repository root. A targeted run skips the tmp-guard wrapper, so check SCRATCH yourself if you suspect a leak.
Measured, not only passed
npm run perf-check(scripts/perf-check) holdsvyredto the idle budget in Section 2 of the spec, principle 8. It builds a throwaway home, seeds the fixture corpus plus a synthetic one of about 20,000 turns, starts a realvyred, waits for indexing to finish, then samples it for 60 seconds of idle. It fails on CPU above budget, more than 60 MB of heap in use after a full GC, a settled resident size over 150 MB or a startup peak over 200 MB in the first 30 seconds (both on the Node the box ships), or any timer that repeats faster than once a minute. CI runs it afternpm test. See Performance.npm run eval:memory(scripts/eval-memory.js) scores memory on a fictional world: whether facts are right, and that none leaks from one project to another.test/eval/memory-eval.test.jsruns it undernpm testagainsttest/eval/memory-baseline.json;npm run eval:memory -- --write-baselinerewrites the baseline.npm run docs:checkholds these docs to their contract. See Writing the docs.
CI
.github/workflows/test.yml runs npm test and then npm run perf-check on Ubuntu and macOS, with Node 22 and 24, on every push and pull request.