# Test and debug

## The preview scenarios

For game products, the preview reseeds the plays array deterministically per scenario, combined with a play count (1, 5, or 25 plays). Submission renders every scenario, so a finished template must look deliberate under every one:

- Mixed: a realistic spread of wins and losses. The default while building.
- All win: every play wins. Checks stacked celebrations and prize layout.
- All lose: every play loses. Checks the losing experience is not broken or bleak.
- Big win: includes a jackpot prize. Checks large values do not break the layout.
- No plays: an empty plays array, what the template renders with when there is nothing to reveal. The empty state must look deliberate here.

Check each scenario at each play count, because the two combine: a layout that holds at 1 play can break at 25.

## Testing with a connected AI

A connected AI tests through the product's private draft. It pushes each change to the draft and gives the creator the watch link from its first write. The creator keeps that page open beside the chat, steps through every scenario as the preview reloads, and tells the AI what to fix. A draft stays private: nothing is listed until staff approve a version.

This works in every client, including web chat (claude.ai, Claude Desktop, the Claude mobile app, ChatGPT), where the AI cannot open a browser itself.

- When you cannot see the preview, ask the creator what each scenario shows rather than assuming it looks right, because the check proves the template is safe, not that it looks deliberate.
- When the check reports an issue, fix every issue before pushing again, because each push is what the creator watches.
- When a fix needs a judgement the creator has not made (a colour, a copy change, removing a feature), ask first.

## Checks before you finish

- For a game, every scenario has been rendered at every play count, No plays included, and the creator has seen them or you have.
- The template check passes with no issues.
- Nothing was submitted; submitting waits for the creator to ask.

## Preview locally

With the local dev kit, an AI that runs commands on the creator's computer previews and checks every product in the workspace before pushing. `npx @rafflex/dev`, run from the workspace folder, serves one preview for the whole workspace and opens it in the browser. It keeps running, so start it in the background. Its index lists every product with its version and marketplace state, and each product's preview has:

- Scenario and play count: the marketplace's own scenarios (Mixed, All win, All lose, Big win, and No plays) at 1 to 25 plays, rendered with the same sample data as the studio preview.
- Device: phone, tablet, and desktop widths.
- Problems panel: render errors, sandbox and safety issues, and files the media library would refuse, updated on every save, with the line where one applies.
- Live reload whenever that product's `template.twig`, `options.json`, or a file in its `assets/` changes.

`npx @rafflex/dev check <product> --json` renders every scenario headlessly and exits with code 1 when anything would block submission; `--all` checks every product. Before every push the AI runs `npx @rafflex/dev verify <product> --json`, which formats, checks, and with Playwright renders every scenario in a real browser, plays games through, and saves screenshots in the product's `.results/` folder. See [docs/dev-kit.md](https://marketplace.rafflex.io/docs/dev-kit.md) for every command, its JSON output, and its exit codes.

The kit's verdict is guidance. The marketplace's own check on the pushed draft decides, so the AI still reads that result, and the creator still steps through every scenario on the draft before submitting.

## Common causes, mapped to fixes

- "Tag not allowed": only if, for, and set exist in the sandbox. Replace any other Twig tag with allowed tags or move the logic into JavaScript.
- "Filter not allowed": only the filters listed below exist. Replace anything else with an allowed filter or do the work in JavaScript over injected data.
- Too many for loops: the whole template allows a limited number of {% for %} loops. Merge loops or render from JavaScript instead.
- Range operator or range() not allowed: build lists in JavaScript, or loop over an existing array from the context.
- External URL rejected: templates are first party only, so hardcoded http(s) and protocol-relative //domain URLs are banned. Replace every external asset with an uploaded file referenced through the files map, replace a CDN script with the approved library from the studio's Add approved library picker (or with your own inline code), and remove external fonts and stylesheets entirely.
- Banned identifier matched: the checker bans network and evaluation APIs (fetch, WebSocket, navigator, eval, and similar) as bare words, so even a comment containing one is rejected. Remove or rename the matched word.
- "The preview blocked a request": the template asked the browser for an external resource at runtime. Find the assembled or hotlinked URL it names and replace it with an uploaded file from the files map.
- "Scripts may load only an approved library through files, for example src="{{ files['three'] }}". Put your own code inline.": a script src points somewhere other than an approved library's files reference. Move your own code inline into the template, and load a library only as src="{{ files['tag'] }}" after adding it from the studio's Add approved library picker.
- "Import approved libraries by their files URL, for example import * as THREE from "{{ files['three'] }}". Bare imports and other URLs are not allowed.": a module import names a bare specifier such as "three" or "three/addons/...", a hand written URL, or a library that is not approved. Import the approved bundle by its files URL, import * as THREE from "{{ files['three'] }}", and use its addons from the same namespace (new THREE.GLTFLoader()).
- "Import maps are not allowed. Import the approved library by its files URL instead.": delete the <script type="importmap"> block and import the library by its files URL.
- "Dynamic script imports are not allowed.": replace import() with a static import at the top of the module script.
- "This file is not an approved library. Put your own code in the template, or email support@rafflex.io to request a library.": the uploaded .js file is not byte identical to an approved build. Put your own code inline, add an approved build from the studio's picker, or email support@rafflex.io to request a new library.
- "Browsers cannot load .blend files. Export from Blender as .glb (File > Export > glTF 2.0, format glTF Binary).": export the model as a .glb and upload that instead.
- "Upload a single .glb instead: .gltf files reference separate files the files map cannot express.": export again with Format set to glTF Binary (.glb), which embeds the textures.
- "Compressed models need decoders the platform does not ship yet. Export without Draco, KTX2, or meshopt compression.": export again with Compression (Draco) unticked, and skip any tool that adds meshopt or KTX2 compression.
- "is not a valid .glb file": the file is not binary glTF 2.0 (often a renamed file or an old glTF 1.0 export). Export it from Blender as glTF 2.0, format glTF Binary.
- A 3D model or texture fails to load in the preview: reference the .glb only through its files tag, keep its textures embedded, and handle the loader's error callback so the game falls back to its 2D reveal.

## Sandbox rules (hard constraints)

The Twig sandbox rejects anything outside this whitelist:

- Allowed tags: if, for, set. No other tag exists (no block, include, extends, macro, verbatim, or apply).
- Allowed filters: upper, lower, date, number_format, default, length, first, last, join, slice, round, trim, escape, json. No other filter exists.
- No Twig functions at all (no range(), cycle(), random(), dump(), max(), or min()).
- No range operator (..).
- At most 3 {% for %} loops in the whole template.
- Iteration caps: at most 1000 items per collection and 25000 total loop iterations, within a 2000 ms render budget.

Scripts are checked in the source and again in the rendered output of every scenario:

- Your own JavaScript stays inline. A script src, or a static import in a <script type="module">, must be exactly a files reference such as {{ files['three'] }} whose tag points at an approved library. Any other src fails with "Scripts may load only an approved library through files, for example src="{{ files['three'] }}". Put your own code inline."
- Bare imports (import * as THREE from "three"), relative or absolute URLs, and libraries that are not approved fail with "Import approved libraries by their files URL, for example import * as THREE from "{{ files['three'] }}". Bare imports and other URLs are not allowed."
- Import maps fail with "Import maps are not allowed. Import the approved library by its files URL instead."
- Dynamic import() fails with "Dynamic script imports are not allowed."

## Submission rules (hard constraints)

- Templates are first party only, with no allowlist and no exceptions: no external domain may appear anywhere, in any attribute, style, or script. That means no CDN scripts, no Google Fonts, no hotlinked images, no absolute http(s) URLs, and no protocol-relative //domain URLs. Reference only relative platform paths and uploaded media through the files map by tag ({{ files['background'] }}). Every layer enforces this: the static checks, a scan of the rendered output, the preview's security policy, human review, and periodic fleet re-scans.
- No <form> elements, formaction attributes, or <link> tags. Game entry flows through the platform, inputs bound to Alpine state cover every interactive need, and styles ship inline.
- No network or exfiltration APIs, even aliased: fetch, XMLHttpRequest, WebSocket, EventSource, sendBeacon, WebRTC, and the entire navigator object are banned as bare identifiers, along with eval, Function, constructor or __proto__ or Reflect access, import.meta, CSS @import, data:text/html, and hex or unicode escape sequences.
- Everything stays inline in the single template: styles in a <style> block, JavaScript in a <script> block. The one exception is an approved library: a script src, or an import in a <script type="module">, may load one, and only through its files reference, such as {{ files['three'] }}. Your own code never moves into a separate file.
- The template must render a graceful, well-designed empty state when plays is empty and when any list it uses has no entries.

## Documents in this series

Every document is public, plain markdown, and self contained. Read the one that matches the task instead of guessing a rule:

- [Start here](https://marketplace.rafflex.io/docs/how-to-create.md): What Rafflex is, the two asset types and when to build each, the creator workflow, and the sandbox rules.
- [Connect your AI](https://marketplace.rafflex.io/docs/how-to-create/connect.md): The remote MCP server: connecting each AI client, signing in, starter prompts, watch mode, uploads per client, what a connected AI can and cannot do, and disconnecting.
- [Architect a game](https://marketplace.rafflex.io/docs/how-to-create/games.md): The complete specification for building a marketplace game: data contract, plays array, injection pattern, and definition of done.
- [Architect a block](https://marketplace.rafflex.io/docs/how-to-create/blocks.md): The complete specification for building a marketplace page block: data contract, theming from settings, and definition of done.
- [Use images and sound](https://marketplace.rafflex.io/docs/how-to-create/media.md): Uploading images, sound, and 3D models to the product media library, adding approved libraries, and wiring them into a template through the files map.
- [List and submit](https://marketplace.rafflex.io/docs/how-to-create/submit.md): Creating the product, the listing fields, pricing and creator earnings, the Ready to submit checklist, the reviewer pitfalls, and what review checks.
- [After launch](https://marketplace.rafflex.io/docs/how-to-create/after-launch.md): New versions, release channels, how installs update, install notes, bug reports, questions, what a connected AI reports, and announcements.
- [Template data contract](https://marketplace.rafflex.io/docs/template-context.md): every variable a template receives, with generated samples.