Testing & Edit Mode
Choose a test mode based on what you want to check. For your first play-tests, use Mode = "Mock": it exercises your game without reading or writing saved profiles.
| I want to… | Use |
|---|---|
| Try gameplay with fresh test data | Mode = "Mock" |
| Check loading and saving across sessions | Mode = "Live" in a separate test experience |
| Inspect an existing saved profile without saving changes | Mode = "NoSave" |
| Preview UI without pressing Play | Edit mode |
| Build an automated test harness | Headless tests |
Play-testing without touching live data
Section titled “Play-testing without touching live data”Set Mode in the shared module that calls Scribe:
return Scribe({ Template = template, ProfileStoreIndex = "ScribeTutorial", ProfileKeyPrefix = "PLAYER_", Mode = "Mock",})Mock mode uses an in-memory store for profiles and leaderboards. It can save and reload within that running server, but its data disappears when the server stops. A new Studio play-test starts with an empty mock store.
Use this for gameplay, validation, and UI tests. It does not tell you whether your game’s existing saved data matches a new template, or whether Roblox’s live services are available.
Test saving across sessions
Section titled “Test saving across sessions”Use a separate test experience for this check, so mistakes cannot affect your live players.
- Set up Getting Started in that experience.
- Publish the place, then enable Enable Studio Access to API Services in its security settings.
- Change the shared module to
Mode = "Live". Keep the same store name and player-key prefix between test runs. - In the server’s
onPlayerAddedfunction, after the coin increment, add the save check below. - Play, wait for
Save confirmed, then stop and play again with the same test player.
if Data.Flush(player) then print("Save confirmed")else warn("Save not confirmed yet; check Output before continuing the test")endWith the Getting Started script, a new test profile prints 50 coins on the first run and 100 on the next: the script adds another 50 each time it loads. The client can still briefly show its default before the loaded value arrives.
Flush waits for save confirmation. false means Scribe could not confirm the save within the wait; it does not prove that the save failed. Do not repeat the coin grant to fix this. Check saving behavior and retry the save if needed.
Return to Mode = "Mock" when you finish testing persistence. "Live" is the default when no mode is specified.
Inspect a real profile without saving it
Section titled “Inspect a real profile without saving it”Use Mode = "NoSave" to load a real profile as a snapshot. Add TargetUserId to inspect one specific player:
Mode = "NoSave",TargetUserId = 123456, -- replace with the player to inspectThese are options in the same table as Template. The store name and key prefix must identify the data you intend to inspect. Your session can read and change its local copy, but Scribe does not save it. Leaderboards use mock storage in this mode too.
This is useful for checking a changed template against data that already exists. It still needs access to Roblox’s DataStores.
Legacy mode options and startup output
UseMock, DontSave, and ViewedUserId remain supported. Prefer Mode for new configurations. If you specify both, Mode takes precedence and Scribe logs MODE_OVERRIDES_LEGACY. See persistence configuration.
Banner = false hides the startup banner; it does not change saving behavior.
Edit mode and storybooks
Section titled “Edit mode and storybooks”When Studio is not running a play-test, the client API starts immediately with template defaults. You can use it in a UI Labs or Hoarcekat story, or the command bar, without a server.
With the shared module from Getting Started, this complete example seeds a coin balance and observes it:
local ReplicatedStorage = game:GetService("ReplicatedStorage")local Data = require(ReplicatedStorage.Shared.GameData).Client
Data.Mock({ Coins = 1250 })local disconnect = Data.Coins.Observe(function(coins) print("Preview coins:", coins) -- 1250end)
-- Call when this preview closes:disconnect()In a real story, keep the observer connected while the component is mounted and call disconnect() when it unmounts. If the story creates its own Scribe instance, also call Data.Stop() when it closes. Do not stop a shared instance that other stories are still using.
Data.Mock and Data.MockCommand only work outside play mode. They throw if used in a running game. Mode = "Mock" is the separate option for play-tests; it does not enable these edit-mode methods.
Preview inventories, purchases, and leaderboards
This larger example uses the Emberfall module, rather than the tutorial’s smaller GameData template:
local ReplicatedStorage = game:GetService("ReplicatedStorage")local Data = require(ReplicatedStorage.Shared.EmberfallData).Client
Data.Mock({ Coins = 1250, Gems = 40, Xp = 4200, Inventory = { Emberblade = { Qty = 1, Rarity = "Epic" }, HealthPotion = { Qty = 7, Rarity = "Common" }, }, Settings = { "Music", "Sfx" },}, { Perks = { "VIP" }, GiftCredits = { GemPack100 = 2 }, Leaderboards = { TopLevel = { { Rank = 1, UserId = 101, Name = "Ava", Score = 42 }, { Rank = 2, UserId = 102, Name = "Ben", Score = 37 }, }, },})
Data.MockCommand("BuyPotion", function(count) return true -- preview only; no real purchase is madeend)Level is derived from XP, so seed Xp and let Scribe compute Level: this example produces level 5. Flags such as Settings are seeded as a list of enabled names.
The second Mock argument supplies Scribe’s own state:
| Key | Client APIs it feeds |
|---|---|
Perks | Owns, ObserveOwned |
GiftCredits | GetGiftCredits |
Leaderboards | GetLeaderboard |
PurchaseLogs | GetPurchases; takes Robux and InGame arrays of log entries |
Mock values still follow your template
Data.Mock merges the supplied fields into current state, then writes through normal accessors. The same field types, bounds policy, and container limits apply. Supply real Roblox datatypes such as CFrame, not their packed buffer representation.
Scribe validates dictionary keys, array layout, declared record fields, and container size. Numbers clamp by default; BoundsPolicy = "Reject" makes out-of-range numbers throw. Invalid enum members are always rejected. See template validation.
How edit mode starts
Scribe checks RunService:IsRunning() because IsServer() can be true in edit mode. It builds only the client preview: no transport folder or handshake is created. Accessing the shared module’s .Server API in edit mode throws.
Scribe Studio
Section titled “Scribe Studio”The Scribe Studio plugin gives you a live panel for inspecting sessions, watching changes, testing commands, and simulating failures. Its guide distinguishes safe previews from tools that edit saved profiles.
Headless tests
Section titled “Headless tests”For most game projects, start with the Studio tests above. An automated harness is useful when you need repeatable tests of your own game logic. Scribe’s repository uses Jest Roblox and Lune. Its helpers provide isolated stores and transports, and its deterministic multi-server simulation also runs the real ProfileStore source against simulated services.
Advanced: constructing an isolated server with internal APIs
Server.build and the template compiler are internal and can change between releases. They are not methods on the public bundle. With a Wally installation, the link module also does not expose their children, so this example uses the versioned _Index folder.
The snippet assumes your test harness provides a compatible player. It is not a standalone command-bar example.
local ReplicatedStorage = game:GetService("ReplicatedStorage")local Scribe = require(ReplicatedStorage.Packages.Scribe)local ScribeRoot = ReplicatedStorage.Packages._Index["ericplane_scribe@2.5.0"].scribelocal Template = require(ScribeRoot.Internal.Template)local Server = require(ScribeRoot.Server)
local options = { Template = { Coins = Scribe.Int(0, { Min = 0 }) }, ProfileStoreIndex = "EmberfallTest", ProfileKeyPrefix = "PLAYER_", Mode = "Mock", TransportChannel = "Test", OwnReceipts = false, KickOnSessionEnd = false, LogLevel = "Fatal",}
local compiled = Template.Compile(options.Template)local Data, ctx = Server.build(options, compiled)
ctx.Persistence.OnPlayerAdded(player)local data, reason = ctx.Persistence.WaitForData(player)assert(data, reason)data.Coins.Increment(50)assert(data.Coins.Get() == 50)
ctx.Persistence.OnPlayerRemoving(player)Data.Stop()build returns the public API and its internal context. Unlike Server.new, it does not connect player events or register a shutdown callback; your harness drives that lifecycle. Removing a player destroys its accessors, so make assertions first.
Always tear down instances, including when an assertion fails, using your test runner’s cleanup hook. Call Data.Stop() on each server and client instance you create. This releases the transport channel, background work, and listeners so later tests can reuse the channel. Stop does not save; call Flush first if persistence is what the test checks.
Optional seams for more advanced tests:
ProfileStore = <your fake>replaces the store.- A custom
Transportcaptures frames instead of usingTransportChannel. Template.Compile(options.Template, { ReplicateRobuxLog = true, ReplicateInGameLog = true })includes purchase logs when your test needs them. PublicScribe(options)derives these flags fromPurchaseLog.
Running Scribe's own suite
In the repository, run rokit install, wally install, then lune run lune/run-tests (or npm run test:unit). The official Jest Roblox packages are pinned in the Wally manifest and lockfile. node scripts/verify-test-runner.mjs (or npm run test:runner) checks the runner’s failure handling.
Use SCRIBE_SPECS to select spec files, SCRIBE_TEST_NAME for a Jest test-name pattern, and SCRIBE_TEST_TIMEOUT to change the per-test timeout in seconds (default 10). SCRIBE_TEST_VERBOSE=1 prints individual test names. Reports are written to test-results/results.json and test-results/junit.xml; SCRIBE_TEST_REPORT_DIR changes the directory. CI also runs with SCRIBE_FRAGMENT_ALL=1 to exercise transport fragmentation and saves that run’s reports separately.
A tight loop that never yields can prevent Jest’s local timeout from firing; stop that Lune process with Ctrl+C. CI also enforces an external deadline on each run.
Snapshot comparisons use __snapshots__/*.spec.snap.lua files beside the specs, mapped by Rojo as ModuleScripts. Normal runs refuse snapshot writes. Set SCRIBE_UPDATE_SNAPSHOTS=1 outside CI to create or update snapshots, then review the diff.
The Studio entry point uses the same Jest configuration. Build rojo build test.project.json -o ScribeTest.rbxlx, open the place, and press Run (F8). ServerScriptService.RunScribeTests prints a startup banner, the spec count, and the command to start Jest. While Run is active, execute require(game:GetService("ReplicatedStorage").ScribeDev.Test.RunTests)() from the server command bar. Jest requires that context’s script-source permissions; the isolated test place enables LoadStringEnabled for its fallback loader. A normal Studio run has been confirmed: 2,813 tests passed, 360 skipped, none failed, and both snapshots passed. Specs that require the Lune simulation host remain headless-only.
The default suite does not write to cloud DataStores. To opt into real backend checks, use a disposable published test universe with Studio API access enabled. Before Run, edit the source of test/Helpers/ConformanceStore.luau to set ConformanceStore.UseReal = true, then rebuild or sync. An external require and table assignment cannot enable them because Jest isolates module instances. Restore false afterward.
Live tests and setup have a 600-second ceiling for write pacing, session loading, and saves; cleanup has 60 seconds. Ordinary tests keep the 10-second default. The shared exchange round trip runs once, and cleanup hooks release owned harnesses after failures or timeouts. After a failed cloud run, stop Run and start a fresh session before retrying.
The deterministic multi-server simulation models latency, request budgets, failures, and contention. Read test/Sim/SCOPE.md for what it covers and what still needs a real Roblox test. These simulations do not measure production service performance.
Where to next
Section titled “Where to next”- Scribe Studio for interactive inspection.
- Diagnostics for logs, health status, and metrics.
- Session Lifecycle for save confirmation and failure handling.
- Configuration for all persistence options.