@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,477 @@
1
+ # Engine testing
2
+
3
+ Test your whole game in-process: no dev server, no browser, and no LLM API key required, because service callbacks are stubbed by default.
4
+
5
+ ## What you'll learn
6
+
7
+ - What `createTestSession` is and when to use it
8
+ - How engine tests fit alongside unit tests and Playwright tests
9
+ - How to write engine tests: ticking, asserting context, injecting decisions
10
+ - How to test hooks, validators, endpoints, undo and timers
11
+ - How service callbacks are stubbed, and how to override them or reach the platform
12
+ - How to move a test from `GameRunner`, which has been removed
13
+
14
+ ## What is createTestSession?
15
+
16
+ `createTestSession` starts the session the dev server runs, inside your test process. Everything a game does is there: `Init` and `Run`, hooks when somebody joins, validators on a participant's write, endpoints, undo, independent teams ticking on their own, the configuration step, and automatic ticks. You drive it with the same commands the console, the CLI and your surfaces send, and read it with the same state `sdk view --full` prints.
17
+
18
+ ```typescript
19
+ import { fileURLToPath } from 'url';
20
+
21
+ import { createTestSession } from '@seriousgaming/sdk-tools/client';
22
+
23
+ const session = await createTestSession({
24
+ packagePath: fileURLToPath(new URL('../..', import.meta.url)),
25
+ });
26
+
27
+ await session.send({ type: 'tick' });
28
+ const team = session.state().teams[0];
29
+
30
+ await session.close();
31
+ ```
32
+
33
+ ### Which runtime version your tests execute
34
+
35
+ `createTestSession` runs a copy of the SDK runtime that is compiled into
36
+ `@seriousgaming/sdk-tools` when that package is built, not
37
+ the `@seriousgaming/sdk` you have installed. Upgrading one
38
+ without the other therefore changes nothing about how your engine executes, so
39
+ keep the two on the same version and treat the tools version as the one that
40
+ decides behaviour. It also means that if you are narrowing down a defect by
41
+ changing versions, only the tools version is a variable.
42
+
43
+ ## The three testing layers
44
+
45
+ Engine tests sit between unit tests and Playwright tests:
46
+
47
+ | Layer | Tool | Tests | Speed | Needs |
48
+ | ---------- | ----------------- | ------------------------------------------------------------------- | ------- | ----------- |
49
+ | Unit | Vitest (jsdom) | Individual Init/Run functions, React components | ~1s | Nothing |
50
+ | **Engine** | **Vitest (node)** | **The whole game: ticks, decisions, hooks, validators, completion** | **~1s** | **Nothing** |
51
+ | Browser | Playwright | UI rendering, user input, iframe communication | ~30s+ | Dev server |
52
+
53
+ **When a Playwright test fails**, run engine tests first to isolate the problem:
54
+
55
+ - Engine test fails → bug is in your game logic
56
+ - Engine test passes → bug is in your UI component or iframe communication
57
+
58
+ ## Setup
59
+
60
+ When you create a package with `sdk new package`, the engine test scaffold is included automatically:
61
+
62
+ ```
63
+ testing/engine/game.engine.test.ts ← starter engine test
64
+ vitest.engine.config.ts ← node environment config
65
+ ```
66
+
67
+ And your `package.json` includes:
68
+
69
+ ```json
70
+ "test:engine": "vitest run --config vitest.engine.config.ts"
71
+ ```
72
+
73
+ For an existing project, see the [Engine Testing recipe](../recipes/test-with-engine.md).
74
+
75
+ ## Writing engine tests
76
+
77
+ ### Basic structure
78
+
79
+ ```typescript
80
+ // testing/engine/game.engine.test.ts
81
+ import { fileURLToPath } from 'url';
82
+
83
+ import { createTestSession, type TestSession } from '@seriousgaming/sdk-tools/client';
84
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
85
+
86
+ import type { Context, SharedContext } from '../../engine/types';
87
+
88
+ const PACKAGE_PATH = fileURLToPath(new URL('../..', import.meta.url));
89
+
90
+ describe('mygame', () => {
91
+ let session: TestSession<Context, SharedContext>;
92
+
93
+ beforeEach(async () => {
94
+ session = await createTestSession<Context, SharedContext>({ packagePath: PACKAGE_PATH });
95
+ });
96
+
97
+ afterEach(async () => {
98
+ await session.close();
99
+ });
100
+
101
+ it('initialises with expected defaults', () => {
102
+ expect(session.team(0).context.main).toEqual({ Delta: 0, Total: 0 });
103
+ });
104
+ });
105
+ ```
106
+
107
+ The two type parameters are the engine's generated `Context` and `SharedContext`. They type what `state()`, `team()` and `shared` return and what `setData` and `setSharedData` accept, so a test reads `session.team(0).context.main.Total` with no cast. Nothing checks them against the package, as nothing checks a cast; leave them out and the contexts are untyped records. A game whose hooks keep capabilities the engine does not declare can intersect their `Context` types too.
108
+
109
+ `beforeEach` starts a fresh session for every test, and `afterEach` stops its timers. Nothing is read from or written to `.seriousgaming-sdk/`, so there are no files to clean up, and a test does not depend on what you last ran `sdk configure` with.
110
+
111
+ ### Ticking and asserting
112
+
113
+ `tick()` ticks as the facilitator does, and answers with how it settled. A tick that succeeds is `{ ok: true }`; one the session refuses, such as a tick past the game's limit, is `{ ok: false, outcome }`. A refusal is an answer, not a throw, so a test can assert on it:
114
+
115
+ ```typescript
116
+ it('adds Delta to Total on each tick', async () => {
117
+ await session.setData(0, { main: { Delta: 5 } });
118
+
119
+ expect(await session.tick()).toEqual({ ok: true, committedAtTick: 1 });
120
+
121
+ expect(session.state().session.completedTicks).toBe(1);
122
+ expect(session.team(0).context.main.Total).toBe(5);
123
+ });
124
+ ```
125
+
126
+ Every helper is a command underneath, and `send` takes any command directly: `session.tick()` is `session.send({ type: 'tick' })`.
127
+
128
+ `state()` is the whole session: `session.completedTicks`, `session.complete`, every team with its `context`, `completedTicks` and `participants`, the shared store, the timer and `settings`, every settings resource's resolved values keyed by identifier.
129
+
130
+ ### A note on `instanceof` and engine-produced values
131
+
132
+ Engine logic is evaluated in its own JavaScript environment, matching the
133
+ production runtime. Values it builds therefore answer `instanceof` and
134
+ `toBeInstanceOf` against that environment's constructors, not your test file's,
135
+ so an assertion written this way can fail on a value that is perfectly correct:
136
+
137
+ ```typescript
138
+ // Fails on an array your engine built, even though it is a real array
139
+ expect(context.main.Segments).toBeInstanceOf(Array);
140
+ ```
141
+
142
+ Assert on the shape instead, which holds wherever the value was built:
143
+
144
+ ```typescript
145
+ expect(Array.isArray(context.main.Segments)).toBe(true);
146
+ expect(context.main.Segments).toEqual(['seg_a', 'seg_b']);
147
+ ```
148
+
149
+ The same applies to `instanceof Array` and `instanceof Object` inside your own
150
+ helpers. `Array.isArray`, `typeof`, and `toEqual` are all safe.
151
+
152
+ ### Injecting decisions
153
+
154
+ `setData(team, patch)` merges a patch into one team's context, by the team's stable index, as a player's surface does: as the team's first participant, through the `ui` surface. So the write is held to that surface's `writes`, and your validators judge it:
155
+
156
+ ```typescript
157
+ await session.setData(0, { pricing: { Price: 10 } });
158
+ await session.tick();
159
+
160
+ const result = await session.setData(0, { addons: { ExtraShift: true, MarketingPush: true } });
161
+ expect(result).toMatchObject({ ok: false, outcome: { kind: 'validation-rejected' } });
162
+ ```
163
+
164
+ Options choose another writer: `participantId` for a participant other than the first, `ui` for another player surface, and `deletes` for paths to remove after the patch merges. `as: 'system'` writes as the CLI does instead, trusted and ungated (see [what bypasses validation](./validators.md#what-bypasses-validation)), which suits arranging a board the test then plays on:
165
+
166
+ ```typescript
167
+ await session.setData(0, { progress: { Score: 45 } }, { as: 'system' });
168
+ ```
169
+
170
+ `requestTick(team)` asks for the next tick as the team's first participant, so your validators' `RequestTick` gates judge it, where `tick()` is the facilitator's and passes them:
171
+
172
+ ```typescript
173
+ expect(await session.requestTick(0)).toMatchObject({
174
+ ok: false,
175
+ outcome: { kind: 'validation-rejected' },
176
+ });
177
+ ```
178
+
179
+ A shared capability is written with `setSharedData`, and every team sees it. With no options it writes as the CLI does; pass `ui: 'session'` to write as the session surface does, held to its `writes`:
180
+
181
+ ```typescript
182
+ await session.setSharedData({ noticeboard: { Message: 'Break at 3' } }, { ui: 'session' });
183
+ expect(session.shared.context.noticeboard.Message).toBe('Break at 3');
184
+ ```
185
+
186
+ `shared` holds the store under `context`, as a session surface's `sessionData.shared` does, so an assertion moves between a test and the surface unchanged. `state().shared` is the store itself.
187
+
188
+ ### Testing game completion
189
+
190
+ A game with a `maxTicks` limit refuses the tick after it. A game whose tick controller returns `complete: true` reports it on `state().session.complete`, and refuses the next tick:
191
+
192
+ ```typescript
193
+ it('completes when the target is reached', async () => {
194
+ await session.setData(0, { progress: { Score: 45 } }, { as: 'system' });
195
+ await session.tick();
196
+
197
+ expect(session.state().session.complete).toBe(true);
198
+ expect(await session.tick()).toMatchObject({
199
+ ok: false,
200
+ outcome: { kind: 'tick-failed' },
201
+ });
202
+ });
203
+ ```
204
+
205
+ ### Teams and participants
206
+
207
+ Pass `teams` for a multi-team game; the default is the game's `teams.min`. In an independent game, `tick()` advances every team and `tick({ team: 1 })` advances one. In a shared-state game every team advances together.
208
+
209
+ Every team starts with one participant, or the game's `perTeam.minParticipants`. Pass `participants` to choose, as one count or one per team. Zero is allowed:
210
+
211
+ ```typescript
212
+ const session = await createTestSession({
213
+ packagePath: PACKAGE_PATH,
214
+ teams: 2,
215
+ participants: [3, 0], // three on team 1, nobody on team 2
216
+ });
217
+
218
+ // Your participantJoin hooks ran for the three.
219
+ await session.send({ type: 'addParticipant', teamId: 'team-2' }); // and now for one more
220
+ await session.send({ type: 'moveParticipant', participantId, teamId: 'team-1' });
221
+ ```
222
+
223
+ ### Endpoints
224
+
225
+ Call one with `callEndpoint`. Leave out `uiIdentifier` and the call is system-trusted, as the CLI's is. Name a surface (`uiIdentifier: 'ui'`) to hold the call to that surface's `endpoints` list, as a player's would be. An identifier the package does not declare, the empty string included, is refused rather than trusted: the call answers with a `denied` failure.
226
+
227
+ ```typescript
228
+ const result = await session.send({
229
+ type: 'callEndpoint',
230
+ endpointIdentifier: 'openround',
231
+ request: { round: 4 },
232
+ });
233
+ expect(result).toEqual({ type: 'endpoint', response: { previous: 0, current: 4 } });
234
+ ```
235
+
236
+ A failed call is an answer too, not a throw: `{ type: 'endpointFailure', failure }`, where `failure` is the same `EndpointFailure` a surface's `EndpointError` carries. Assert on its kind and on what had already landed:
237
+
238
+ ```typescript
239
+ const refused = await session.send({
240
+ type: 'callEndpoint',
241
+ endpointIdentifier: 'openround',
242
+ request: { round: 'four' },
243
+ });
244
+ expect(refused).toMatchObject({
245
+ type: 'endpointFailure',
246
+ failure: { outcome: { kind: 'invalid-request' }, applied: [] },
247
+ });
248
+ ```
249
+
250
+ ### Undo, reset and starting again
251
+
252
+ ```typescript
253
+ await session.send({ type: 'undo' }); // take back the last tick
254
+ await session.send({ type: 'reset' }); // back to tick 0, keeping teams and scenario
255
+ await session.send({ type: 'reinitialise' }); // start again from the package
256
+ ```
257
+
258
+ ### Automatic ticks
259
+
260
+ Timers run on the clock you pass. Pass a `FakeClock` and move it with `advanceClock`, which runs every tick that falls due on the way, so a test says how much time passes rather than waiting for it:
261
+
262
+ ```typescript
263
+ import { FakeClock } from '@seriousgaming/sdk-tools/client';
264
+
265
+ const session = await createTestSession({ packagePath: PACKAGE_PATH, clock: new FakeClock(0) });
266
+ await session.send({ type: 'advanceClock', byMs: 180_000 }); // three one-minute ticks
267
+ expect(session.state().session.completedTicks).toBe(3);
268
+ ```
269
+
270
+ `pauseTimer`, `resumeTimer` and `extendTick` work as they do from a session UI.
271
+
272
+ The same clock is what logic reads as `meta.now`, so a `FakeClock` also pins the timestamps a game records: a session started on `new FakeClock(Date.UTC(2026, 0, 1))` hands its first `Init` exactly that instant.
273
+
274
+ ### Pinning a scenario
275
+
276
+ By default the session resolves the game's default scenario when one is declared, exactly as the dev server does. Your assertions then silently track whatever scenario is currently the default: if someone later marks a different one as default, your expected values change without any test edit. Pin it:
277
+
278
+ ```typescript
279
+ // A specific scenario (it must be listed in the game's `scenarios`)
280
+ await createTestSession({ packagePath: PACKAGE_PATH, scenarioId: 'easy_mode' });
281
+
282
+ // Or opt out of the default scenario and test the settings defaults
283
+ await createTestSession({ packagePath: PACKAGE_PATH, scenarioId: null });
284
+ ```
285
+
286
+ ### The configuration step
287
+
288
+ A game with a `setup` slot starts at its configuration step. `createTestSession` presses Play for you, so a tick has a game to run. Pass `play: false` to test the step itself, then send `updateSettings` (keyed by settings identifier, for example `{ type: 'updateSettings', payload: { engine: { BaseDemand: 200 } } }`) and `play` as the setup screen would. Settings are fixed from Play onwards, so `state().settings` after `play` is what every tick reads.
289
+
290
+ ## Service callbacks are stubbed
291
+
292
+ Tests are offline by default, whether they run a whole session or call one logic function. A test session, and the arguments `logicArgs` builds for a unit test (see [Testing the Run function](./testing.md#testing-the-run-function)), answer the service callbacks from the same deterministic stubs, so a test runs without a network or a platform key and gets the same answer every run:
293
+
294
+ - **`generateText`** answers `[stub <hash>] <excerpt>`: an eight-digit hex hash of the whole request (prompt, system prompt, tier and the rest), then the first 60 characters of the prompt, or of the last `user` message's text, with whitespace collapsed and `...` appended when cut. A request with a `jsonSchema` is answered with the smallest JSON value that satisfies the schema: each property's `const`, first `enum` member or `default` where it has one, `'[stub]'` for a string, the `minimum` or `0` for a number, `false`, and `minItems` entries for an array
295
+ - **`generateSpeech`** stores 100ms of silence as a WAV blob and returns `{ reference: 'blob:<id>', mimeType: 'audio/wav', durationMs: 100, charactersBilled: 0 }`, so `readAsset` resolves it as it would real audio
296
+ - **`extractText`** answers `{ content: '[stub] text of <reference>', mimeType: 'text/plain' }`, without reading the file
297
+
298
+ With no session behind them, the arguments `logicArgs` builds stub the rest as well: `writeBlob` stores in memory and returns `blob:stub-<n>`, `readAsset` reads back only a blob written in the same test (and rejects anything else, `asset:` references included), and `getPastState` answers `[]`.
299
+
300
+ | Export | From | Use it to |
301
+ | --------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
302
+ | `stubGenerateText(request)` | `@seriousgaming/sdk/testing`, `@seriousgaming/sdk-tools/client` | Compute the stub's answer, so a test can assert on it exactly |
303
+ | `createStubCallbacks()` | `@seriousgaming/sdk/testing` | Get a fresh set of every stubbed callback, with its own blob store, as `logicArgs` fills them |
304
+ | `createStubServices(packagePath)` | `@seriousgaming/sdk-tools/client` | Get the three service stubs a test session uses, which store speech in the package's blob store |
305
+
306
+ ### Overriding a callback
307
+
308
+ When a test needs a particular answer, override that callback alone; the others stay stubbed. In a test session, pass `callbacks`:
309
+
310
+ ```typescript
311
+ const session = await createTestSession({
312
+ packagePath: PACKAGE_PATH,
313
+ callbacks: {
314
+ generateText: async () => ({
315
+ content: [{ type: 'text' as const, text: 'mock response' }],
316
+ stopReason: 'endTurn' as const,
317
+ }),
318
+ },
319
+ });
320
+ ```
321
+
322
+ In a unit test, pass it to `logicArgs` the same way, which makes a spy easy to assert on:
323
+
324
+ ```typescript
325
+ import { logicArgs } from '@seriousgaming/sdk/testing';
326
+ import { expect, it, vi } from 'vitest';
327
+
328
+ it('includes the LLM report in context', async () => {
329
+ const generateText = vi.fn().mockResolvedValue({
330
+ content: [{ type: 'text' as const, text: 'Mocked report.' }],
331
+ stopReason: 'endTurn' as const,
332
+ });
333
+
334
+ const result = await Run(
335
+ logicArgs.run({ context, settings: defaultSettings, callbacks: { generateText } })
336
+ );
337
+
338
+ expect(generateText).toHaveBeenCalledOnce();
339
+ expect(result.capabilities.report.Content).toBe('Mocked report.');
340
+ });
341
+ ```
342
+
343
+ For logic that expects structured JSON output, inspect the options to return the right format:
344
+
345
+ ```typescript
346
+ import type { LLMGenerateOptions } from '@seriousgaming/sdk';
347
+
348
+ const mockGenerateText = async (options: LLMGenerateOptions | string) => {
349
+ if (typeof options === 'object' && options.jsonSchema) {
350
+ return {
351
+ content: [
352
+ { type: 'text' as const, text: JSON.stringify({ score: 75, status: 'Good' }) },
353
+ ],
354
+ stopReason: 'endTurn' as const,
355
+ };
356
+ }
357
+ return {
358
+ content: [{ type: 'text' as const, text: 'Tick summary: performance is on track.' }],
359
+ stopReason: 'endTurn' as const,
360
+ };
361
+ };
362
+ ```
363
+
364
+ `callbacks` is typed as `Partial<SignatureCallbacks>`, so you only override what your logic uses. In a test session, anything you don't override keeps its default (a stub, or the platform's with `services: 'platform'`), and the override reaches hooks, validators and endpoints as well as the engine. A callback the resource did not declare in `uses` still throws there, override or not.
365
+
366
+ ## Calling the platform
367
+
368
+ To send the service calls to the Serious Gaming platform, pass `services: 'platform'`:
369
+
370
+ ```typescript
371
+ const session = await createTestSession({ packagePath: PACKAGE_PATH, services: 'platform' });
372
+ ```
373
+
374
+ The session then reads the platform credentials from the same sources as the CLI: the `SERIOUSGAMING_API_KEY` and `SERIOUSGAMING_API_HOST` environment variables, a `.env` file in the package root, and the global settings file. You can also pass them explicitly with `platform: { apiKey, apiHost }`. Keep such tests out of CI unless it has a key.
375
+
376
+ ## Running engine tests
377
+
378
+ ```bash
379
+ # Run all engine tests once
380
+ pnpm test:engine
381
+
382
+ # Watch mode (re-runs on file changes)
383
+ pnpm exec vitest --config vitest.engine.config.ts
384
+
385
+ # Run a specific file
386
+ pnpm exec vitest --config vitest.engine.config.ts testing/engine/game.engine.test.ts
387
+ ```
388
+
389
+ Engine tests run in milliseconds per test because there are no subprocesses, no HTTP, and no browser startup.
390
+
391
+ ## CI integration
392
+
393
+ Engine tests require nothing beyond Node.js: no dev server, no browser, and no platform API key, because the service callbacks are stubbed unless a test asks for the platform. Add them to your CI pipeline alongside unit tests:
394
+
395
+ ```yaml
396
+ # Example GitHub Actions step
397
+ - name: Run tests
398
+ run: |
399
+ pnpm test:run # unit and component tests
400
+ pnpm test:engine # engine tests
401
+ ```
402
+
403
+ Run Playwright tests in a separate job or stage, since they need a browser and the dev server.
404
+
405
+ ## API reference
406
+
407
+ ### `createTestSession(options)`
408
+
409
+ | Option | Type | Description |
410
+ | -------------- | ----------------------------- | ------------------------------------------------------------------------------------ |
411
+ | `packagePath` | `string` | Absolute path to the project root (where `seriousgaming.sdk.json` lives) |
412
+ | `teams` | `number` | Teams to start with. Defaults to the game's `teams.min` |
413
+ | `participants` | `number \| number[]` | Participants per team, one count or one per team. Zero allowed. Defaults to one each |
414
+ | `scenarioId` | `string \| null` | Scenario to start on. Omit for the default; `null` for the settings defaults |
415
+ | `locale` | `string` | Session locale |
416
+ | `callbacks` | `Partial<SignatureCallbacks>` | Replace callbacks your logic receives, such as `generateText` |
417
+ | `services` | `'stub' \| 'platform'` | Where `generateText`, `generateSpeech` and `extractText` go. Default `'stub'` |
418
+ | `clock` | `Clock` | The clock timers run on. Pass a `FakeClock` to move time by hand |
419
+ | `play` | `boolean` | Press Play on a game with a configuration step. Default `true` |
420
+ | `platform` | `{ apiKey?, apiHost? }` | Platform credentials, used with `services: 'platform'` |
421
+
422
+ `createTestSession<Context, Shared>(options)` takes two optional type parameters, the engine's generated `Context` and `SharedContext`, and returns a `TestSession<Context, Shared>`.
423
+
424
+ ### `TestSession`
425
+
426
+ | Member | Returns | Description |
427
+ | -------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
428
+ | `send(command)` | `Promise<MutationResult>` or `Promise<QueryResult>` | Run a command as the dev server would. A refusal is an answer, not a throw |
429
+ | `state()` | `TestSessionState<Context, Shared>` | The whole session now, each team's context typed |
430
+ | `team(index)` | `TestSessionTeam<Context>` | One team by stable index: `context`, `participants`, `completedTicks` and the rest. Throws for an unknown one |
431
+ | `shared` | `{ context: Shared }` | The shared store now, under `context` as a session surface's `sessionData.shared` holds it |
432
+ | `tick(options?)` | `Promise<MutationResult>` | A facilitator's tick. `{ team }` ticks one team in independent mode |
433
+ | `requestTick(team, options?)` | `Promise<MutationResult>` | A participant's request, which `RequestTick` gates judge. `{ participantId }` to choose who asks |
434
+ | `setData(team, patch, options?)` | `Promise<MutationResult>` | A participant's write through `ui`, or `{ as: 'system' }` for a trusted one. Also `deletes`, `participantId`, `ui` |
435
+ | `setSharedData(patch, options?)` | `Promise<MutationResult>` | A write to the shared store. `{ ui }` holds it to that session surface's `writes`; also `deletes` |
436
+ | `settled()` | `Promise<void>` | Resolves once everything queued has run, including a timer's tick |
437
+ | `close()` | `Promise<void>` | Stop the session's timers. Call it in `afterEach` |
438
+
439
+ `Command`, `MutationCommand`, `QueryCommand`, `QueryResult`, `MutationResult`, `StateProjection`, `TestSessionState`, `TestSessionTeam`, `ContextPatch`, `SetDataOptions`, `SetSharedDataOptions`, `FakeClock`, `ParticipantCounts`, `TestServices`, `createStubServices` and `stubGenerateText` are exported from the same module.
440
+
441
+ To call one logic function directly instead, with no session, build its arguments with `logicArgs` from `@seriousgaming/sdk/testing` (see [Testing](./testing.md#testing-the-run-function)).
442
+
443
+ ## Moving from GameRunner
444
+
445
+ `GameRunner` and `MultiTeamGameRunner` were deprecated in 0.6.0 and have been removed. They drove the engine alone, so a test written with them never saw a hook, a validator, an endpoint, a participant, undo, a per-team tick or a timer. Each call has a `createTestSession` equivalent:
446
+
447
+ | `GameRunner` | `createTestSession` |
448
+ | --------------------------------------------------------- | ---------------------------------------------------------------- |
449
+ | `GameRunner.create(options)` then `init()` | `createTestSession(options)`: it is ready at tick 0 |
450
+ | `MultiTeamGameRunner.create({ teamCount })` | `createTestSession({ teams })`, in either interaction mode |
451
+ | `runner.tick()` | `session.tick()`, then read `session.team(0)` |
452
+ | `runner.update([{ op: 'replace', path: '/a/B', value }])` | `session.setData(0, { a: { B: value } }, { as: 'system' })` |
453
+ | `runner.update(patches, { teamId: 'team-2' })` | the same, with team `1` |
454
+ | `runner.updateShared(patches)` | `session.setSharedData(payload)` |
455
+ | `runner.getContext()` / `getContext('team-2')` | `session.team(0).context` / `session.team(1).context` |
456
+ | `runner.getTick()` | `session.state().session.completedTicks` |
457
+ | `runner.complete()`, and `tick()` throwing | `session.state().session.complete`, and a refused tick |
458
+ | `runner.init()` to start again | `session.send({ type: 'reinitialise' })`, or `undo` for one tick |
459
+ | `persist: true` | Not offered: a test session never touches `.seriousgaming-sdk/` |
460
+
461
+ Two behaviours differ, both towards what the dev server does. A tick past the limit comes back as `{ ok: false }` rather than throwing, and a multi-team game runs in its declared interaction mode, where `MultiTeamGameRunner` always ran shared-state.
462
+
463
+ ## Best practices
464
+
465
+ 1. **Leave the services stubbed unless a test is about the model**: the stubs keep tests fast, deterministic, and runnable in CI without an API key. Override `generateText` when a test needs a particular answer
466
+ 2. **Start a fresh session in `beforeEach`, and close it in `afterEach`**: each test starts clean, and no timer outlives it
467
+ 3. **Assert on computed values, not just the value you set**: the point of an engine test is that the game processes inputs correctly
468
+ 4. **Write a decision as a participant when a validator should judge it**: `setData` does by default; `as: 'system'` is the facilitator's and skips validators
469
+ 5. **Pin `scenarioId` when the game declares scenarios**: explicit pinning (or `null` for the settings defaults) keeps assertions stable when the default scenario changes
470
+ 6. **Run engine tests before Playwright**: if an engine test fails, there is no point running browser tests for the same scenario
471
+
472
+ ## Related
473
+
474
+ - [Engine testing recipe](../recipes/test-with-engine.md) - step-by-step setup for an existing project
475
+ - [Testing guide](./testing.md) - unit tests for the engine and React components
476
+ - [E2E testing with Playwright](../recipes/e2e-testing.md) - browser-based UI tests
477
+ - [Drive a game from the CLI](../cli-simulation-testing.md) - Test through `sdk tick` and `sdk view` against a live session