Test Suite (asset type)
A test suite is a registrable asset that bundles one group of automated engine tests. Each <name>.testSuite/ folder holds an init.luau whose body registers a suite (and its tests) against the…
Test suites are the engine's regression net. Every Luau-facing system —
FFI bindings, components, the renderer, the VFS, multiplayer, the input
stack — ships its coverage as one or more .testSuite assets. Because a
suite is an ordinary registrable asset, a new system or package adds its
own tests by dropping a .testSuite folder next to its code; nothing
central (the runner, the shell script, CI) has to be edited.
When to use one
- You added or changed a Luau-facing capability and want a permanent regression guard for it.
- You fixed a bug — add a test that reproduces it so it can't silently
come back (the
bug_regressionsuite is the canonical home for one-test-per-issue guards). - A package or system ships behavior that should be validated on every CI run.
If you just want to manually exercise something once, use execute
directly — don't author a throwaway suite.
Where it lives
- Built-in engine suites:
src/lua/lib/tests/suites/<name>.testSuite/(VFS:/zero/source/libs/@builtin/tests/suites/<name>.testSuite/). - Package / system suites: anywhere under the package, e.g.
…/voxelEngine.package/tests/voxel_grid.testSuite/. - Identity: the folder stem (
strip_suffix: true), e.g.vfs. Suite names must be unique across the whole tree — the runner and CI key on the stem.
Folder shape
<name>.testSuite/
├── init.luau # registers the suite via Test.describe / Test.it
└── README.md # what this suite validates + how to run it
Suites may also ship fixture files (helper .module folders, sample
data) alongside init.luau — allow_unlisted: true permits them.
How to construct a new suite
-
Create
<name>.testSuite/init.luau. Require the framework, then declare the suite withTest.describeand each test withTest.it. Both accept an optional metadata table (second argument) so the suite and every test carry a human-readable description and, for tests, an explicit pass condition — the same "describe the surface" discipline.tool/.moduleassets use for functions. The metadata is whatTest.schema()exposes and what ZeroMind renders.local Test = require("@builtin::modules.test") Test.describe("Entity Lifecycle", { description = "Spawn / despawn / exists / find over the entity API.", tags = { "entity", "core" }, }, function() Test.it("spawn returns a stable string id", { description = "entity.spawn(name) creates an entity and returns its proxy; .id reads its stable id.", pass = "The returned id is a non-empty string and entity.exists(id) is true.", }, function() local id = entity.spawn("probe").id Test.expect(type(id)).toBe("string") Test.expect(entity.exists(id)).toBe(true) end) end) return trueThe legacy two-argument forms (
Test.describe(name, fn)/Test.it(name, fn)) still work — the metadata table is optional and purely additive — but new tests should include it. -
Create
<name>.testSuite/README.mddescribing the suite (what system it covers, notable edge cases, how to run just this suite). -
That's it. No registration step, no runner edit. The next test run discovers the suite via
asset.list("testSuite").
See template/ for a copy-ready skeleton (the templater fills in the
[name] markers).
How to run tests
- All suites (writes
/source/tmp/test_results.md+.json):engine.mode = "edit" return tests.run("*") - One suite directly (instance-direct — runs only this suite):
return asset.resolve("vfs", "testSuite"):run() - From the shell / CI:
./scripts/run_luau_tests.sh(all) or./scripts/run_luau_tests.sh <name>(one).--listprints every discovered suite. - From inside the engine (agent loop): the
teststoolbox —tests.list(),tests.run({ suite = "vfs" }),tests.compare(before, after)to diff two runs and surface NEW failures. See the toolbox README.
Discovery commands
asset.list("testSuite") -- every registered suite, as AssetRef handles
asset.inspect("vfs") -- summary for one suite
asset.resolve("vfs", "testSuite") -- handle { guid, identity, path, type }
tests.list() -- { count, suites = { { name, identity, path, testCount? } } }
Test.schema() -- per-suite + per-test metadata (for rendering)
Authoring conventions
- One suite per asset. Keep a suite focused on one system; large
systems split into multiple
.testSuiteassets (e.g.zinput_state,zinput_events,zinput_actions). - No
await()insideTest.it(). The framework runs synchronously; for multi-frame behavior use the per-test contextt.waitFrames(n)(see the framework README). - Clean up. Entities the runner can't attribute are despawned
automatically, but prefer
Test.afterEachfor deterministic cleanup. - Write fixtures via
Test.tmpPath. Any file a test writes to the VFS must go under its per-test sandbox:Test.tmpPath("Foo.component/init.luau")→/zero/source/tmp/tests/<suite>/<test>/.... That root is under/source/tmp/(excluded from world saves + multiplayer sync, so it never clogs the synced VFS) and is torn down per-test and again at end-of-run, regardless of pass/fail/crash. Call it INSIDE the test body. For a fixture shared across tests in one block, write underTest.TMP_ROOTdirectly (survives per-test cleanup, still swept at end-of-run). Never hand-roll a bare/source/<name>path — it persists and syncs. - Idempotent. A suite must produce the same result run twice and must not depend on ordering between suites.
- Never silence the LSP. No
--!nocheck/---@diagnostic disablein a suite — fix the underlying type/global instead.
Common pitfalls
- Duplicate stem. Two
.testSuitefolders with the same stem collide; the runner warns and skips the duplicate, and--listfails. Pick a unique name. - Forgetting
return true. Harmless but conventional — every suite ends withreturn true. - Deferred mutations.
despawn/setParent/scene.clearapply next frame; verify their effects witht.waitFramesor a follow-up query, not in the same frame.
Related types
.module— reusable Luau library (a suite often requires the system module under test)..tool— agent-callable function; theteststoolbox tools are.toolassets..component— script component; component behavior is frequently what a suite exercises.