{"id":"jannik-ritter/checkpoint","name":"checkpoint","scope":"jannik-ritter","platform":"roblox","description":"End-to-end tests for Rojo-synced Luau games, run inside a real Roblox Studio playtest.","version":"0.1.1","latest":"0.1.1","versions":["0.1.0","0.1.1"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{"osyrisrblx/t":{"version":"^3.1.1","alias":"t"}},"integrity":"c8a4ddce489cccce4afd040864dbf3130b9508282f2656e87c05afda8a414e3c","likes":0,"downloads":0,"install":"forest install jannik-ritter/checkpoint","url":"https://forest.dev/p/roblox/jannik-ritter/checkpoint","files":"https://api.forest.dev/ai/package/roblox/jannik-ritter/checkpoint/files","readme":"# Checkpoint\n\nEnd-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.\n\n```text\n$ lune run checkpoint run\nrunning 2 scenario(s) in MyGame\ngame ready\n\ncore/boot  Server and client finish loading, and no module fails to require.\n  ✓ server loaded (server, 4ms)\n  ✓ client loaded (client, 9ms)\n\nshop/buy  Buying an item spends coins once.\n  ✓ open shop (client, 120ms)\n  ✗ coins spent (server, 10003ms)\n      expected coins to be 50 within 10s, last 100\n      .checkpoint/shop-buy--coins-spent.jpg\n      .checkpoint/shop-buy--coins-spent.log\n\n1 passed, 1 failed\n```\n\nOne Wally package holds both halves: the in-game harness (`src/`) and the Lune runner (`runner/`). They always come from the same version.\n\n## Setup\n\nRequirements: [Rokit](https://github.com/rojo-rbx/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.\n\n1. Add the package as a dev dependency and install it:\n\n   ```toml\n   # wally.toml\n   [dev-dependencies]\n   checkpoint = \"jannik-ritter/checkpoint@0.1.1\"\n   ```\n\n   ```bash\n   rokit add lune-org/lune\n   wally install\n   ```\n\n   Dev dependencies install into `DevPackages/`, apart from the game's `Packages/`.\n\n2. Copy the templates from `DevPackages/_Index/jannik-ritter_checkpoint@<version>/checkpoint/templates/` into the project root:\n\n   - `.lune/checkpoint.luau`: the shim behind `lune run checkpoint`. It finds the installed version, so it never needs editing.\n   - `tests/adapter.luau` and `tests/core/boot.luau`: a starting adapter and scenario.\n\n3. 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:\n\n   ```json\n   \"ServerStorage\": {\n   \t\"DevPackages\": { \"$path\": { \"optional\": \"DevPackages\" } }\n   }\n   ```\n\n   `optional` keeps Rojo working when dev dependencies aren't installed.\n\n4. Optionally copy `skill/` to `.agents/skills/checkpoint/` so coding agents know how to write and run scenarios.\n\n## Running\n\nOpen the game in Studio, sync it with `rojo serve`, and stay in Edit mode. Then:\n\n```bash\nlune run checkpoint list                    # scenarios, grouped by domain\nlune run checkpoint run                     # every scenario\nlune run checkpoint run shop                # a domain, a path (shop/buy) or a name (buy)\nlune run checkpoint run --studio MyGame     # pick a Studio when several are connected (or set CHECKPOINT_STUDIO)\nlune run checkpoint run --json              # one JSON event per line, for agents and CI\n```\n\nExit 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`.\n\n### How a run works\n\n1. 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.\n2. 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`.\n3. 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.\n4. The runner stops the playtest and removes everything it injected, pass or fail.\n\nGame code can tell it is under test with `RunService:IsStudio() and ReplicatedStorage:FindFirstChild(\"Checkpoint\")`, for example to use mock persistence.\n\n## Writing tests\n\n```text\ntests/\n\tadapter.luau             hooks: ready, reset\n\t<domain>/<name>.luau     one scenario per module\n\t_<anything>/             helper modules (not scenarios)\n```\n\n### Adapter\n\n```luau\nlocal Checkpoint = require(\"../DevPackages/checkpoint\")\n\nreturn Checkpoint.adapter({\n\t-- Polled on the server and the client until both return true.\n\tready = function(side)\n\t\treturn workspace:GetAttribute(if side == \"server\" then \"ServerLoaded\" else \"ClientLoaded\") == true\n\tend,\n\n\t-- Runs on the server before every scenario: restore a neutral state and apply the scenario's fixture.\n\treset = function(player, fixture) end,\n})\n```\n\n### Scenarios\n\n```luau\n--!strict\nlocal Checkpoint = require(\"../../DevPackages/checkpoint\")\nlocal PlayerData = require(\"@game/ReplicatedStorage/Modules/Shared/PlayerData\")\nlocal RewardData = require(\"@game/ReplicatedStorage/Modules/Data/RewardData\")\n\nlocal t = Checkpoint.t\n\nreturn Checkpoint.scenario({\n\tdescription = \"Claiming the daily reward grants coins once.\",\n\tfixture = { Coins = 0 },\n\tsteps = {\n\t\tCheckpoint.client(\"claim\", function(ctx)\n\t\t\t-- Act the way a player would: call the controller, fire the remote, trigger the prompt.\n\t\tend),\n\t\tCheckpoint.server(\"coins granted\", function(ctx)\n\t\t\tctx.poll(function()\n\t\t\t\treturn PlayerData.Server.Get(ctx.player).Coins.Get()\n\t\t\tend, \"coins\"):toBe(100)\n\t\t\tctx.expect(RewardData.Daily, \"daily reward\"):toMatch(t.interface({ Coins = t.numberPositive }))\n\t\tend),\n\t},\n})\n```\n\n- 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.\n- 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.\n- 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).\n- Matchers: `toBe`, `toEqual`, `toBeTruthy`, `toBeFalsy`, `toBeNil`, `toBeGreaterThan`, `toBeLessThan`, `toContain`, and `toMatch(check)` for any [`t`](https://github.com/osyrisrblx/t) check (`Checkpoint.t`).\n- Scenarios and the adapter are validated when they load, so a malformed one fails with a message instead of mid-run.\n- Keep scenarios independent: use `fixture` and the adapter's `reset`, and `onCleanup` for anything else a scenario changes.\n\n## Limits\n\n- 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.\n- One player per run. Multiplayer would use `StudioTestService`.\n- Runs need a running Studio, so they cannot run on hosted CI.\n\n## Development\n\nSee [AGENTS.md](AGENTS.md). `scripts/install-local.sh <project>` installs the working copy into a game's `DevPackages` to try changes before publishing.\n\n## License\n\nMIT\n","readmeTruncated":false}