Log inGet started

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_regression suite 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.luauallow_unlisted: true permits them.

How to construct a new suite

  1. Create <name>.testSuite/init.luau. Require the framework, then declare the suite with Test.describe and each test with Test.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 / .module assets use for functions. The metadata is what Test.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 true
    

    The 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.

  2. Create <name>.testSuite/README.md describing the suite (what system it covers, notable edge cases, how to run just this suite).

  3. 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). --list prints every discovered suite.
  • From inside the engine (agent loop): the tests toolbox — 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 .testSuite assets (e.g. zinput_state, zinput_events, zinput_actions).
  • No await() inside Test.it(). The framework runs synchronously; for multi-frame behavior use the per-test context t.waitFrames(n) (see the framework README).
  • Clean up. Entities the runner can't attribute are despawned automatically, but prefer Test.afterEach for 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 under Test.TMP_ROOT directly (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 disable in a suite — fix the underlying type/global instead.

Common pitfalls

  • Duplicate stem. Two .testSuite folders with the same stem collide; the runner warns and skips the duplicate, and --list fails. Pick a unique name.
  • Forgetting return true. Harmless but conventional — every suite ends with return true.
  • Deferred mutations. despawn / setParent / scene.clear apply next frame; verify their effects with t.waitFrames or a follow-up query, not in the same frame.
  • .module — reusable Luau library (a suite often requires the system module under test).
  • .tool — agent-callable function; the tests toolbox tools are .tool assets.
  • .component — script component; component behavior is frequently what a suite exercises.
  • asset-type
  • reference