Forest Logo
search
package_2

checkpoint

By @jannik-ritter

Roblox

Mirrored from Wally

Checkpoint

End-to-end tests for Rojo-synced Luau games. Scenarios are Luau modules that run inside a real Roblox Studio playtest; a Lune CLI drives Studio over the Studio MCP, reports each step, and saves a screenshot and the console log when one fails.

$ lune run checkpoint run
running 2 scenario(s) in MyGame
game ready

core/boot  Server and client finish loading, and no module fails to require.
  ✓ server loaded (server, 4ms)
  ✓ client loaded (client, 9ms)

shop/buy  Buying an item spends coins once.
  ✓ open shop (client, 120ms)
  ✗ coins spent (server, 10003ms)
      expected coins to be 50 within 10s, last 100
      .checkpoint/shop-buy--coins-spent.jpg
      .checkpoint/shop-buy--coins-spent.log

1 passed, 1 failed

One Wally package holds both halves: the in-game harness (src/) and the Lune runner (runner/). They always come from the same version.

Setup

Requirements: Rokit with Wally, Rojo and Lune, and Roblox Studio with its MCP server enabled (%LOCALAPPDATA%\Roblox\mcp.bat). The runner starts the MCP through cmd.exe, so it works from Windows and WSL.

  1. Add the package as a dev dependency and install it:

    # wally.toml
    [dev-dependencies]
    checkpoint = "jannik-ritter/checkpoint@0.1.1"
    
    rokit add lune-org/lune
    wally install
    

    Dev dependencies install into DevPackages/, apart from the game's Packages/.

  2. Copy the templates from DevPackages/_Index/jannik-ritter_checkpoint@<version>/checkpoint/templates/ into the project root:

    • .lune/checkpoint.luau: the shim behind lune run checkpoint. It finds the installed version, so it never needs editing.
    • tests/adapter.luau and tests/core/boot.luau: a starting adapter and scenario.
  3. For type checking, map DevPackages in default.project.json so luau-lsp can follow Wally's link files. This syncs the package into the place's ServerStorage, where clients never see it and nothing runs unless the runner starts it:

    "ServerStorage": {
    	"DevPackages": { "$path": { "optional": "DevPackages" } }
    }
    

    optional keeps Rojo working when dev dependencies aren't installed.

  4. Optionally copy skill/ to .agents/skills/checkpoint/ so coding agents know how to write and run scenarios.

Running

Open the game in Studio, sync it with rojo serve, and stay in Edit mode. Then:

lune run checkpoint list                    # scenarios, grouped by domain
lune run checkpoint run                     # every scenario
lune run checkpoint run shop                # a domain, a path (shop/buy) or a name (buy)
lune run checkpoint run --studio MyGame     # pick a Studio when several are connected (or set CHECKPOINT_STUDIO)
lune run checkpoint run --json              # one JSON event per line, for agents and CI

Exit codes: 0 all passed, 1 a scenario failed, 2 the run could not complete (no Studio, already playing, timeout). Failure screenshots and console logs go to .checkpoint/; add it to .gitignore.

How a run works

  1. The runner injects the project's DevPackages/ and tests/ into the Edit data model at ReplicatedStorage.Checkpoint, mirroring the project layout, plus a Script and a LocalScript that start the harness. Every injected instance has the CheckpointInjected attribute.
  2. It starts a playtest. The server waits for the test player and for the adapter's ready hook on both sides, then runs the selected scenarios in order and writes progress events to ServerStorage.CheckpointStatus.
  3. The runner polls those events and prints them. A failed step holds the game state until the runner has saved its screenshot and the console.
  4. The runner stops the playtest and removes everything it injected, pass or fail.

Game code can tell it is under test with RunService:IsStudio() and ReplicatedStorage:FindFirstChild("Checkpoint"), for example to use mock persistence.

Writing tests

tests/
	adapter.luau             hooks: ready, reset
	<domain>/<name>.luau     one scenario per module
	_<anything>/             helper modules (not scenarios)

Adapter

local Checkpoint = require("../DevPackages/checkpoint")

return Checkpoint.adapter({
	-- Polled on the server and the client until both return true.
	ready = function(side)
		return workspace:GetAttribute(if side == "server" then "ServerLoaded" else "ClientLoaded") == true
	end,

	-- Runs on the server before every scenario: restore a neutral state and apply the scenario's fixture.
	reset = function(player, fixture) end,
})

Scenarios

--!strict
local Checkpoint = require("../../DevPackages/checkpoint")
local PlayerData = require("@game/ReplicatedStorage/Modules/Shared/PlayerData")
local RewardData = require("@game/ReplicatedStorage/Modules/Data/RewardData")

local t = Checkpoint.t

return Checkpoint.scenario({
	description = "Claiming the daily reward grants coins once.",
	fixture = { Coins = 0 },
	steps = {
		Checkpoint.client("claim", function(ctx)
			-- Act the way a player would: call the controller, fire the remote, trigger the prompt.
		end),
		Checkpoint.server("coins granted", function(ctx)
			ctx.poll(function()
				return PlayerData.Server.Get(ctx.player).Coins.Get()
			end, "coins"):toBe(100)
			ctx.expect(RewardData.Daily, "daily reward"):toMatch(t.interface({ Coins = t.numberPositive }))
		end),
	},
})
  • The relative require resolves on disk (luau-lsp) and in Studio (the injected tree mirrors the project), so scenarios are type-checked against Checkpoint and the game.
  • Steps run in order on their side. After a failure the remaining steps are skipped. Each step times out after 30s; pass a third argument to change it.
  • Steps receive a context, named ctx by convention so t stays free for the type checker. It has player, side, expect(value, label?), poll(read, label?, timeout?), waitFor(read, label?, timeout?) and onCleanup(fn). There is no sleep: wait for an observable condition with poll or waitFor (10s by default).
  • Matchers: toBe, toEqual, toBeTruthy, toBeFalsy, toBeNil, toBeGreaterThan, toBeLessThan, toContain, and toMatch(check) for any t check (Checkpoint.t).
  • Scenarios and the adapter are validated when they load, so a malformed one fails with a message instead of mid-run.
  • Keep scenarios independent: use fixture and the adapter's reset, and onCleanup for anything else a scenario changes.

Limits

  • Scripts cannot press a GuiButton or send real keyboard and mouse input. Drive the code a button or key would call (a controller method, a remote, a prompt's handler) or give the game a Studio-only test hook.
  • One player per run. Multiplayer would use StudioTestService.
  • Runs need a running Studio, so they cannot run on hosted CI.

Development

See AGENTS.md. scripts/install-local.sh <project> installs the working copy into a game's DevPackages to try changes before publishing.

License

MIT

Package Details

Install command (Click to copy)


Version

0.1.1

License

MIT

check_circle

Safe for commercial use

Automated license review — not legal advice.