@seriousgaming/sdk-tools 0.0.0-stage → 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (391) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +36 -2
  3. package/dist/bundling/logic-exports.d.ts +34 -0
  4. package/dist/bundling/node-builtins.d.ts +21 -0
  5. package/dist/bundling/transpiler.d.ts +7 -0
  6. package/dist/cli.js +27368 -0
  7. package/dist/client/dev-server.d.ts +59 -0
  8. package/dist/client/index.d.ts +28 -0
  9. package/dist/client/stub-services.d.ts +32 -0
  10. package/dist/client/test-session.d.ts +195 -0
  11. package/dist/client.js +16884 -0
  12. package/dist/core/asset-reference-translator.d.ts +46 -0
  13. package/dist/core/clock.d.ts +57 -0
  14. package/dist/core/faults.d.ts +65 -0
  15. package/dist/core/game-settings.d.ts +65 -0
  16. package/dist/core/history.d.ts +89 -0
  17. package/dist/core/listen-with-fallback.d.ts +30 -0
  18. package/dist/core/mutation-queue.d.ts +61 -0
  19. package/dist/core/package-scenario.d.ts +89 -0
  20. package/dist/core/participant-registry.d.ts +109 -0
  21. package/dist/core/pending-changes.d.ts +14 -0
  22. package/dist/core/protocol.d.ts +83 -0
  23. package/dist/core/scenario-drafts.d.ts +32 -0
  24. package/dist/core/session-endpoints.d.ts +108 -0
  25. package/dist/core/session-gates.d.ts +75 -0
  26. package/dist/core/session-host.d.ts +94 -0
  27. package/dist/core/session-refusals.d.ts +56 -0
  28. package/dist/core/session-rewind.d.ts +32 -0
  29. package/dist/core/session-roster.d.ts +128 -0
  30. package/dist/core/session-surfaces.d.ts +32 -0
  31. package/dist/core/session-ticks.d.ts +18 -0
  32. package/dist/core/session-timers.d.ts +84 -0
  33. package/dist/core/session-writes.d.ts +40 -0
  34. package/dist/core/session.d.ts +385 -0
  35. package/dist/core/settings-locks.d.ts +22 -0
  36. package/dist/core/team-names.d.ts +52 -0
  37. package/dist/core/team-state-store.d.ts +209 -0
  38. package/dist/core/tick-timer.d.ts +75 -0
  39. package/dist/dev/blob-links.d.ts +10 -0
  40. package/dist/dev/console/assets/index-DgFglsnA.js +204 -0
  41. package/dist/dev/console/index.html +12 -0
  42. package/dist/dev/console-path.d.ts +19 -0
  43. package/dist/dev/lockfile.d.ts +124 -0
  44. package/dist/dev/package-watcher.d.ts +21 -0
  45. package/dist/dev/routes.d.ts +67 -0
  46. package/dist/dev/shell.d.ts +260 -0
  47. package/dist/dev/surfaces.d.ts +41 -0
  48. package/dist/dev/tools-identity.d.ts +23 -0
  49. package/dist/dev/view/batch.d.ts +172 -0
  50. package/dist/dev/view/commands.d.ts +75 -0
  51. package/dist/dev/view/route.d.ts +58 -0
  52. package/dist/dev/view/view.d.ts +78 -0
  53. package/dist/dev/view/viewer.d.ts +32 -0
  54. package/dist/discovery/config.d.ts +9 -0
  55. package/dist/discovery/manifest-paths.d.ts +21 -0
  56. package/dist/discovery/removed-fields.d.ts +90 -0
  57. package/dist/docs/api/deck.md +386 -0
  58. package/dist/docs/api/index.md +67 -0
  59. package/dist/docs/api/sdk-commands.md +286 -0
  60. package/dist/docs/api/sdk-e2e.md +433 -0
  61. package/dist/docs/api/sdk-projection.md +207 -0
  62. package/dist/docs/api/sdk-testing.md +214 -0
  63. package/dist/docs/api/sdk-tools-client.md +560 -0
  64. package/dist/docs/api/sdk-ui-testing.md +231 -0
  65. package/dist/docs/api/sdk-ui.md +1269 -0
  66. package/dist/docs/api/sdk.md +2140 -0
  67. package/dist/docs/changelog-0.x.md +1156 -0
  68. package/dist/docs/changelog.md +236 -0
  69. package/dist/docs/cli-and-dev-server.md +217 -0
  70. package/dist/docs/cli-error-codes.md +154 -0
  71. package/dist/docs/cli-reference.md +2488 -0
  72. package/dist/docs/cli-simulation-testing.md +222 -0
  73. package/dist/docs/concepts/index.md +46 -0
  74. package/dist/docs/concepts/terminology.md +322 -0
  75. package/dist/docs/environment-variables.md +102 -0
  76. package/dist/docs/getting-started.md +145 -0
  77. package/dist/docs/guides/ai-facilitation.md +220 -0
  78. package/dist/docs/guides/capabilities.md +460 -0
  79. package/dist/docs/guides/endpoints.md +335 -0
  80. package/dist/docs/guides/engine-testing.md +477 -0
  81. package/dist/docs/guides/engine.md +527 -0
  82. package/dist/docs/guides/file-handling.md +405 -0
  83. package/dist/docs/guides/games.md +176 -0
  84. package/dist/docs/guides/global-settings.md +94 -0
  85. package/dist/docs/guides/guide.md +123 -0
  86. package/dist/docs/guides/hooks.md +183 -0
  87. package/dist/docs/guides/index.md +51 -0
  88. package/dist/docs/guides/internationalization.md +288 -0
  89. package/dist/docs/guides/llm-integration.md +635 -0
  90. package/dist/docs/guides/multi-team-games.md +586 -0
  91. package/dist/docs/guides/patterns.md +391 -0
  92. package/dist/docs/guides/presentation.md +196 -0
  93. package/dist/docs/guides/production-constraints.md +130 -0
  94. package/dist/docs/guides/scenario-editor.md +143 -0
  95. package/dist/docs/guides/scenarios.md +515 -0
  96. package/dist/docs/guides/session.md +972 -0
  97. package/dist/docs/guides/settings.md +458 -0
  98. package/dist/docs/guides/setup.md +193 -0
  99. package/dist/docs/guides/speech-generation.md +233 -0
  100. package/dist/docs/guides/starter-game.md +164 -0
  101. package/dist/docs/guides/testing.md +575 -0
  102. package/dist/docs/guides/tick-controller.md +569 -0
  103. package/dist/docs/guides/type-safety.md +516 -0
  104. package/dist/docs/guides/ui.md +1389 -0
  105. package/dist/docs/guides/upgrading.md +636 -0
  106. package/dist/docs/guides/validators.md +217 -0
  107. package/dist/docs/guides/waiting-room.md +244 -0
  108. package/dist/docs/index.md +41 -0
  109. package/dist/docs/recipes/act-on-the-session-from-an-endpoint.md +95 -0
  110. package/dist/docs/recipes/adapt-a-game-for-walkthroughs.md +133 -0
  111. package/dist/docs/recipes/adapt-ui-for-observers.md +74 -0
  112. package/dist/docs/recipes/call-endpoint-from-ui.md +109 -0
  113. package/dist/docs/recipes/check-a-package-from-a-script.md +117 -0
  114. package/dist/docs/recipes/create-capability.md +66 -0
  115. package/dist/docs/recipes/create-endpoint.md +127 -0
  116. package/dist/docs/recipes/create-engine.md +111 -0
  117. package/dist/docs/recipes/create-game.md +70 -0
  118. package/dist/docs/recipes/create-guide.md +75 -0
  119. package/dist/docs/recipes/create-hook.md +80 -0
  120. package/dist/docs/recipes/create-presentation.md +142 -0
  121. package/dist/docs/recipes/create-scenario-editor.md +102 -0
  122. package/dist/docs/recipes/create-scenario.md +98 -0
  123. package/dist/docs/recipes/create-session-ui.md +116 -0
  124. package/dist/docs/recipes/create-settings.md +88 -0
  125. package/dist/docs/recipes/create-setup.md +115 -0
  126. package/dist/docs/recipes/create-tick-controller.md +65 -0
  127. package/dist/docs/recipes/create-user-interface.md +96 -0
  128. package/dist/docs/recipes/create-validator.md +67 -0
  129. package/dist/docs/recipes/create-waiting-room-ui.md +109 -0
  130. package/dist/docs/recipes/declare-api-uses.md +67 -0
  131. package/dist/docs/recipes/design-state-for-concurrent-writes.md +96 -0
  132. package/dist/docs/recipes/distribute-package.md +80 -0
  133. package/dist/docs/recipes/e2e-testing.md +116 -0
  134. package/dist/docs/recipes/end-game-on-condition.md +154 -0
  135. package/dist/docs/recipes/fix-duplicate-packages.md +101 -0
  136. package/dist/docs/recipes/index.md +86 -0
  137. package/dist/docs/recipes/make-an-endpoint-external.md +101 -0
  138. package/dist/docs/recipes/negotiate-across-teams.md +164 -0
  139. package/dist/docs/recipes/observe-other-teams-state.md +119 -0
  140. package/dist/docs/recipes/publish-a-package.md +139 -0
  141. package/dist/docs/recipes/remember-a-viewers-choice.md +55 -0
  142. package/dist/docs/recipes/review-a-past-day.md +73 -0
  143. package/dist/docs/recipes/share-assets-across-scenarios.md +120 -0
  144. package/dist/docs/recipes/share-logic-between-engine-and-ui.md +130 -0
  145. package/dist/docs/recipes/show-all-teams-participants.md +109 -0
  146. package/dist/docs/recipes/start-from-an-empty-package.md +169 -0
  147. package/dist/docs/recipes/take-a-file-in-an-endpoint.md +82 -0
  148. package/dist/docs/recipes/test-a-multi-role-game.md +51 -0
  149. package/dist/docs/recipes/test-user-interface.md +95 -0
  150. package/dist/docs/recipes/test-with-engine.md +115 -0
  151. package/dist/docs/recipes/upload-and-display-files.md +148 -0
  152. package/dist/docs/recipes/use-request-play.md +65 -0
  153. package/dist/docs/recipes/use-request-tick.md +75 -0
  154. package/dist/docs/troubleshooting.md +816 -0
  155. package/dist/execution/blob-inputs.d.ts +57 -0
  156. package/dist/execution/bundle-reload.d.ts +42 -0
  157. package/dist/execution/config-resolver.d.ts +127 -0
  158. package/dist/execution/configuration-builder.d.ts +66 -0
  159. package/dist/execution/context-composition.d.ts +161 -0
  160. package/dist/execution/endpoint-actions.d.ts +9 -0
  161. package/dist/execution/endpoint-payloads.d.ts +32 -0
  162. package/dist/execution/endpoint-runner.d.ts +129 -0
  163. package/dist/execution/engine-state.d.ts +40 -0
  164. package/dist/execution/executor-callbacks.d.ts +56 -0
  165. package/dist/execution/executor.d.ts +436 -0
  166. package/dist/execution/gates.d.ts +45 -0
  167. package/dist/execution/hook-chain.d.ts +25 -0
  168. package/dist/execution/logic-calls.d.ts +190 -0
  169. package/dist/execution/logic-failure.d.ts +84 -0
  170. package/dist/execution/logic-loader.d.ts +54 -0
  171. package/dist/execution/logic-result.d.ts +76 -0
  172. package/dist/execution/tick-controller-call.d.ts +32 -0
  173. package/dist/execution/types.d.ts +48 -0
  174. package/dist/generation/ts-type.d.ts +39 -0
  175. package/dist/guide/locale.d.ts +12 -0
  176. package/dist/guide/video-registry.d.ts +42 -0
  177. package/dist/help-examples.d.ts +125 -0
  178. package/dist/host/adapter.d.ts +67 -0
  179. package/dist/host/host-channel.d.ts +222 -0
  180. package/dist/host/index.d.ts +20 -0
  181. package/dist/host/protocol.d.ts +783 -0
  182. package/dist/host/surfaces.d.ts +42 -0
  183. package/dist/host/theme.d.ts +55 -0
  184. package/dist/host.js +583 -0
  185. package/dist/prompts/index.d.ts +7 -0
  186. package/dist/prompts/schema-builder.d.ts +29 -0
  187. package/dist/prompts/types.d.ts +108 -0
  188. package/dist/prompts/validators.d.ts +32 -0
  189. package/dist/runtime/capability-scope.d.ts +6 -0
  190. package/dist/runtime/context-merge.d.ts +29 -0
  191. package/dist/runtime/context-projection.d.ts +41 -0
  192. package/dist/runtime/guarded-callbacks.d.ts +15 -0
  193. package/dist/runtime/index.d.ts +27 -0
  194. package/dist/runtime/manifest-version.d.ts +55 -0
  195. package/dist/runtime/tick-validation.d.ts +25 -0
  196. package/dist/runtime.js +346 -0
  197. package/dist/schema.json +1546 -0
  198. package/dist/server/vite-plugin.d.ts +46 -0
  199. package/dist/services/tts/client.d.ts +51 -0
  200. package/dist/services/tts/elevenlabs.d.ts +24 -0
  201. package/dist/services/tts/types.d.ts +44 -0
  202. package/dist/storage/asset-resolver.d.ts +49 -0
  203. package/dist/storage/blob-store.d.ts +41 -0
  204. package/dist/templates/endpoint/logic.test.ts.liquid +61 -0
  205. package/dist/templates/endpoint/logic.ts.liquid +25 -0
  206. package/dist/templates/engine/init.json.liquid +7 -0
  207. package/dist/templates/engine/logic-aggregate.test.ts.liquid +90 -0
  208. package/dist/templates/engine/logic-aggregate.ts.liquid +46 -0
  209. package/dist/templates/engine/logic.test.ts.liquid +61 -0
  210. package/dist/templates/engine/logic.ts.liquid +32 -0
  211. package/dist/templates/engine/run.json.liquid +7 -0
  212. package/dist/templates/guide/01-at-a-glance.md.liquid +19 -0
  213. package/dist/templates/guide/02-preparation.md.liquid +18 -0
  214. package/dist/templates/guide/03-running-the-session.md.liquid +37 -0
  215. package/dist/templates/hook/logic.test.ts.liquid +46 -0
  216. package/dist/templates/hook/logic.ts.liquid +12 -0
  217. package/dist/templates/package/CLAUDE.md.template +338 -0
  218. package/dist/templates/package/DESIGN.md.liquid +144 -0
  219. package/dist/templates/package/README.md.liquid +143 -0
  220. package/dist/templates/package/claude/rules/workflow-create-capability.md +26 -0
  221. package/dist/templates/package/claude/rules/workflow-create-endpoint.md +35 -0
  222. package/dist/templates/package/claude/rules/workflow-create-engine.md +35 -0
  223. package/dist/templates/package/claude/rules/workflow-create-guide.md +40 -0
  224. package/dist/templates/package/claude/rules/workflow-create-presentation.md +37 -0
  225. package/dist/templates/package/claude/rules/workflow-create-user-interface.md +31 -0
  226. package/dist/templates/package/claude/rules/workflow-publish-package.md +54 -0
  227. package/dist/templates/package/claude/rules/workflow-test-engine-or-ui.md +30 -0
  228. package/dist/templates/package/claude/rules/workflow-update-sdk-version.md +43 -0
  229. package/dist/templates/package/claude/settings.json.template +16 -0
  230. package/dist/templates/package/gitignore.template +15 -0
  231. package/dist/templates/package/global-setup.ts.template +21 -0
  232. package/dist/templates/package/globals.d.ts.template +2 -0
  233. package/dist/templates/package/oxfmtrc.json.template +14 -0
  234. package/dist/templates/package/oxlintrc.json.template +238 -0
  235. package/dist/templates/package/package.json.liquid +65 -0
  236. package/dist/templates/package/playwright.config.ts.liquid +19 -0
  237. package/dist/templates/package/pnpm-workspace.yaml.template +27 -0
  238. package/dist/templates/package/test-setup.ts.liquid +7 -0
  239. package/dist/templates/package/tsconfig.json.liquid +15 -0
  240. package/dist/templates/package/vite.config.ts.liquid +19 -0
  241. package/dist/templates/package/vitest.config.ts.liquid +25 -0
  242. package/dist/templates/package/vitest.engine.config.ts.liquid +12 -0
  243. package/dist/templates/presentation/index.html.liquid +20 -0
  244. package/dist/templates/presentation/src/App.tsx.liquid +49 -0
  245. package/dist/templates/presentation/src/Slides.tsx.liquid +81 -0
  246. package/dist/templates/presentation/src/i18n.ts.liquid +54 -0
  247. package/dist/templates/presentation/src/main.tsx.liquid +14 -0
  248. package/dist/templates/scenario_editor/src/App.test.tsx.liquid +30 -0
  249. package/dist/templates/scenario_editor/src/App.tsx.liquid +95 -0
  250. package/dist/templates/scenario_editor/src/i18n.ts.liquid +44 -0
  251. package/dist/templates/session/src/App.test.tsx.liquid +60 -0
  252. package/dist/templates/session/src/App.tsx.liquid +128 -0
  253. package/dist/templates/session/src/i18n.ts.liquid +53 -0
  254. package/dist/templates/session/src/main.tsx.liquid +48 -0
  255. package/dist/templates/session/src/theme.ts.liquid +46 -0
  256. package/dist/templates/settings-editor/index.html.liquid +12 -0
  257. package/dist/templates/settings-editor/src/main.tsx.liquid +48 -0
  258. package/dist/templates/settings-editor/src/theme.ts.liquid +24 -0
  259. package/dist/templates/setup/src/App.test.tsx.liquid +39 -0
  260. package/dist/templates/setup/src/App.tsx.liquid +102 -0
  261. package/dist/templates/setup/src/i18n.ts.liquid +47 -0
  262. package/dist/templates/starter-game/capabilities/briefing/capability.sdk.json +18 -0
  263. package/dist/templates/starter-game/capabilities/plan/capability.sdk.json +17 -0
  264. package/dist/templates/starter-game/capabilities/results/capability.sdk.json +61 -0
  265. package/dist/templates/starter-game/capabilities/roles/capability.sdk.json +17 -0
  266. package/dist/templates/starter-game/capabilities/weather/capability.sdk.json +23 -0
  267. package/dist/templates/starter-game/engine/defaults.ts +81 -0
  268. package/dist/templates/starter-game/engine/engine.sdk.json +12 -0
  269. package/dist/templates/starter-game/engine/logic.test.ts +221 -0
  270. package/dist/templates/starter-game/engine/logic.ts +149 -0
  271. package/dist/templates/starter-game/engine/test-helpers.ts +74 -0
  272. package/dist/templates/starter-game/engine/types.ts +139 -0
  273. package/dist/templates/starter-game/guide/content/en/01-at-a-glance.md +51 -0
  274. package/dist/templates/starter-game/guide/content/en/02-preparation.md +35 -0
  275. package/dist/templates/starter-game/guide/content/en/03-running-the-day.md +58 -0
  276. package/dist/templates/starter-game/guide/guide.sdk.json +10 -0
  277. package/dist/templates/starter-game/guide/guide.test.ts +108 -0
  278. package/dist/templates/starter-game/hooks/assignproducts/defaults.ts +37 -0
  279. package/dist/templates/starter-game/hooks/assignproducts/hook.sdk.json +13 -0
  280. package/dist/templates/starter-game/hooks/assignproducts/logic/logic.test.ts +77 -0
  281. package/dist/templates/starter-game/hooks/assignproducts/logic/logic.ts +47 -0
  282. package/dist/templates/starter-game/hooks/assignproducts/types.ts +55 -0
  283. package/dist/templates/starter-game/package.json +68 -0
  284. package/dist/templates/starter-game/playwright.config.ts +19 -0
  285. package/dist/templates/starter-game/scenarios/heatwave/scenario.sdk.json +20 -0
  286. package/dist/templates/starter-game/scenarios/ordinary_week/scenario.sdk.json +12 -0
  287. package/dist/templates/starter-game/seriousgaming.sdk.json +23 -0
  288. package/dist/templates/starter-game/session/defaults.ts +80 -0
  289. package/dist/templates/starter-game/session/index.html +12 -0
  290. package/dist/templates/starter-game/session/session.sdk.json +11 -0
  291. package/dist/templates/starter-game/session/src/App.test.tsx +307 -0
  292. package/dist/templates/starter-game/session/src/App.tsx +309 -0
  293. package/dist/templates/starter-game/session/src/Section.tsx +37 -0
  294. package/dist/templates/starter-game/session/src/TeamDoor.tsx +96 -0
  295. package/dist/templates/starter-game/session/src/format.ts +8 -0
  296. package/dist/templates/starter-game/session/src/i18n.ts +51 -0
  297. package/dist/templates/starter-game/session/src/main.tsx +48 -0
  298. package/dist/templates/starter-game/session/src/theme.ts +45 -0
  299. package/dist/templates/starter-game/session/types.ts +135 -0
  300. package/dist/templates/starter-game/settings/engine/settings.sdk.json +50 -0
  301. package/dist/templates/starter-game/settings/menu/settings.sdk.json +78 -0
  302. package/dist/templates/starter-game/settings/session/settings.sdk.json +23 -0
  303. package/dist/templates/starter-game/settings/ui/settings.sdk.json +25 -0
  304. package/dist/templates/starter-game/test-setup.ts +7 -0
  305. package/dist/templates/starter-game/testing/e2e/game.spec.ts +139 -0
  306. package/dist/templates/starter-game/testing/e2e/global-setup.ts +21 -0
  307. package/dist/templates/starter-game/testing/engine/game.engine.test.ts +171 -0
  308. package/dist/templates/starter-game/tsconfig.json +19 -0
  309. package/dist/templates/starter-game/ui/defaults.ts +60 -0
  310. package/dist/templates/starter-game/ui/index.html +12 -0
  311. package/dist/templates/starter-game/ui/src/App.test.tsx +138 -0
  312. package/dist/templates/starter-game/ui/src/App.tsx +175 -0
  313. package/dist/templates/starter-game/ui/src/DayBar.test.tsx +78 -0
  314. package/dist/templates/starter-game/ui/src/DayBar.tsx +72 -0
  315. package/dist/templates/starter-game/ui/src/HistoryPanel.test.tsx +74 -0
  316. package/dist/templates/starter-game/ui/src/HistoryPanel.tsx +120 -0
  317. package/dist/templates/starter-game/ui/src/MakeRow.tsx +129 -0
  318. package/dist/templates/starter-game/ui/src/Section.tsx +37 -0
  319. package/dist/templates/starter-game/ui/src/ThemeToggle.tsx +49 -0
  320. package/dist/templates/starter-game/ui/src/TodayPanel.test.tsx +179 -0
  321. package/dist/templates/starter-game/ui/src/TodayPanel.tsx +92 -0
  322. package/dist/templates/starter-game/ui/src/YesterdayPanel.tsx +142 -0
  323. package/dist/templates/starter-game/ui/src/format.ts +12 -0
  324. package/dist/templates/starter-game/ui/src/i18n.ts +95 -0
  325. package/dist/templates/starter-game/ui/src/main.tsx +48 -0
  326. package/dist/templates/starter-game/ui/src/menu.ts +18 -0
  327. package/dist/templates/starter-game/ui/src/test-helpers.ts +58 -0
  328. package/dist/templates/starter-game/ui/src/theme.ts +45 -0
  329. package/dist/templates/starter-game/ui/src/writes.ts +43 -0
  330. package/dist/templates/starter-game/ui/types.ts +137 -0
  331. package/dist/templates/starter-game/ui/ui.sdk.json +11 -0
  332. package/dist/templates/starter-game/validators/capacity/defaults.ts +37 -0
  333. package/dist/templates/starter-game/validators/capacity/logic/logic.test.ts +96 -0
  334. package/dist/templates/starter-game/validators/capacity/logic/logic.ts +45 -0
  335. package/dist/templates/starter-game/validators/capacity/types.ts +55 -0
  336. package/dist/templates/starter-game/validators/capacity/validator.sdk.json +11 -0
  337. package/dist/templates/starter-game/vite.config.ts +19 -0
  338. package/dist/templates/starter-game/vitest.config.ts +15 -0
  339. package/dist/templates/starter-game/vitest.engine.config.ts +19 -0
  340. package/dist/templates/starter-game/waiting_room/defaults.ts +40 -0
  341. package/dist/templates/starter-game/waiting_room/index.html +12 -0
  342. package/dist/templates/starter-game/waiting_room/src/App.test.tsx +102 -0
  343. package/dist/templates/starter-game/waiting_room/src/App.tsx +194 -0
  344. package/dist/templates/starter-game/waiting_room/src/i18n.ts +33 -0
  345. package/dist/templates/starter-game/waiting_room/src/main.tsx +48 -0
  346. package/dist/templates/starter-game/waiting_room/src/theme.ts +45 -0
  347. package/dist/templates/starter-game/waiting_room/types.ts +59 -0
  348. package/dist/templates/starter-game/waiting_room/waiting_room.sdk.json +10 -0
  349. package/dist/templates/tick-controller/logic.ts.liquid +8 -0
  350. package/dist/templates/ui/index.html.liquid +12 -0
  351. package/dist/templates/ui/src/App.test.tsx.liquid +41 -0
  352. package/dist/templates/ui/src/App.tsx.liquid +103 -0
  353. package/dist/templates/ui/src/i18n.ts.liquid +47 -0
  354. package/dist/templates/ui/src/main.tsx.liquid +48 -0
  355. package/dist/templates/ui/src/theme.ts.liquid +46 -0
  356. package/dist/templates/validator/logic.test.ts.liquid +42 -0
  357. package/dist/templates/validator/logic.ts.liquid +21 -0
  358. package/dist/templates/waiting-room/index.html.liquid +12 -0
  359. package/dist/templates/waiting-room/src/App.test.tsx.liquid +57 -0
  360. package/dist/templates/waiting-room/src/App.tsx.liquid +118 -0
  361. package/dist/templates/waiting-room/src/i18n.ts.liquid +44 -0
  362. package/dist/templates/waiting-room/src/main.tsx.liquid +48 -0
  363. package/dist/templates/waiting-room/src/theme.ts.liquid +46 -0
  364. package/dist/templating/resource-metadata.d.ts +131 -0
  365. package/dist/types.d.ts +14 -0
  366. package/dist/utils/atomic-write.d.ts +40 -0
  367. package/dist/utils/cli-error.d.ts +64 -0
  368. package/dist/utils/document-parser-client.d.ts +20 -0
  369. package/dist/utils/env-loader.d.ts +40 -0
  370. package/dist/utils/error-codes.d.ts +335 -0
  371. package/dist/utils/errors.d.ts +19 -0
  372. package/dist/utils/global-settings.d.ts +65 -0
  373. package/dist/utils/llm-client.d.ts +34 -0
  374. package/dist/utils/logger.d.ts +36 -0
  375. package/dist/utils/output.d.ts +109 -0
  376. package/dist/utils/platform.d.ts +14 -0
  377. package/dist/utils/prompt.d.ts +36 -0
  378. package/dist/utils/real-path.d.ts +6 -0
  379. package/dist/utils/schema-properties.d.ts +35 -0
  380. package/dist/validation/ajv.d.ts +17 -0
  381. package/dist/validation/facilitator-guide-validator.d.ts +27 -0
  382. package/dist/validation/findings.d.ts +40 -0
  383. package/dist/validation/game-constraints.d.ts +10 -0
  384. package/dist/validation/game-validator.d.ts +34 -0
  385. package/dist/validation/package-validation.d.ts +41 -0
  386. package/dist/validation/package-validator.d.ts +19 -0
  387. package/dist/validation/resource-validator.d.ts +79 -0
  388. package/dist/validation/schema-defaults-validator.d.ts +29 -0
  389. package/dist/validation/schema-validation.d.ts +115 -0
  390. package/dist/validation/semver.d.ts +9 -0
  391. package/package.json +122 -3
@@ -0,0 +1,575 @@
1
+ # Testing
2
+
3
+ This guide shows you how to write and run tests for your engine and user interfaces.
4
+
5
+ ## What you'll learn
6
+
7
+ - How to write unit tests for the engine
8
+ - How to test React user interface components
9
+ - How to run tests locally
10
+
11
+ ## Testing engine logic
12
+
13
+ ### Starting from the generated defaults
14
+
15
+ Alongside `types.ts`, the build generates a `defaults.ts` beside every resource, holding the values a session opens with:
16
+
17
+ ```typescript
18
+ // engine/defaults.ts (generated, do not edit)
19
+ export const defaultContext: Context = {
20
+ inventory: { Stock: 0, LastRestocked: '' },
21
+ sales: { Sales: 0 },
22
+ };
23
+
24
+ export const defaultSettings: Settings = {
25
+ engine: { DailySales: 10 },
26
+ };
27
+ ```
28
+
29
+ A test spreads those and names only what the case is about:
30
+
31
+ ```typescript
32
+ const context: Context = {
33
+ ...defaultContext,
34
+ inventory: { Stock: 100, LastRestocked: '2024-01-01' },
35
+ };
36
+ ```
37
+
38
+ This matters as an engine grows. `Context` is the union of every capability the engine reads or writes, and TypeScript requires all of them, so a test about one capability would otherwise have to write out twenty. The usual escape is `as unknown as Context`, which switches off shape checking for the whole fixture, so the test stops catching the typo it existed to catch. Spreading the defaults keeps both the brevity and the checking, and a capability added later arrives in every fixture already populated instead of breaking each one.
39
+
40
+ Because they are generated from the same schemas the types are, the values cannot drift from what a real session starts with.
41
+
42
+ An engine's `defaults.ts` also exports `defaultTeamContext` and `defaultSharedContext`, the same values split by scope to match the generated `TeamContext` and `SharedContext`. They are what an aggregate function's `teams` and `shared` arguments hold (see [Testing an aggregate function](#testing-an-aggregate-function)).
43
+
44
+ Every capability variable must declare a `default` for this to work. The build says so, naming the capability and the variable, if one does not.
45
+
46
+ ### Testing the Run function
47
+
48
+ `Init` and `Run` receive a single args object `{ context, settings, meta, participants, callbacks }`. Most of it is the runtime's to supply, so build it with `logicArgs` from `@seriousgaming/sdk/testing` and name only what the test is about:
49
+
50
+ ```typescript
51
+ // engine/logic.test.ts
52
+ import { logicArgs } from '@seriousgaming/sdk/testing';
53
+ import { describe, it, expect } from 'vitest';
54
+ import { defaultSettings, defaultContext } from './defaults';
55
+ import { Run } from './logic';
56
+ import type { Context, Settings } from './types';
57
+
58
+ describe('Warehouse Engine', () => {
59
+ it('should decrease stock by daily sales', async () => {
60
+ const context: Context = {
61
+ ...defaultContext,
62
+ inventory: { Stock: 100, LastRestocked: '2024-01-01' },
63
+ };
64
+ const settings: Settings = { engine: { ...defaultSettings.engine, DailySales: 10 } };
65
+
66
+ const result = await Run(logicArgs.run({ context, settings }));
67
+
68
+ expect(result.capabilities.inventory.Stock).toBe(90);
69
+ });
70
+
71
+ it('should not allow negative stock', async () => {
72
+ const context: Context = {
73
+ ...defaultContext,
74
+ inventory: { Stock: 5, LastRestocked: '2024-01-01' },
75
+ };
76
+
77
+ const result = await Run(logicArgs.run({ context, settings: defaultSettings }));
78
+
79
+ expect(result.capabilities.inventory.Stock).toBeGreaterThanOrEqual(0);
80
+ });
81
+ });
82
+ ```
83
+
84
+ The result passes straight to `Run` with no cast: its types come from `Run`'s own, which come from the generated `types.ts`. Everything not named is filled with fixed values:
85
+
86
+ | Argument | Default |
87
+ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | `meta` | `tick: 1` (`0` for `logicArgs.init`), `teamId: 'team-1'`, `index: 0`, `locale: 'en'`, `seed: 'test-seed'`, `runKind: 'session'`, `now` 2026-01-01T00:00:00Z. Pass any of them in `meta` to change it |
89
+ | `participants` | One connected participant, id `'1'`, on the call's team |
90
+ | `callbacks` | The offline stubs (see [Testing with LLM callbacks](#testing-with-llm-callbacks)). Pass any of them in `callbacks` to replace it |
91
+ | `settings` | None. There is no runtime to resolve them, so pass the generated `defaultSettings`, spread with what the test changes. A resource that declares no settings may leave it out |
92
+
93
+ ```typescript
94
+ const args = logicArgs.run({
95
+ context,
96
+ settings: defaultSettings,
97
+ meta: { tick: 3, runKind: 'walkthrough' },
98
+ state: { BestProfit: 100 },
99
+ });
100
+ ```
101
+
102
+ A step function an engine calls from `Run` takes the same arguments, so `logicArgs.run` builds them too.
103
+
104
+ ### Testing the Init function
105
+
106
+ `logicArgs.init` is `logicArgs.run` with `meta.tick` at 0, as `Init` sees it:
107
+
108
+ ```typescript
109
+ import { Init } from './logic';
110
+
111
+ describe('Warehouse Init', () => {
112
+ it('should initialize with correct default values', async () => {
113
+ const settings: Settings = { engine: { ...defaultSettings.engine, InitialStock: 500 } };
114
+
115
+ const result = await Init(logicArgs.init({ context: defaultContext, settings }));
116
+
117
+ expect(result.capabilities.warehouse.Stock).toBe(500);
118
+ expect(result.capabilities.warehouse.ReorderCount).toBe(0);
119
+ });
120
+ });
121
+ ```
122
+
123
+ ### Testing an aggregate function
124
+
125
+ `InitAggregate` and `RunAggregate` receive `{ shared, teams, settings, meta, participants, callbacks }`. Pass each team from `defaultTeamContext`, which carries team scope only, and the shared store once, in `shared`. `logicArgs.aggregate` names each team from its `index` and seats one participant on each:
126
+
127
+ ```typescript
128
+ import { logicArgs } from '@seriousgaming/sdk/testing';
129
+ import { defaultSettings, defaultSharedContext, defaultTeamContext } from './defaults';
130
+ import { RunAggregate } from './logic';
131
+
132
+ function makeTeams(...prices: number[]) {
133
+ return prices.map((price, index) => ({
134
+ index,
135
+ context: { ...defaultTeamContext, price: { Price: price } },
136
+ }));
137
+ }
138
+
139
+ it('writes the average price to the shared store', async () => {
140
+ const result = await RunAggregate(
141
+ logicArgs.aggregate({
142
+ shared: { context: defaultSharedContext },
143
+ teams: makeTeams(4, 6),
144
+ settings: defaultSettings,
145
+ })
146
+ );
147
+
148
+ expect(result.shared.capabilities.market?.AverageMarketPrice).toBe(5);
149
+ });
150
+ ```
151
+
152
+ For `InitAggregate`, pass `meta: { tick: 0 }`. A shared capability belongs in `result.shared.capabilities`, never in a `teams` entry: returning one there fails the tick at runtime, so a test that asserts on `result.teams[0].capabilities.market` is asserting on something the runtime would reject.
153
+
154
+ ### Testing hooks, endpoints, validators and tick controllers
155
+
156
+ Each kind of logic has its own builder, typed from the function it is passed to:
157
+
158
+ | Builder | For | Fills, beyond `meta`, `participants` and `callbacks` |
159
+ | -------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
160
+ | `logicArgs.hook` | A hook's `Run` | The roster is the payload's `participant`. The signal defaults to `participantJoin`; name `signal: 'participantMove'` for a move, whose payload it then requires |
161
+ | `logicArgs.endpoint` | An endpoint's `Handle` or `Plan` | `team` (`team-1`), `session` (independent, no ticks completed) and `caller`: the team's first participant through `ui`, or pass `caller: { role: 'facilitator' }` (from `session`) or `caller: { role: 'author' }` (from `scenario_editor`, with no `sessionId`) |
162
+ | `logicArgs.validator` | A validator's `UpdateData` | `actor`: the roster's first participant |
163
+ | `logicArgs.requestTick` | A validator's `RequestTick` | `actor`, and `requestedTick: 1` |
164
+ | `logicArgs.tickController` | `GetTickController` | `teams`: the one team `meta` names, with `context`. `waitingRoomOpen: false` |
165
+
166
+ ```typescript
167
+ const result = await Handle(
168
+ logicArgs.endpoint({ request: { price: 12 }, context, caller: { role: 'facilitator' } })
169
+ );
170
+
171
+ const verdict = await UpdateData(
172
+ logicArgs.validator({ previous, next, settings: defaultSettings })
173
+ );
174
+
175
+ await Run(
176
+ logicArgs.hook({
177
+ signal: 'participantMove',
178
+ context,
179
+ participant,
180
+ fromTeamId: 'team-1',
181
+ toTeamId: 'team-2',
182
+ phase: 'departure',
183
+ })
184
+ );
185
+ ```
186
+
187
+ ### Testing with LLM callbacks
188
+
189
+ The callbacks `logicArgs` fills are offline stubs, the same a test session uses; pass one in `callbacks` to choose its answer. See [Service callbacks are stubbed](./engine-testing.md#service-callbacks-are-stubbed) for what each stub answers and how to override it.
190
+
191
+ ## Testing user interfaces
192
+
193
+ User interfaces are React components. The SDK scaffold sets up [Vitest](https://vitest.dev/) with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) automatically: `App.test.tsx` is created alongside `App.tsx`.
194
+
195
+ ### How it works
196
+
197
+ A package is one game, so it has one of each of these at its root:
198
+
199
+ - `vitest.config.ts`: Vitest config with `jsdom` environment
200
+ - `test-setup.ts`: imports `@testing-library/jest-dom` matchers and sets up cleanup
201
+
202
+ Each UI's tests sit beside its component: the game UI's are at `ui/src/App.test.tsx`.
203
+
204
+ ### Using render helpers
205
+
206
+ The SDK provides render helpers that set up all required context providers with sensible defaults. Each UI type has a dedicated helper:
207
+
208
+ | Helper | UI type |
209
+ | ------------------------------ | ----------------------------- |
210
+ | `renderUserInterface()` | Game UIs |
211
+ | `renderSettingsEditor()` | Setup and the scenario editor |
212
+ | `renderSessionUserInterface()` | Session UIs |
213
+
214
+ Every helper accepts an options object for overriding any context value and returns the `@testing-library/react` render result plus any mock functions (`setData`, a settings editor's `updateSettings`, `callbacks`) for assertions.
215
+
216
+ #### Game UIs
217
+
218
+ ```typescript
219
+ // ui/src/App.test.tsx
220
+ import { describe, it, expect, vi } from 'vitest';
221
+ import { screen, fireEvent } from '@testing-library/react';
222
+ import { renderUserInterface } from '@seriousgaming/sdk/ui/testing';
223
+ import type { Context, Settings } from '../types';
224
+ import App from './App';
225
+
226
+ const mockData: Context = {
227
+ inventory: { Stock: 100 },
228
+ pricing: { Price: 25 },
229
+ };
230
+
231
+ const mockSettings: Settings = { ui: { MaxPrice: 500 } };
232
+
233
+ describe('Dashboard', () => {
234
+ it('renders the current stock', () => {
235
+ renderUserInterface(<App />, { data: mockData, settings: mockSettings });
236
+ expect(screen.getByText(/100/)).toBeInTheDocument();
237
+ });
238
+
239
+ it('calls setData when price is changed', () => {
240
+ const setData = vi.fn();
241
+ renderUserInterface(<App />, { data: mockData, settings: mockSettings, setData });
242
+
243
+ fireEvent.change(screen.getByLabelText(/price/i), { target: { value: '50' } });
244
+ expect(setData).toHaveBeenCalledWith({
245
+ pricing: { Price: 50 },
246
+ });
247
+ });
248
+
249
+ it('can request a tick via callbacks', () => {
250
+ const requestTick = vi.fn();
251
+ renderUserInterface(<App />, {
252
+ data: mockData,
253
+ settings: mockSettings,
254
+ callbacks: { requestTick },
255
+ });
256
+
257
+ screen.getByText('Submit').click();
258
+ expect(requestTick).toHaveBeenCalledOnce();
259
+ });
260
+ });
261
+ ```
262
+
263
+ Default values when you omit an option:
264
+
265
+ | Option | Default |
266
+ | ---------------- | --------------------------------------------------------------------------------- |
267
+ | `data` | `{}` |
268
+ | `settings` | `{}` |
269
+ | `completedTicks` | `0` |
270
+ | `setData` | no-op |
271
+ | `theme` | `createMockTheme()` (light mode, MUI palette) |
272
+ | `locale` | `{ locale: 'en' }` |
273
+ | `participant` | `{ id: '1', name: 'Participant 1', index: 0, connected: true, teamId: 'team-1' }` |
274
+ | `participants` | `[]` (`createMockParticipants(n)` builds a roster) |
275
+ | `timer` | Disabled (no auto-tick) |
276
+ | `callbacks` | All present as no-ops |
277
+
278
+ Every option, and those of the other two helpers, is listed in the [UI testing API reference](../api/sdk-ui-testing.md#renderuserinterfaceoptions).
279
+
280
+ A waiting room renders under the game UI's providers, so `renderUserInterface` tests it too. A presentation has no render helper: see [Presentations](./presentation.md#testing).
281
+
282
+ #### Setup and the scenario editor
283
+
284
+ The generated `Settings` of a `setup` or `scenario_editor` slot is keyed by the settings resources it lists in `configures`, and every property of each is present, so its generated `defaultSettings` is what a fixture starts from. Writing the settings defaults out by hand instead leaves a copy that rots silently the next time the schema changes.
285
+
286
+ ```typescript
287
+ import { renderSettingsEditor } from '@seriousgaming/sdk/ui/testing';
288
+ import { defaultSettings } from './defaults';
289
+
290
+ it('calls updateSettings when a value changes', () => {
291
+ const updateSettings = vi.fn();
292
+ renderSettingsEditor(<App />, {
293
+ settings: { ...defaultSettings, engine: { ...defaultSettings.engine, BaseDemand: 100 } },
294
+ updateSettings,
295
+ });
296
+
297
+ fireEvent.change(screen.getByLabelText(/base demand/i), { target: { value: '200' } });
298
+ expect(updateSettings).toHaveBeenCalledWith({ engine: { BaseDemand: 200 } });
299
+ });
300
+ ```
301
+
302
+ #### Session UIs
303
+
304
+ Type the helpers with the surface's generated types: `renderSessionUserInterface<SessionData>`, `createMockTeamState<TeamContext>` and `createMockSharedState<SharedContext>`. Each fixture's context then accepts only capabilities of its own scope, any of them optional, so a shared capability left on a team's fixture fails to compile, as reading one through a team does in the surface. Left untyped, the helpers accept any context.
305
+
306
+ ```typescript
307
+ import {
308
+ createMockSharedState,
309
+ createMockTeamState,
310
+ renderSessionUserInterface,
311
+ } from '@seriousgaming/sdk/ui/testing';
312
+
313
+ import type { SessionData, SharedContext, TeamContext } from '../types';
314
+
315
+ it('resets all teams', () => {
316
+ const resetGame = vi.fn();
317
+ renderSessionUserInterface<SessionData>(<SessionDashboard />, {
318
+ sessionData: {
319
+ // Shared capabilities arrive once, beside the teams. Either half
320
+ // may be left out, and defaults to empty
321
+ shared: createMockSharedState<SharedContext>({
322
+ context: { briefing: { Message: 'Welcome' } },
323
+ }),
324
+ teams: {
325
+ 'team-1': createMockTeamState<TeamContext>({ completedTicks: 3 }),
326
+ },
327
+ },
328
+ teams: [{ teamId: 'team-1', index: 0, name: 'Alpha' }],
329
+ teamConfiguration: { mode: 'independent', max: 6 },
330
+ callbacks: { resetGame },
331
+ });
332
+
333
+ screen.getByText('Reset All').click();
334
+ expect(resetGame).toHaveBeenCalledOnce();
335
+ });
336
+
337
+ it('writes a per-team override', () => {
338
+ const setData = vi.fn();
339
+ renderSessionUserInterface<SessionData>(<SessionDashboard />, {
340
+ sessionData: {
341
+ teams: {
342
+ 'team-1': createMockTeamState<TeamContext>({ context: { price: { Price: 10 } } }),
343
+ },
344
+ },
345
+ teams: [{ teamId: 'team-1', index: 0, name: 'Alpha' }],
346
+ setData,
347
+ });
348
+
349
+ fireEvent.change(screen.getByLabelText(/price/i), { target: { value: '20' } });
350
+ expect(setData).toHaveBeenCalledWith('team-1', { price: { Price: 20 } });
351
+ });
352
+ ```
353
+
354
+ A session UI's `setData` takes the team id first for a team's own capabilities, and the update alone for a shared one. Either form passes its options last, so a delete is asserted as `expect(setData).toHaveBeenCalledWith('team-1', {}, { deletes: [['pinboard', 'Notes', 'p1']] })`.
355
+
356
+ ### Never wire the providers by hand
357
+
358
+ Always render through these helpers. They supply every context a surface reads, with complete defaults, and each option overrides one value, so a test that needs a particular theme, locale, callback or viewer role passes it as an option. A hand-built provider tree has to supply every callback and every field itself, and it silently stops matching the runtime when the SDK adds one.
359
+
360
+ ### Testing controlled inputs
361
+
362
+ For inputs that call `setData` on change, use `fireEvent.change` rather than `userEvent.type`. Because `setData` is mocked, the component's displayed value won't update between keystrokes. `fireEvent.change` fires a single synthetic event with the exact target value, which correctly triggers `onChange`:
363
+
364
+ ```typescript
365
+ import { fireEvent } from '@testing-library/react';
366
+
367
+ it('calls setData when price is changed', () => {
368
+ const setData = vi.fn();
369
+ renderUserInterface(<App />, { data: mockData, settings: mockSettings, setData });
370
+
371
+ const priceInput = screen.getByLabelText(/price/i);
372
+ fireEvent.change(priceInput, { target: { value: '50' } });
373
+
374
+ expect(setData).toHaveBeenCalledWith({
375
+ pricing: { Price: 50 },
376
+ });
377
+ });
378
+ ```
379
+
380
+ ### Testing a stateful harness
381
+
382
+ Where a test keeps the context in local state so the component re-renders after each write, build `setData` with `mergingSetData` from `@seriousgaming/sdk/ui/testing`. It writes as the host does: objects merge recursively, arrays and scalars replace whole, and the paths in `deletes` are removed after the merge. A hand-rolled `{ ...previous, ...patch }` replaces whole capabilities, so a minimal write that is correct in the game fails the test:
383
+
384
+ ```tsx
385
+ import { mergingSetData } from '@seriousgaming/sdk/ui/testing';
386
+
387
+ const [data, setLocal] = useState(mockData);
388
+ const setData = mergingSetData(setLocal);
389
+
390
+ setData({}, { deletes: ['pinboard.Notes.p1'] }); // the note's key is gone, not null
391
+ ```
392
+
393
+ ### Troubleshooting
394
+
395
+ **"Invalid hook call" or "Cannot read properties of null (reading 'useContext')"**
396
+
397
+ Two copies of React are loaded: one from your package and one from the `file://` SDK dependency. Add `resolve.dedupe` to your `vitest.config.ts`, with the SDK packages beside React so a second copy of the SDK cannot hide the providers either:
398
+
399
+ ```typescript
400
+ export default defineConfig({
401
+ resolve: {
402
+ dedupe: [
403
+ 'react',
404
+ 'react-dom',
405
+ '@emotion/react',
406
+ '@emotion/styled',
407
+ '@seriousgaming/sdk',
408
+ '@seriousgaming/deck',
409
+ ],
410
+ },
411
+ // ...
412
+ });
413
+ ```
414
+
415
+ This is already included in the scaffold-generated `vitest.config.ts`.
416
+
417
+ **"Found multiple elements with the text: X" / DOM accumulation between tests**
418
+
419
+ React Testing Library cannot auto-detect Vitest's test lifecycle unless `globals: true` is set. Without it, the DOM is not cleaned up between tests. The scaffold-generated `test-setup.ts` handles this explicitly:
420
+
421
+ ```typescript
422
+ import { cleanup } from '@testing-library/react';
423
+ import { afterEach } from 'vitest';
424
+
425
+ afterEach(cleanup);
426
+ ```
427
+
428
+ If you see stale elements from a previous test appearing in a later test, check that this is present in your `test-setup.ts`.
429
+
430
+ ## Running tests
431
+
432
+ ```bash
433
+ # Run all tests once
434
+ pnpm run test
435
+
436
+ # Watch mode
437
+ pnpm run test -- --watch
438
+
439
+ # Specific file
440
+ pnpm run test -- ui/src/App.test.tsx
441
+ ```
442
+
443
+ ## The runtime your tests do not reproduce
444
+
445
+ An engine test imports `logic.ts` straight into Vitest, which runs under **Node**, with Node's full set of globals. Your game's production runtime is a **bare V8 isolate** with only the ECMAScript intrinsics (see [the language environment your logic runs in](./engine.md#the-language-environment-your-logic-runs-in)).
446
+
447
+ So a unit test cannot tell you that your logic is portable. This passes:
448
+
449
+ ```typescript
450
+ // logic.ts
451
+ export const Run: RunFunction<Context, Settings> = async ({ context }) => {
452
+ const snapshot = structuredClone(context); // works in Node, throws in production
453
+ // ...
454
+ };
455
+ ```
456
+
457
+ Its test goes green, because Node has `structuredClone`. The same tick throws `ReferenceError` once deployed.
458
+
459
+ Two things close the gap, and you should rely on both:
460
+
461
+ 1. **Lint.** A scaffolded package restricts the absent globals inside the directories that hold logic (`engine/`, `tick_controller/`, and each hook's, validator's and endpoint's `logic/`), so `pnpm run lint` flags the reference before you run anything. This is the fastest signal and the one to trust. The rule is deliberately **off** for `*.test.ts` files, because a test legitimately runs in Node
462
+ 2. **Exercise the logic through the real environment.** `pnpm run sdk dev` and `pnpm run sdk tick` both run logic in the restricted environment, so they reproduce the production failure. An [engine test](#engine-tests) drives the same execution path, which makes it a stronger portability signal than a direct unit test
463
+
464
+ The practical rule: unit-test your logic for correctness, and never treat a green unit test as evidence that it will run in production.
465
+
466
+ ### Writes are checked when they happen
467
+
468
+ A unit test that calls `Init` or `Run` directly checks only what you assert. The runtime also checks each write against the declarations, and two of those checks see nothing until the write happens:
469
+
470
+ - **A capability outside `writes`.** An engine, aggregate or hook that changes a capability it only reads, or never declared, fails the call
471
+ - **A shared capability from a per-team `Init` or `Run`.** In an aggregate engine, only `InitAggregate` and `RunAggregate` may write shared state. A per-team `Init` that does stops the session starting
472
+
473
+ Both compare the value before and after the call, so a write that leaves the value as it was passes. An engine that changes a read-only capability only in round 4, or only under one scenario, fails there and nowhere else. `sdk build` does not run your logic, and a one-tick `sdk tick --no-server` passes every path that tick did not take.
474
+
475
+ So drive every code path that writes through the runtime: an [engine test](#engine-tests) that plays each round, each branch and each scenario, not one tick.
476
+
477
+ ## Best practices
478
+
479
+ 1. **Test what the user sees**: assert on rendered text, input values, and visible state rather than internal component state.
480
+
481
+ 2. **Use `fireEvent.change` for controlled inputs**: `userEvent.type` won't work correctly when the mock `setData`/`updateSettings` doesn't update the displayed value between keystrokes.
482
+
483
+ 3. **Keep context data close to realistic**: populate `mockData` with values that reflect a real game state. Tests that rely on fallback/undefined values hide bugs.
484
+
485
+ 4. **Test edge cases**: zero values, maximum values, empty arrays, and missing optional capabilities.
486
+
487
+ 5. **One `renderApp` helper per test file**: accept `dataOverrides` and `configOverrides` parameters so individual tests can vary only the data they care about.
488
+
489
+ ## Engine tests
490
+
491
+ Unit tests check one function. An engine test runs the whole game in-process with `createTestSession` from `@seriousgaming/sdk-tools/client`: the session the dev server runs, with its hooks, validators, endpoints and timers, and offline stubs for the platform services. The [Engine testing](./engine-testing.md) guide is the reference: setup, the typed helpers, completion, endpoints, automatic ticks, and stubbing `generateText`.
492
+
493
+ ```bash
494
+ pnpm test:engine
495
+ ```
496
+
497
+ When a Playwright test fails, run the engine tests first. If one fails, the bug is in your logic; if they pass, it is in the UI or its connection to the host.
498
+
499
+ ## Browser E2E tests (Playwright)
500
+
501
+ Unit and integration tests cover logic and components in isolation. Browser E2E tests verify that your game UI works correctly inside the dev server, including iframe embedding, tick progression, and the full interaction flow.
502
+
503
+ The SDK provides selectors and helpers via a dedicated subpath export with no Playwright dependency of its own:
504
+
505
+ ```typescript
506
+ import { expect, test } from '@playwright/test';
507
+ import { DEV_SERVER, reinitialiseSession } from '@seriousgaming/sdk/e2e';
508
+
509
+ test.beforeEach(async () => {
510
+ await reinitialiseSession(); // a fresh session from the package, started
511
+ });
512
+
513
+ test('the value rises after a tick', async ({ page }) => {
514
+ // `?bare` drops the dev console's chrome, so this test is about the game.
515
+ await page.goto(DEV_SERVER.bare(DEV_SERVER.playerUrl(1)));
516
+
517
+ const frame = page.frameLocator(DEV_SERVER.iframeSelector('ui'));
518
+ await expect(frame.locator('#value')).toHaveText('0');
519
+
520
+ // Tick via the game's own control (the starter dashboard wires requestTick)
521
+ await frame.getByRole('button', { name: 'Next tick' }).click();
522
+ await expect(frame.locator('#value')).toHaveText('10');
523
+ });
524
+ ```
525
+
526
+ Anything a facilitator can do, a test can do without going through a surface:
527
+ `runCommand({ type: 'tick' })` advances the session, `getState()` reads the
528
+ whole projection, and `runCommand({ type: 'armFault', fault: 'conflict' })`
529
+ makes the 'Try again' path your UI offers reachable. `waitForState` waits for
530
+ something that happens after its command answers, and `advanceClock` moves the
531
+ time of a server started with a `FakeClock` on, so automatic ticks are tested without waiting.
532
+
533
+ When you create a project with `sdk new package`, a `playwright.config.ts`, the global setup that starts its dev server (`testing/e2e/global-setup.ts`, below) and a `testing/e2e/game.spec.ts` starter are generated automatically.
534
+
535
+ ### A dev server for browser tests
536
+
537
+ `startDevServer` starts the dev server in your test runner's process, isolated from the one you are working in, and resolves once it answers. It is what `sdk dev --isolated --port auto` runs, without spawning the CLI and parsing its output:
538
+
539
+ - It takes free ports, so it starts beside your own dev server rather than asking you to stop it
540
+ - It writes no lock file, so `sdk tick` in your terminal still reaches your own server
541
+ - It reads configuration from the package alone, ignoring what you last ran `sdk configure` with
542
+ - Uploads go to a temporary directory, removed by `stop()`
543
+
544
+ ```typescript
545
+ // testing/e2e/global-setup.ts, as the scaffold writes it
546
+ import { dirname } from 'path';
547
+
548
+ import { startDevServer } from '@seriousgaming/sdk-tools/client';
549
+ import type { FullConfig } from '@playwright/test';
550
+
551
+ export default async function globalSetup(config: FullConfig): Promise<() => Promise<void>> {
552
+ const server = await startDevServer({
553
+ packagePath: config.configFile ? dirname(config.configFile) : process.cwd(),
554
+ });
555
+ process.env.SERIOUSGAMING_DEV_URL = server.url;
556
+ return () => server.stop();
557
+ }
558
+ ```
559
+
560
+ `playwright.config.ts` names it as `globalSetup` and sets `use.baseURL` to `process.env.SERIOUSGAMING_DEV_URL`, which each worker reads after the setup has run. Stopping the server is a call in Playwright's own process, so the run ends when its tests do; a `webServer` running `pnpm run sdk dev` is stopped through `pnpm`, never reaches the server, and hangs.
561
+
562
+ It returns `{ url, assetUrl, stop() }`: `url` is the console and API, and `assetUrl` is where game surfaces are served from. The `@seriousgaming/sdk/e2e` helpers address the server `SERIOUSGAMING_DEV_URL` names, so a test passes them no options; pass `{ url: server.url }` where you hold the server yourself. It also takes `teams`, `participants`, `scenarioId`, `locale`, `clock` (a `FakeClock`) and `callbacks`, as [`createTestSession`](./engine-testing.md) does. Always call `stop()`: the server holds its ports and a file watcher open until then.
563
+
564
+ To drive it from the CLI as well, set `SERIOUSGAMING_DEV_URL` to its `url`.
565
+
566
+ See [Recipe: Test UIs end to end with Playwright](../recipes/e2e-testing.md) for the full setup guide, all available selectors, and common patterns.
567
+
568
+ ## Next steps
569
+
570
+ - [Building user interfaces](./ui.md) - Build and develop custom UIs
571
+ - [Creating engines](./engine.md) - Write a testable engine
572
+ - [Engine testing](./engine-testing.md) - Whole-game tests with `createTestSession`
573
+ - [Recipe: Test a user interface](../recipes/test-user-interface.md) - Step-by-step testing walkthrough
574
+ - [Recipe: Test a whole game in-process](../recipes/test-with-engine.md) - In-process engine test setup
575
+ - [Recipe: Test UIs end to end with Playwright](../recipes/e2e-testing.md) - Browser-based end-to-end tests