Skip to main content

Contributing

Issues and pull requests are welcome. Lest is MIT licensed, and contributions are accepted under that same license.

Getting set up​

You'll need Rust and rokit.

$ git clone https://github.com/lest-luau/lest
$ cd lest
$ rokit install # lune, lute, stylua, and selene, at the pinned versions
$ cargo build

Rust builds the CLI; rokit provides the two runtimes the spawned-runtime suites need plus the two Luau linters, stylua and selene, all pinned in rokit.toml.

Repository layout​

One crate, one binary, and one language per directory.

lest
├── Cargo.toml the single package; src/main.rs → lest
├── build.rs walks luau/core, emits the embedded framework
├── src/ Rust only
│ ├── main.rs CLI surface, orchestration, exit policy
│ ├── backend/ native · runtime (lune/lute) · cloud/
│ ├── resolve/ require resolver, dependency graph, rojo mapping
│ └── report/ protocol schema, reporters, snapshot format, coverage
├── luau/ Luau only — everything lest ships inside the binary
│ ├── core/ the framework: spec API, matchers, hooks, emitter
│ └── runtime/ in-runtime plumbing: the harness template, cloud collector
├── tests/ every Luau spec, one root, one directory per suite
├── docs/ this documentation
└── lest.toml lest testing lest

Rust tests are inline #[cfg(test)] modules beside the code they cover, so tests/ never mixes languages.

src/resolve/ deliberately imports nothing else from the crate — it's a self-contained unit that could become its own crate with a directory move and a manifest. Please keep it that way.

Lest tests itself​

The repository dogfoods its own binary. lest.toml defines five suites across all four backends, and the framework under test is this working copy — not the copy embedded in the binary — via [settings] core.

$ cargo test # the Rust side
$ cargo run -- run # the Luau side, through lest itself
$ cargo run -- run core # just the framework's own specs

The engine suite is default = false, so it stays out of the default run. It needs an Open Cloud key and a place — see Backends.

Before you open a pull request​

Open it against canary, not main. Every change lands on canary first, where it ships as a 0.x.0-canary.N prerelease; main carries the stable releases. GitHub defaults a new pull request to main, so this is the easy one to miss — if you already opened one there, the Edit button beside the pull request title switches the base branch, and nothing else about the pull request changes.

Then, locally:

$ cargo fmt --all --check
$ cargo clippy --all-targets --locked -- -D warnings # must be clean, not merely compiling
$ cargo test --locked
$ cargo run --locked -- run core coverage lune-runtime lute-runtime
$ stylua --check luau tests
$ selene luau tests

These are the exact invocations CI runs (see .github/workflows/ci.yml), so a green local pass is a green CI pass. The Lest run names its suites explicitly rather than running a bare lest: the engine suite is default = false and would auto-enable under $CI, firing an Open Cloud run per platform — it stays a manual lest run engine. stylua --check reports rather than rewrites; drop the --check when you want it to reformat in place.

Conventions​

Rust. Comments explain why, especially where they guard a subtle failure mode. Clippy clean at --all-targets.

Luau. Tabs, single quotes, a space before the paren in function definitions, --!strict at the top of every file. The stylua and selene configs are checked in; run them rather than matching by eye.

Naming. The embedded-VM backend is called native, never "vm". The framework module is Lest (PascalCase) when required. Matchers are dot-calls, and negations are spelled toNot* — not is a Luau keyword, so Jest's .not.toBe isn't available.

Two things that will bite you​

Path identity is an invariant. Any code that generates a require string must normalize both sides before computing a relative path. Mixing a normalized path with an unnormalized one diverges right after the Windows drive letter, producing a require that climbs to the filesystem root and walks back down. That path reads the right file — but Lune and Lute key their module caches on it, so a spec reaching the framework by any other spelling loads a second copy, registers its tests into a registry nobody runs, and the suite reports zero tests while failing nothing. A long ../../../.. run in a generated require is the symptom.

The harness template is real Luau, and must stay that way. luau/runtime/harness.luau is substituted line-by-line, keyed on trailing -- __LEST_*__ marker comments, and every marked line holds a working default. That's what lets it type-check and be formatted like any other file. Keep each marker at the end of the line it belongs to, and never spell a complete marker in prose — matching is by substring.

Architecture​

The documentation covers Lest from a user's side; for the internals, the code is organized to be read. Most modules open with a doc comment explaining what they own and why, and the trickier invariants (path identity in src/resolve/, the harness template in src/backend/runtime.rs) are commented where they live. Start from src/main.rs — its execute_run reads top-to-bottom as config load → suite selection → discovery → per-backend execution → reporting → exit policy — and follow it into whichever component a change touches.