---
title: "Test Suite (asset type)"
description: "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…"
section: "Types"
slug: "types-testsuite"
canonical: "https://origozero.ai/docs/types-testsuite"
updated: "2026-09-05T16:41:47.480254700+00:00"
tags: ["asset-type", "reference"]
---

# Test Suite (asset type)

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
└── .metadata      # agent-editable tags + free-form fields
```

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

1. Create `<name>.testSuite/init.luau`. Reach the framework the type
   ships, then declare the suite with `Test.describe(name, fn)` and
   each test with `Test.it(name, fn)`. Documentation is authored as
   doc-comments the engine's doc parser extracts statically: a
   top-of-file `--!desc` for the suite, and a `--!desc` (what the test
   checks) plus `--!pass` (the condition that must hold) directly above
   each `Test.it`.

   ```lua
   --!desc Spawn / despawn / exists / find over the entity API.

   local Test = asset.containing(__FILE__).modules.shared

   Test.describe("Entity Lifecycle", function()
       --!desc entity.spawn(name) creates an entity and returns its proxy.
       --!pass The returned id is a non-empty string and entity.exists(id) is true.
       Test.it("spawn returns a stable string id", function()
           local id = entity.spawn("probe").id
           Test.expect(type(id)).toBe("string")
           Test.expect(entity.exists(id)).toBe(true)
       end)
   end)

   return true
   ```

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`;
  refused in play mode). A bare `run()` covers the world-authored
  suites; `"*"` adds the baked `@builtin` library:
  ```lua
  return tools.use("tests", "run", "*")
  ```
- **One suite directly** (instance-direct — runs only this suite):
  ```lua
  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 —
  `tools.use("tests", "list")`,
  `tools.use("tests", "run", { suite = "vfs" })`,
  `tools.use("tests", "compare", before, after)` to diff two runs and
  surface NEW failures. See the toolbox README.

## Discovery commands

```lua
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 }
tools.use("tests", "list")         -- { count, suites = { { name, identity, path, origin, testCount? } } }
```

## 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 clears apply
  next frame; verify their effects with `t.waitFrames` or 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; the `tests` toolbox tools are
  `.tool` assets.
- **`.component`** — script component; component behavior is frequently
  what a suite exercises.
