@dreamboard-games/cli 0.1.30-alpha.1 → 0.1.30-alpha.2

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 (114) hide show
  1. package/README.md +179 -22
  2. package/dist/{chunk-C6UAT6EH.js → chunk-N7XPNNUI.js} +9 -12
  3. package/dist/chunk-N7XPNNUI.js.map +1 -0
  4. package/dist/chunk-SEGVTWSK.js +44 -0
  5. package/dist/{chunk-RS7UXJZV.js → chunk-TAQKH67O.js} +21300 -35881
  6. package/dist/chunk-TAQKH67O.js.map +1 -0
  7. package/dist/{global-config-AGFBDFYD.js → global-config-S4ZIPECE.js} +3 -3
  8. package/dist/index.js +415 -37
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal.js +3 -4
  11. package/dist/{agent-verifier/keychain-backend-TNOPQV3Z.mjs → keychain-backend-HDF4TZDL.js} +2 -1
  12. package/dist/{agent-verifier/prompt-3BAINGAQ.mjs → prompt-NDV3AE5L.js} +2 -1
  13. package/package.json +6 -6
  14. package/skills/dreamboard/references/building-your-first-game.md +510 -0
  15. package/skills/dreamboard/references/cli.md +104 -0
  16. package/skills/dreamboard/references/game-interface.md +548 -0
  17. package/skills/dreamboard/references/manifest-authoring.md +597 -0
  18. package/skills/dreamboard/references/quickstart.md +66 -0
  19. package/skills/dreamboard/references/reducer.md +864 -0
  20. package/skills/dreamboard/references/rule-authoring.md +147 -0
  21. package/skills/dreamboard/references/testing.md +249 -0
  22. package/skills/dreamboard/scripts/events-extract.mjs +218 -0
  23. package/dist/agent-verifier/agent-workspace-verifier.mjs +0 -227
  24. package/dist/agent-verifier/chunk-2E5P5NWG.mjs +0 -835
  25. package/dist/agent-verifier/chunk-2GBBP27W.mjs +0 -301
  26. package/dist/agent-verifier/chunk-2QMNAVV4.mjs +0 -14522
  27. package/dist/agent-verifier/chunk-2SZHMP6F.mjs +0 -264
  28. package/dist/agent-verifier/chunk-4WD3YU2E.mjs +0 -166
  29. package/dist/agent-verifier/chunk-54TAYXUD.mjs +0 -12
  30. package/dist/agent-verifier/chunk-6A5HRJMQ.mjs +0 -3174
  31. package/dist/agent-verifier/chunk-6UUJEYDV.mjs +0 -213
  32. package/dist/agent-verifier/chunk-7653FPGJ.mjs +0 -381
  33. package/dist/agent-verifier/chunk-7E65UQLY.mjs +0 -38
  34. package/dist/agent-verifier/chunk-BVVNBJM4.mjs +0 -221
  35. package/dist/agent-verifier/chunk-CEDUHGNH.mjs +0 -74
  36. package/dist/agent-verifier/chunk-CEQ2VJWN.mjs +0 -149
  37. package/dist/agent-verifier/chunk-CFU5EWIC.mjs +0 -69
  38. package/dist/agent-verifier/chunk-CJEEA6NJ.mjs +0 -730
  39. package/dist/agent-verifier/chunk-EIQWDQWJ.mjs +0 -186
  40. package/dist/agent-verifier/chunk-EOQIV6PS.mjs +0 -649
  41. package/dist/agent-verifier/chunk-HBNDKQT5.mjs +0 -8381
  42. package/dist/agent-verifier/chunk-HJFQDSTU.mjs +0 -225
  43. package/dist/agent-verifier/chunk-JH22JNYD.mjs +0 -1681
  44. package/dist/agent-verifier/chunk-LI3ZR3BI.mjs +0 -41
  45. package/dist/agent-verifier/chunk-LM3OZLZG.mjs +0 -48
  46. package/dist/agent-verifier/chunk-MINCYHXN.mjs +0 -106
  47. package/dist/agent-verifier/chunk-MRCUP5SW.mjs +0 -128
  48. package/dist/agent-verifier/chunk-RBDDIIPM.mjs +0 -19
  49. package/dist/agent-verifier/chunk-SHUMAVAP.mjs +0 -59
  50. package/dist/agent-verifier/chunk-SYPLYRGB.mjs +0 -2812
  51. package/dist/agent-verifier/chunk-U6OJN7XS.mjs +0 -8092
  52. package/dist/agent-verifier/chunk-VYJTHSYR.mjs +0 -44
  53. package/dist/agent-verifier/chunk-XYDL7GY6.mjs +0 -10
  54. package/dist/agent-verifier/compile-5QSPIOUT.mjs +0 -313
  55. package/dist/agent-verifier/global-config-WX3ZZIVU.mjs +0 -17
  56. package/dist/agent-verifier/local-files-MTPLP62S.mjs +0 -46
  57. package/dist/agent-verifier/local-typecheck-QFYYAZOK.mjs +0 -9
  58. package/dist/agent-verifier/materialize-workspace-FKALAE2T.mjs +0 -90
  59. package/dist/agent-verifier/project-state-7GR6BQTQ.mjs +0 -32
  60. package/dist/agent-verifier/reducer-bundle-preflight-C73LEXI2.mjs +0 -23
  61. package/dist/agent-verifier/reducer-contract-preflight-22X7DSZW.mjs +0 -10
  62. package/dist/agent-verifier/reducer-native-test-harness-GMWBUISX.mjs +0 -53
  63. package/dist/agent-verifier/static-scaffold-AJMZZQWS.mjs +0 -28
  64. package/dist/agent-verifier/sync-3DUQH32H.mjs +0 -594
  65. package/dist/agent-verifier/test-P4U5INTD.mjs +0 -356
  66. package/dist/agent-verifier/testing-5K2BJYF2.mjs +0 -674
  67. package/dist/agent-verifier/workspace-codegen-JDZJRSDV.mjs +0 -11
  68. package/dist/agent-verifier/workspace-dependencies-HZ6VVS4G.mjs +0 -14
  69. package/dist/chunk-2H7UOFLK.js +0 -11
  70. package/dist/chunk-7FOO4AJI.js +0 -50
  71. package/dist/chunk-7FOO4AJI.js.map +0 -1
  72. package/dist/chunk-C6UAT6EH.js.map +0 -1
  73. package/dist/chunk-RS7UXJZV.js.map +0 -1
  74. package/dist/internal.d.ts +0 -311
  75. package/dist/keychain-backend-JHTXAKWC.js +0 -135
  76. package/dist/prompt-GMZABCJC.js +0 -756
  77. package/dist/runtime-packages/ui-host-runtime/src/actor-principal.ts +0 -71
  78. package/dist/runtime-packages/ui-host-runtime/src/browser-interaction.ts +0 -139
  79. package/dist/runtime-packages/ui-host-runtime/src/components/host-controls.tsx +0 -374
  80. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback-toaster.tsx +0 -266
  81. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback.tsx +0 -212
  82. package/dist/runtime-packages/ui-host-runtime/src/components/host-primitives.tsx +0 -271
  83. package/dist/runtime-packages/ui-host-runtime/src/components/host-session-metadata.tsx +0 -135
  84. package/dist/runtime-packages/ui-host-runtime/src/components/index.ts +0 -5
  85. package/dist/runtime-packages/ui-host-runtime/src/components/perf-overlay.tsx +0 -194
  86. package/dist/runtime-packages/ui-host-runtime/src/gameplay-authority-transport.ts +0 -626
  87. package/dist/runtime-packages/ui-host-runtime/src/host-controls.tsx +0 -1
  88. package/dist/runtime-packages/ui-host-runtime/src/host-feedback.tsx +0 -1
  89. package/dist/runtime-packages/ui-host-runtime/src/host-session-transport.ts +0 -294
  90. package/dist/runtime-packages/ui-host-runtime/src/index.ts +0 -3
  91. package/dist/runtime-packages/ui-host-runtime/src/logger.ts +0 -11
  92. package/dist/runtime-packages/ui-host-runtime/src/perf.ts +0 -324
  93. package/dist/runtime-packages/ui-host-runtime/src/plugin-bridge.ts +0 -195
  94. package/dist/runtime-packages/ui-host-runtime/src/plugin-health-check.ts +0 -138
  95. package/dist/runtime-packages/ui-host-runtime/src/plugin-messages.ts +0 -159
  96. package/dist/runtime-packages/ui-host-runtime/src/plugin-session-gateway.ts +0 -551
  97. package/dist/runtime-packages/ui-host-runtime/src/runtime/index.ts +0 -13
  98. package/dist/runtime-packages/ui-host-runtime/src/screenshot/projection-to-snapshot.ts +0 -122
  99. package/dist/runtime-packages/ui-host-runtime/src/screenshot/static-store-api.ts +0 -26
  100. package/dist/runtime-packages/ui-host-runtime/src/session-ingress-controller.ts +0 -583
  101. package/dist/runtime-packages/ui-host-runtime/src/session-ingress.ts +0 -219
  102. package/dist/runtime-packages/ui-host-runtime/src/session-live-runtime.ts +0 -117
  103. package/dist/runtime-packages/ui-host-runtime/src/session-model.ts +0 -431
  104. package/dist/runtime-packages/ui-host-runtime/src/session-projection.ts +0 -211
  105. package/dist/runtime-packages/ui-host-runtime/src/session-recovery.ts +0 -80
  106. package/dist/runtime-packages/ui-host-runtime/src/session-state-reducer.ts +0 -1034
  107. package/dist/runtime-packages/ui-host-runtime/src/sse-manager.ts +0 -416
  108. package/dist/runtime-packages/ui-host-runtime/src/unified-session-store.ts +0 -184
  109. package/dist/testing-KLSV6CPJ.js +0 -674
  110. package/dist/testing-KLSV6CPJ.js.map +0 -1
  111. /package/dist/{chunk-2H7UOFLK.js.map → chunk-SEGVTWSK.js.map} +0 -0
  112. /package/dist/{global-config-AGFBDFYD.js.map → global-config-S4ZIPECE.js.map} +0 -0
  113. /package/dist/{keychain-backend-JHTXAKWC.js.map → keychain-backend-HDF4TZDL.js.map} +0 -0
  114. /package/dist/{prompt-GMZABCJC.js.map → prompt-NDV3AE5L.js.map} +0 -0
@@ -1,3174 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- contractFingerprint,
4
- createReducerBundle,
5
- zod_exports
6
- } from "./chunk-HBNDKQT5.mjs";
7
- import {
8
- bundleTypeScriptModuleText,
9
- importTypeScriptModule
10
- } from "./chunk-CEDUHGNH.mjs";
11
- import {
12
- STALE_CONTRACT_ARTIFACT_CODE,
13
- isStaleContractArtifactError,
14
- toDreamboardApiError
15
- } from "./chunk-BVVNBJM4.mjs";
16
- import {
17
- loadManifest
18
- } from "./chunk-EIQWDQWJ.mjs";
19
- import {
20
- REDUCER_TESTING_TYPES_WRAPPER_CONTENT,
21
- buildReducerTestingContractContent
22
- } from "./chunk-2GBBP27W.mjs";
23
- import {
24
- createGameplayCapability,
25
- createProjectSession,
26
- createProjectSessionFromReducerSnapshot,
27
- getSessionSnapshot,
28
- hashContent,
29
- startGame
30
- } from "./chunk-SYPLYRGB.mjs";
31
- import {
32
- external_exports
33
- } from "./chunk-2QMNAVV4.mjs";
34
- import {
35
- materializeManifestTable
36
- } from "./chunk-U6OJN7XS.mjs";
37
- import {
38
- ensureDir,
39
- exists,
40
- readTextFileIfExists,
41
- writeJsonFile,
42
- writeTextFile
43
- } from "./chunk-LM3OZLZG.mjs";
44
- import {
45
- PROJECT_DIR_NAME
46
- } from "./chunk-SHUMAVAP.mjs";
47
-
48
- // src/services/testing/reducer-native-test-harness.ts
49
- import path2 from "path";
50
- import { randomUUID as randomUUID2 } from "crypto";
51
- import {
52
- existsSync,
53
- mkdirSync,
54
- readFileSync,
55
- rmSync,
56
- writeFileSync
57
- } from "fs";
58
- import { readdir } from "fs/promises";
59
- import { isDeepStrictEqual } from "util";
60
-
61
- // src/services/gameplay-authority-submit.ts
62
- import { randomUUID } from "crypto";
63
-
64
- // ../../node_modules/.pnpm/@dreamboard-games+gameplay-authority-protocol@0.1.0-alpha.0/node_modules/@dreamboard-games/gameplay-authority-protocol/src/frames.ts
65
- var GAMEPLAY_AUTHORITY_AUDIENCE = "dreamboard-gameplay-authority";
66
- var GAMEPLAY_TUNNEL_AUDIENCE = "dreamboard-gameplay-authority-tunnel";
67
- var GameplayPermissionSchema = external_exports.enum([
68
- "observe",
69
- "submit",
70
- "restore-history"
71
- ]);
72
- var GameplayConnectionContextSchema = external_exports.object({
73
- sessionId: external_exports.string().uuid(),
74
- playerId: external_exports.string().min(1),
75
- permissions: external_exports.array(GameplayPermissionSchema)
76
- });
77
- var GameplayActorKindSchema = external_exports.enum(["user", "demo", "perf"]);
78
- var JsonPatchOperationSchema = external_exports.object({
79
- op: external_exports.string().min(1),
80
- path: external_exports.string()
81
- }).catchall(external_exports.unknown());
82
- var ProjectedDeltaSchema = external_exports.object({
83
- kind: external_exports.literal("delta"),
84
- generation: external_exports.number().int().nonnegative(),
85
- baseVersion: external_exports.number().int().nonnegative(),
86
- version: external_exports.number().int().nonnegative(),
87
- patch: external_exports.array(JsonPatchOperationSchema)
88
- });
89
- var ProjectedSnapshotSchema = external_exports.object({
90
- kind: external_exports.literal("snapshot"),
91
- generation: external_exports.number().int().nonnegative(),
92
- version: external_exports.number().int().nonnegative(),
93
- view: external_exports.unknown()
94
- });
95
- var BaseGameplayCapabilityClaimsSchema = external_exports.object({
96
- typ: external_exports.literal("gameplay-capability"),
97
- aud: external_exports.literal(GAMEPLAY_AUTHORITY_AUDIENCE),
98
- iss: external_exports.string().min(1),
99
- exp: external_exports.number().int().positive(),
100
- iat: external_exports.number().int().positive().optional(),
101
- jti: external_exports.string().min(1).optional(),
102
- sessionId: external_exports.string().uuid(),
103
- playerId: external_exports.string().min(1),
104
- permissions: external_exports.array(GameplayPermissionSchema)
105
- });
106
- var UserGameplayCapabilityClaimsSchema = BaseGameplayCapabilityClaimsSchema.extend({
107
- actorKind: external_exports.literal("user")
108
- });
109
- var DemoGameplayCapabilityClaimsSchema = BaseGameplayCapabilityClaimsSchema.extend({
110
- actorKind: external_exports.literal("demo")
111
- });
112
- var PerfGameplayCapabilityClaimsSchema = BaseGameplayCapabilityClaimsSchema.extend({
113
- actorKind: external_exports.literal("perf"),
114
- perfRunId: external_exports.string().uuid(),
115
- laneId: external_exports.string().min(1)
116
- });
117
- var GameplayCapabilityClaimsSchema = external_exports.discriminatedUnion(
118
- "actorKind",
119
- [
120
- UserGameplayCapabilityClaimsSchema,
121
- DemoGameplayCapabilityClaimsSchema,
122
- PerfGameplayCapabilityClaimsSchema
123
- ]
124
- );
125
- var GameplayTunnelClaimsSchema = external_exports.object({
126
- aud: external_exports.literal(GAMEPLAY_TUNNEL_AUDIENCE),
127
- iss: external_exports.string().min(1),
128
- exp: external_exports.number().int().positive(),
129
- iat: external_exports.number().int().positive().optional(),
130
- jti: external_exports.string().min(1).optional(),
131
- targetInstanceId: external_exports.string().min(1),
132
- sessionId: external_exports.string().uuid(),
133
- playerId: external_exports.string().min(1),
134
- // Permissions are cryptographically bound into the tunnel token so the owner
135
- // task trusts the signed claim set rather than the unauthenticated
136
- // `tunnel.bind` frame. An ingress task cannot grant a player capabilities the
137
- // issued token did not attest.
138
- permissions: external_exports.array(GameplayPermissionSchema)
139
- });
140
- var AuthConnectFrameSchema = external_exports.object({
141
- type: external_exports.literal("auth.connect"),
142
- capabilityToken: external_exports.string().min(1)
143
- });
144
- var AuthRefreshFrameSchema = external_exports.object({
145
- type: external_exports.literal("auth.refresh"),
146
- capabilityToken: external_exports.string().min(1)
147
- });
148
- var SessionResumeFrameSchema = external_exports.object({
149
- type: external_exports.literal("session.resume"),
150
- lastSeenGeneration: external_exports.number().int().nonnegative().nullable(),
151
- lastSeenVersion: external_exports.number().int().nonnegative().nullable(),
152
- unacknowledgedClientActionIds: external_exports.array(external_exports.string().min(1)).max(32)
153
- });
154
- var CommandSubmitFrameSchema = external_exports.object({
155
- type: external_exports.literal("command.submit"),
156
- clientActionId: external_exports.string().min(1).max(128),
157
- expectedVersion: external_exports.number().int().nonnegative(),
158
- actionSetVersion: external_exports.string().min(1),
159
- interactionId: external_exports.string().min(1),
160
- inputs: external_exports.record(external_exports.string(), external_exports.unknown())
161
- });
162
- var HistoryRestoreFrameSchema = external_exports.object({
163
- type: external_exports.literal("history.restore"),
164
- restoreId: external_exports.string().min(1).max(128),
165
- targetGeneration: external_exports.number().int().nonnegative(),
166
- targetVersion: external_exports.number().int().nonnegative()
167
- });
168
- var ClientGameplayFrameSchema = external_exports.discriminatedUnion("type", [
169
- AuthConnectFrameSchema,
170
- AuthRefreshFrameSchema,
171
- SessionResumeFrameSchema,
172
- CommandSubmitFrameSchema,
173
- HistoryRestoreFrameSchema
174
- ]);
175
- var TunnelBindFrameSchema = external_exports.object({
176
- type: external_exports.literal("tunnel.bind"),
177
- context: GameplayConnectionContextSchema
178
- });
179
- var TunnelClientFrameSchema = external_exports.discriminatedUnion("type", [
180
- TunnelBindFrameSchema,
181
- SessionResumeFrameSchema,
182
- CommandSubmitFrameSchema,
183
- HistoryRestoreFrameSchema
184
- ]);
185
- var CommandAcceptedFrameSchema = external_exports.object({
186
- type: external_exports.literal("command.accepted"),
187
- clientActionId: external_exports.string().min(1),
188
- generation: external_exports.number().int().nonnegative(),
189
- version: external_exports.number().int().nonnegative(),
190
- stateHash: external_exports.string().min(1),
191
- update: ProjectedSnapshotSchema
192
- });
193
- var CommandRejectedFrameSchema = external_exports.object({
194
- type: external_exports.literal("command.rejected"),
195
- clientActionId: external_exports.string().min(1).optional(),
196
- errorCode: external_exports.string().min(1),
197
- message: external_exports.string().min(1),
198
- currentGeneration: external_exports.number().int().nonnegative().optional(),
199
- currentVersion: external_exports.number().int().nonnegative().optional()
200
- });
201
- var HistoryRestoredFrameSchema = external_exports.object({
202
- type: external_exports.literal("history.restored"),
203
- restoreId: external_exports.string().min(1),
204
- generation: external_exports.number().int().nonnegative(),
205
- version: external_exports.number().int().nonnegative(),
206
- stateHash: external_exports.string().min(1),
207
- update: ProjectedSnapshotSchema
208
- });
209
- var HistoryRestoreRejectedFrameSchema = external_exports.object({
210
- type: external_exports.literal("history.restoreRejected"),
211
- restoreId: external_exports.string().min(1).optional(),
212
- errorCode: external_exports.string().min(1),
213
- message: external_exports.string().min(1),
214
- currentGeneration: external_exports.number().int().nonnegative().optional(),
215
- currentVersion: external_exports.number().int().nonnegative().optional()
216
- });
217
- var AuthAcceptedFrameSchema = external_exports.object({
218
- type: external_exports.literal("auth.accepted"),
219
- expiresAt: external_exports.string().datetime()
220
- });
221
- var AuthorityRecoveringFrameSchema = external_exports.object({
222
- type: external_exports.literal("authority.recovering"),
223
- retryAfterMs: external_exports.number().int().positive(),
224
- message: external_exports.string().min(1)
225
- });
226
- var SessionSnapshotFrameSchema = external_exports.object({
227
- type: external_exports.literal("session.snapshot"),
228
- generation: external_exports.number().int().nonnegative(),
229
- version: external_exports.number().int().nonnegative(),
230
- stateHash: external_exports.string().min(1),
231
- update: ProjectedSnapshotSchema
232
- });
233
- var SessionDeltaFrameSchema = external_exports.object({
234
- type: external_exports.literal("session.delta"),
235
- clientActionId: external_exports.string().min(1).optional(),
236
- update: ProjectedDeltaSchema
237
- });
238
- var ServerGameplayFrameSchema = external_exports.discriminatedUnion("type", [
239
- AuthAcceptedFrameSchema,
240
- AuthorityRecoveringFrameSchema,
241
- SessionSnapshotFrameSchema,
242
- SessionDeltaFrameSchema,
243
- CommandAcceptedFrameSchema,
244
- CommandRejectedFrameSchema,
245
- HistoryRestoredFrameSchema,
246
- HistoryRestoreRejectedFrameSchema
247
- ]);
248
- var TunnelBoundFrameSchema = external_exports.object({
249
- type: external_exports.literal("tunnel.bound"),
250
- sessionId: external_exports.string().uuid(),
251
- playerId: external_exports.string().min(1)
252
- });
253
- var TunnelServerFrameSchema = external_exports.discriminatedUnion("type", [
254
- TunnelBoundFrameSchema,
255
- AuthorityRecoveringFrameSchema,
256
- SessionSnapshotFrameSchema,
257
- SessionDeltaFrameSchema,
258
- CommandAcceptedFrameSchema,
259
- CommandRejectedFrameSchema,
260
- HistoryRestoredFrameSchema,
261
- HistoryRestoreRejectedFrameSchema
262
- ]);
263
-
264
- // ../../node_modules/.pnpm/@dreamboard-games+gameplay-authority-client@0.1.0-alpha.1/node_modules/@dreamboard-games/gameplay-authority-client/src/index.ts
265
- var DEFAULT_OPEN_TIMEOUT_MS = 5e3;
266
- var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
267
- var GameplayAuthorityRecoveringError = class extends Error {
268
- retryAfterMs;
269
- constructor(message, retryAfterMs) {
270
- super(message);
271
- this.name = "GameplayAuthorityRecoveringError";
272
- this.retryAfterMs = retryAfterMs;
273
- }
274
- };
275
- async function connectGameplayAuthority(input) {
276
- const webSocketFactory = input.webSocketFactory ?? browserWebSocketFactory();
277
- const openTimeoutMs = input.openTimeoutMs ?? DEFAULT_OPEN_TIMEOUT_MS;
278
- const requestTimeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
279
- const socket = new webSocketFactory(input.websocketUrl, input.webSocketInit);
280
- try {
281
- await waitUntilOpen(socket, openTimeoutMs);
282
- socket.send(
283
- JSON.stringify({
284
- type: "auth.connect",
285
- capabilityToken: input.capabilityToken
286
- })
287
- );
288
- await waitForServerFrame(
289
- socket,
290
- (frame) => frame.type === "auth.accepted",
291
- requestTimeoutMs
292
- );
293
- return new WebSocketGameplayAuthorityClient(socket, requestTimeoutMs);
294
- } catch (error) {
295
- socket.close();
296
- throw error;
297
- }
298
- }
299
- var WebSocketGameplayAuthorityClient = class {
300
- constructor(socket, requestTimeoutMs) {
301
- this.socket = socket;
302
- this.requestTimeoutMs = requestTimeoutMs;
303
- }
304
- socket;
305
- requestTimeoutMs;
306
- resume(input = {}) {
307
- this.socket.send(
308
- JSON.stringify({
309
- type: "session.resume",
310
- lastSeenGeneration: input.lastSeenGeneration ?? null,
311
- lastSeenVersion: input.lastSeenVersion ?? null,
312
- unacknowledgedClientActionIds: input.unacknowledgedClientActionIds ?? []
313
- })
314
- );
315
- }
316
- async submitCommand(input) {
317
- this.socket.send(
318
- JSON.stringify({
319
- type: "command.submit",
320
- clientActionId: input.clientActionId,
321
- expectedVersion: input.expectedVersion,
322
- actionSetVersion: input.actionSetVersion,
323
- interactionId: input.interactionId,
324
- inputs: input.inputs
325
- })
326
- );
327
- const frame = await waitForServerFrame(
328
- this.socket,
329
- (candidate) => candidate.type === "authority.recovering" || candidate.type === "command.accepted" && candidate.clientActionId === input.clientActionId || candidate.type === "command.rejected" && (candidate.clientActionId === void 0 || candidate.clientActionId === input.clientActionId),
330
- this.requestTimeoutMs
331
- );
332
- throwIfRecovering(frame);
333
- if (frame.type !== "command.accepted" && frame.type !== "command.rejected") {
334
- throw new Error("Unexpected gameplay authority command frame.");
335
- }
336
- return frame;
337
- }
338
- async restoreHistory(input) {
339
- this.socket.send(
340
- JSON.stringify({
341
- type: "history.restore",
342
- restoreId: input.restoreId,
343
- targetGeneration: input.targetGeneration,
344
- targetVersion: input.targetVersion
345
- })
346
- );
347
- const frame = await waitForServerFrame(
348
- this.socket,
349
- (candidate) => candidate.type === "authority.recovering" || candidate.type === "history.restored" && candidate.restoreId === input.restoreId || candidate.type === "history.restoreRejected" && (candidate.restoreId === void 0 || candidate.restoreId === input.restoreId),
350
- this.requestTimeoutMs
351
- );
352
- throwIfRecovering(frame);
353
- if (frame.type !== "history.restored" && frame.type !== "history.restoreRejected") {
354
- throw new Error("Unexpected gameplay authority history frame.");
355
- }
356
- return frame;
357
- }
358
- frames(signal) {
359
- return readServerFrames(this.socket, signal);
360
- }
361
- close() {
362
- this.socket.close();
363
- }
364
- };
365
- function throwIfRecovering(frame) {
366
- if (frame.type === "authority.recovering") {
367
- throw new GameplayAuthorityRecoveringError(
368
- frame.message,
369
- frame.retryAfterMs
370
- );
371
- }
372
- }
373
- function browserWebSocketFactory() {
374
- return BrowserGameplayAuthorityWebSocket;
375
- }
376
- var BrowserGameplayAuthorityWebSocket = class {
377
- socket;
378
- listeners = /* @__PURE__ */ new Map();
379
- constructor(url, init) {
380
- if (init?.headers && Object.keys(init.headers).length > 0) {
381
- throw new Error(
382
- "Browser WebSocket does not support custom headers; provide a webSocketFactory for header-aware runtimes."
383
- );
384
- }
385
- this.socket = new WebSocket(url);
386
- }
387
- send(data) {
388
- this.socket.send(data);
389
- }
390
- close() {
391
- this.socket.close();
392
- }
393
- on(event, listener) {
394
- const wrapped = (observed) => {
395
- if (event === "message" && "data" in observed) {
396
- listener(observed.data);
397
- return;
398
- }
399
- listener(observed);
400
- };
401
- const listeners = this.listeners.get(event) ?? /* @__PURE__ */ new Map();
402
- listeners.set(listener, wrapped);
403
- this.listeners.set(event, listeners);
404
- this.socket.addEventListener(event, wrapped);
405
- }
406
- off(event, listener) {
407
- const listeners = this.listeners.get(event);
408
- const wrapped = listeners?.get(listener);
409
- if (!listeners || !wrapped) {
410
- return;
411
- }
412
- this.socket.removeEventListener(event, wrapped);
413
- listeners.delete(listener);
414
- }
415
- };
416
- async function waitUntilOpen(socket, timeoutMs) {
417
- await waitForEvent(socket, "open", timeoutMs);
418
- }
419
- async function waitForEvent(socket, event, timeoutMs) {
420
- return new Promise((resolve, reject) => {
421
- const cleanup = installSocketListeners(socket, {
422
- timeoutMs,
423
- onEvent(observed) {
424
- if (observed === event) {
425
- cleanup();
426
- resolve();
427
- }
428
- },
429
- onError(error) {
430
- cleanup();
431
- reject(
432
- error instanceof Error ? error : new Error("gameplay socket error")
433
- );
434
- },
435
- onClose() {
436
- cleanup();
437
- reject(new Error("gameplay socket closed"));
438
- },
439
- onTimeout() {
440
- cleanup();
441
- reject(new Error("gameplay socket timed out"));
442
- }
443
- });
444
- });
445
- }
446
- async function waitForServerFrame(socket, predicate, timeoutMs) {
447
- return new Promise((resolve, reject) => {
448
- const cleanup = installSocketListeners(socket, {
449
- timeoutMs,
450
- onMessage(data) {
451
- let frame;
452
- try {
453
- frame = ServerGameplayFrameSchema.parse(
454
- JSON.parse(messageToString(data))
455
- );
456
- } catch (error) {
457
- cleanup();
458
- reject(
459
- error instanceof Error ? error : new Error("invalid gameplay authority frame")
460
- );
461
- return;
462
- }
463
- if (predicate(frame)) {
464
- cleanup();
465
- resolve(frame);
466
- }
467
- },
468
- onError(error) {
469
- cleanup();
470
- reject(
471
- error instanceof Error ? error : new Error("gameplay socket error")
472
- );
473
- },
474
- onClose() {
475
- cleanup();
476
- reject(new Error("gameplay socket closed"));
477
- },
478
- onTimeout() {
479
- cleanup();
480
- reject(new Error("gameplay socket timed out"));
481
- }
482
- });
483
- });
484
- }
485
- function installSocketListeners(socket, handlers) {
486
- const timer = setTimeout(handlers.onTimeout, handlers.timeoutMs);
487
- const onOpen = () => handlers.onEvent?.("open");
488
- const onMessage = (data) => handlers.onMessage?.(data);
489
- const onError = (error) => handlers.onError(error);
490
- const onClose = () => handlers.onClose();
491
- socket.on("open", onOpen);
492
- socket.on("message", onMessage);
493
- socket.on("error", onError);
494
- socket.on("close", onClose);
495
- return () => {
496
- clearTimeout(timer);
497
- socket.off?.("open", onOpen);
498
- socket.off?.("message", onMessage);
499
- socket.off?.("error", onError);
500
- socket.off?.("close", onClose);
501
- };
502
- }
503
- function readServerFrames(socket, signal) {
504
- const queue = [];
505
- let wake = null;
506
- let closed = false;
507
- let error = null;
508
- const wakeReader = () => {
509
- wake?.();
510
- wake = null;
511
- };
512
- const onMessage = (data) => {
513
- try {
514
- queue.push(
515
- ServerGameplayFrameSchema.parse(JSON.parse(messageToString(data)))
516
- );
517
- } catch (candidate) {
518
- error = candidate instanceof Error ? candidate : new Error("invalid gameplay authority frame");
519
- }
520
- wakeReader();
521
- };
522
- const onError = (candidate) => {
523
- error = candidate instanceof Error ? candidate : new Error("gameplay socket error");
524
- wakeReader();
525
- };
526
- const onClose = () => {
527
- closed = true;
528
- wakeReader();
529
- };
530
- const onAbort = () => {
531
- closed = true;
532
- wakeReader();
533
- };
534
- socket.on("message", onMessage);
535
- socket.on("error", onError);
536
- socket.on("close", onClose);
537
- signal?.addEventListener("abort", onAbort, { once: true });
538
- const cleanup = () => {
539
- socket.off?.("message", onMessage);
540
- socket.off?.("error", onError);
541
- socket.off?.("close", onClose);
542
- signal?.removeEventListener("abort", onAbort);
543
- };
544
- const iterator = {
545
- async next() {
546
- while (!closed && !signal?.aborted) {
547
- const next = queue.shift();
548
- if (next) {
549
- return { done: false, value: next };
550
- }
551
- if (error) {
552
- cleanup();
553
- throw error;
554
- }
555
- await new Promise((resolve) => {
556
- wake = resolve;
557
- });
558
- }
559
- cleanup();
560
- if (error) {
561
- throw error;
562
- }
563
- return { done: true, value: void 0 };
564
- },
565
- async return(value) {
566
- closed = true;
567
- wakeReader();
568
- cleanup();
569
- return { done: true, value };
570
- },
571
- async throw(candidate) {
572
- closed = true;
573
- wakeReader();
574
- cleanup();
575
- throw candidate;
576
- },
577
- [Symbol.asyncIterator]() {
578
- return this;
579
- },
580
- async [Symbol.asyncDispose]() {
581
- await this.return(void 0);
582
- }
583
- };
584
- return iterator;
585
- }
586
- function messageToString(data) {
587
- if (typeof data === "string") {
588
- return data;
589
- }
590
- if (data instanceof ArrayBuffer) {
591
- return new TextDecoder().decode(data);
592
- }
593
- if (ArrayBuffer.isView(data)) {
594
- return new TextDecoder().decode(data);
595
- }
596
- return String(data);
597
- }
598
-
599
- // src/services/gameplay-authority-submit.ts
600
- var CLIENT_ACTION_ID_HEADER = "X-Dreamboard-Client-Action-Id";
601
- var DEFAULT_TIMEOUT_MS = 1e4;
602
- async function submitGameplayAuthorityAction(options) {
603
- try {
604
- const requestCapability = options.capabilityRequester ?? createGameplayCapability;
605
- const connectAuthority = options.connectAuthority ?? connectGameplayAuthority;
606
- const capability = await requestCapability({
607
- path: {
608
- sessionId: options.path.sessionId,
609
- playerId: options.path.playerId
610
- }
611
- });
612
- if (capability.error || !capability.data) {
613
- return { error: capability.error ?? new Error("Missing capability.") };
614
- }
615
- const clientActionId = options.headers?.[CLIENT_ACTION_ID_HEADER] ?? options.clientActionIdFactory?.() ?? randomUUID();
616
- const client = await connectAuthority({
617
- websocketUrl: capability.data.websocketUrl,
618
- capabilityToken: capability.data.token,
619
- openTimeoutMs: DEFAULT_TIMEOUT_MS,
620
- requestTimeoutMs: DEFAULT_TIMEOUT_MS
621
- });
622
- try {
623
- const frame = await client.submitCommand({
624
- clientActionId,
625
- expectedVersion: options.body.expectedVersion,
626
- actionSetVersion: options.body.actionSetVersion,
627
- interactionId: options.path.interactionId,
628
- inputs: options.body.inputs ?? {}
629
- });
630
- if (frame.type === "command.rejected") {
631
- return {
632
- data: {
633
- success: false,
634
- accepted: false,
635
- version: typeof frame.currentVersion === "number" ? frame.currentVersion : options.body.expectedVersion,
636
- actionSetVersion: options.body.actionSetVersion,
637
- errorCode: typeof frame.errorCode === "string" ? frame.errorCode : void 0,
638
- message: typeof frame.message === "string" ? frame.message : void 0,
639
- clientActionId
640
- }
641
- };
642
- }
643
- const acceptedVersion = typeof frame.version === "number" ? frame.version : void 0;
644
- if (acceptedVersion === void 0) {
645
- return {
646
- error: new Error("Accepted frame did not include a numeric version.")
647
- };
648
- }
649
- const update = frame.update && typeof frame.update === "object" ? frame.update : {};
650
- const view = update.view && typeof update.view === "object" ? update.view : {};
651
- const projectedActionSetVersion = typeof view.actionSetVersion === "string" ? view.actionSetVersion : void 0;
652
- return {
653
- data: {
654
- success: true,
655
- accepted: true,
656
- durabilityStatus: "COMMITTED",
657
- version: acceptedVersion,
658
- actionSetVersion: projectedActionSetVersion ?? options.body.actionSetVersion,
659
- clientActionId
660
- }
661
- };
662
- } catch (error) {
663
- if (error instanceof GameplayAuthorityRecoveringError) {
664
- return {
665
- error: new Error(error.message || "Session authority is recovering.")
666
- };
667
- }
668
- throw error;
669
- } finally {
670
- client.close();
671
- }
672
- } catch (error) {
673
- return { error };
674
- }
675
- }
676
-
677
- // src/ui/playwright-runner.ts
678
- import os from "os";
679
- import path from "path";
680
- function configurePlaywrightBrowsersPath() {
681
- if (process.env.PLAYWRIGHT_BROWSERS_PATH) {
682
- return;
683
- }
684
- const runtimeHome = process.env.HOME;
685
- let realHome;
686
- try {
687
- realHome = os.userInfo().homedir;
688
- } catch {
689
- return;
690
- }
691
- if (!runtimeHome || path.resolve(runtimeHome) === path.resolve(realHome)) {
692
- return;
693
- }
694
- const browserCachePath = process.platform === "darwin" ? path.join(realHome, "Library", "Caches", "ms-playwright") : process.platform === "win32" ? path.join(realHome, "AppData", "Local", "ms-playwright") : path.join(realHome, ".cache", "ms-playwright");
695
- process.env.PLAYWRIGHT_BROWSERS_PATH = browserCachePath;
696
- }
697
- async function buildBrowserAuthInitScript(config) {
698
- if (!config.authToken) return null;
699
- return `(function(){localStorage.setItem('dreamboard_auth_token',${JSON.stringify(config.authToken)});})();`;
700
- }
701
- async function waitForGameReady(page, timeoutMs = 6e4) {
702
- await page.waitForSelector('iframe[title="Game UI Plugin"]', {
703
- timeout: timeoutMs
704
- });
705
- }
706
-
707
- // src/services/workflows/resolve-setup-profile.ts
708
- function resolveSetupProfileSelection(options) {
709
- const setupProfiles = options.manifest.setupProfiles ?? [];
710
- const requestedSetupProfileId = options.requestedSetupProfileId?.trim() || void 0;
711
- if (requestedSetupProfileId) {
712
- const requestedProfile = setupProfiles.find(
713
- (profile) => profile.id === requestedSetupProfileId
714
- );
715
- if (!requestedProfile) {
716
- const knownProfiles = setupProfiles.map((profile) => profile.id).join(", ");
717
- throw new Error(
718
- setupProfiles.length === 0 ? `Unknown setup profile '${requestedSetupProfileId}'. The manifest defines no setup profiles.` : `Unknown setup profile '${requestedSetupProfileId}'. Expected one of: ${knownProfiles}.`
719
- );
720
- }
721
- return {
722
- id: requestedProfile.id,
723
- name: requestedProfile.name,
724
- source: "explicit"
725
- };
726
- }
727
- if (setupProfiles.length === 0) {
728
- return {
729
- id: null,
730
- name: null,
731
- source: "none"
732
- };
733
- }
734
- if (setupProfiles.length === 1) {
735
- return {
736
- id: setupProfiles[0]?.id ?? null,
737
- name: setupProfiles[0]?.name ?? null,
738
- source: "implicit-single"
739
- };
740
- }
741
- return {
742
- id: setupProfiles[0]?.id ?? null,
743
- name: setupProfiles[0]?.name ?? null,
744
- source: "implicit-first"
745
- };
746
- }
747
-
748
- // src/utils/session-game-source.ts
749
- function projectIdFromSessionGameSource(source) {
750
- if (source.kind === "USER_COMPILED") {
751
- return source.projectId;
752
- }
753
- return `demo:${source.slug}:${source.revisionId}`;
754
- }
755
-
756
- // src/services/testing/reducer-native-test-harness.ts
757
- globalThis.__DREAMBOARD_AUTHORING_WARNINGS__ = true;
758
- var GENERATED_TESTING_TYPES_PREFIX = "// Generated by dreamboard";
759
- var TESTING_TYPES_STUB = "export function defineScenario(scenario) { return scenario; }\n";
760
- var DEFAULT_TIMEOUT_MS2 = 1e4;
761
- var BASE_SUFFIX = ".base.ts";
762
- var SCENARIO_SUFFIX = ".scenario.ts";
763
- var SDK_UI_RUNTIME_EXTERNALS = [
764
- "@radix-ui/react-accordion",
765
- "@radix-ui/react-dialog",
766
- "@radix-ui/react-label",
767
- "@radix-ui/react-select",
768
- "@radix-ui/react-slot",
769
- "@radix-ui/react-tooltip",
770
- "@use-gesture/react",
771
- "clsx",
772
- "framer-motion",
773
- "lucide-react",
774
- "react",
775
- "react-dom",
776
- "vaul"
777
- ];
778
- function formatScenarioErrorForDisplay(options) {
779
- const message = options.error.message || "Scenario failed.";
780
- const scenarioFrame = findScenarioStackFrame({
781
- stack: options.error.stack,
782
- projectRoot: options.projectRoot,
783
- scenarioFilePath: options.scenarioFilePath
784
- });
785
- return scenarioFrame ? `${message}
786
- ${scenarioFrame}` : message;
787
- }
788
- function findScenarioStackFrame(options) {
789
- if (!options.stack) {
790
- return null;
791
- }
792
- const absolutePath = path2.resolve(options.scenarioFilePath);
793
- const relativePath = path2.relative(options.projectRoot, absolutePath);
794
- const normalizedRelativePath = relativePath.split(path2.sep).join("/");
795
- const escapedAbsolutePath = escapeRegExp(absolutePath);
796
- const escapedRelativePath = escapeRegExp(normalizedRelativePath);
797
- const absoluteFrame = new RegExp(`${escapedAbsolutePath}:(\\d+):(\\d+)`);
798
- const relativeFrame = new RegExp(`${escapedRelativePath}:(\\d+):(\\d+)`);
799
- for (const line of options.stack.split("\n")) {
800
- const normalizedLine = line.split(path2.sep).join("/");
801
- const match = normalizedLine.match(absoluteFrame) ?? normalizedLine.match(relativeFrame);
802
- if (match?.[1] && match?.[2]) {
803
- return `at ${normalizedRelativePath}:${match[1]}:${match[2]}`;
804
- }
805
- }
806
- return null;
807
- }
808
- function escapeRegExp(value) {
809
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
810
- }
811
- var testingExpectApiFactoryPromise;
812
- async function loadTestingExpectApiFactory() {
813
- testingExpectApiFactoryPromise ??= import("./testing-5K2BJYF2.mjs").then(
814
- (module) => module.createExpectApi
815
- );
816
- return testingExpectApiFactoryPromise;
817
- }
818
- function createSubmissionError(errorCode, message, fallbackMessage) {
819
- const error = new Error(message ?? fallbackMessage);
820
- error.name = "SubmissionError";
821
- error.errorCode = errorCode;
822
- return error;
823
- }
824
- function parseJsonValue(value) {
825
- if (typeof value !== "string") {
826
- return value;
827
- }
828
- try {
829
- return JSON.parse(value);
830
- } catch {
831
- return value;
832
- }
833
- }
834
- function deepEqual(left, right) {
835
- return isDeepStrictEqual(left, right);
836
- }
837
- function normalizeScenarioRunners(runners) {
838
- return runners && runners.length > 0 ? runners : ["reducer"];
839
- }
840
- function shouldRefreshReducerTestingTypes(existingContent) {
841
- if (existingContent === null || existingContent.trim().length === 0 || existingContent === TESTING_TYPES_STUB || existingContent.startsWith(GENERATED_TESTING_TYPES_PREFIX)) {
842
- return true;
843
- }
844
- return false;
845
- }
846
- async function discoverFiles(root, suffix) {
847
- if (!await exists(root)) {
848
- return [];
849
- }
850
- const entries = await readdir(root, { withFileTypes: true });
851
- const files = [];
852
- for (const entry of entries) {
853
- const entryPath = path2.join(root, entry.name);
854
- if (entry.isDirectory()) {
855
- files.push(...await discoverFiles(entryPath, suffix));
856
- continue;
857
- }
858
- if (entry.isFile() && entry.name.endsWith(suffix)) {
859
- files.push(entryPath);
860
- }
861
- }
862
- files.sort();
863
- return files;
864
- }
865
- function isFunction(value) {
866
- return typeof value === "function";
867
- }
868
- function parseTypedBaseDefinition(value) {
869
- if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string" || !("setup" in value) || !isFunction(value.setup)) {
870
- throw new Error("Invalid reducer-native base definition.");
871
- }
872
- const parentBaseId = "extends" in value && typeof value.extends === "string" ? value.extends : void 0;
873
- const seed = "seed" in value && typeof value.seed === "number" ? value.seed : void 0;
874
- const players = "players" in value && typeof value.players === "number" ? value.players : void 0;
875
- if ((seed === void 0 || players === void 0) && !parentBaseId) {
876
- throw new Error(
877
- "Invalid reducer-native base definition. Base definitions without --extends must declare numeric seed and players."
878
- );
879
- }
880
- const setupProfileId = "setupProfileId" in value && typeof value.setupProfileId === "string" ? value.setupProfileId : void 0;
881
- return {
882
- ...value,
883
- seed,
884
- players,
885
- setupProfileId,
886
- extends: parentBaseId
887
- };
888
- }
889
- function parseTypedScenarioDefinition(value) {
890
- if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string" || !("from" in value) || typeof value.from !== "string" || !("when" in value) || !isFunction(value.when) || !("then" in value) || !isFunction(value.then)) {
891
- throw new Error("Invalid reducer-native scenario definition.");
892
- }
893
- const phase = "phase" in value && typeof value.phase === "string" ? value.phase : void 0;
894
- const stage = "stage" in value && typeof value.stage === "string" ? value.stage : void 0;
895
- return {
896
- ...value,
897
- phase,
898
- stage
899
- };
900
- }
901
- async function isReducerNativeTestingWorkspace(projectRoot) {
902
- return await exists(path2.join(projectRoot, "app", "game.ts")) && await exists(
903
- path2.join(projectRoot, "shared", "generated", "ui-contract.ts")
904
- );
905
- }
906
- async function ensureReducerNativeTestingFiles(projectRoot) {
907
- const testingTypesPath = path2.join(projectRoot, "test", "testing-types.ts");
908
- const testingContractPath = path2.join(
909
- projectRoot,
910
- "test",
911
- "generated",
912
- "testing-contract.ts"
913
- );
914
- const baseStatesPath = path2.join(
915
- projectRoot,
916
- "test",
917
- "generated",
918
- "base-states.generated.ts"
919
- );
920
- const baseStatesDtsPath = path2.join(
921
- projectRoot,
922
- "test",
923
- "generated",
924
- "base-states.generated.d.ts"
925
- );
926
- const scenarioManifestPath = path2.join(
927
- projectRoot,
928
- "test",
929
- "generated",
930
- "scenario-manifest.generated.ts"
931
- );
932
- await ensureDir(path2.dirname(testingTypesPath));
933
- await ensureDir(path2.dirname(testingContractPath));
934
- const existingTestingTypes = await readTextFileIfExists(testingTypesPath);
935
- if (shouldRefreshReducerTestingTypes(existingTestingTypes)) {
936
- await writeTextFile(
937
- testingTypesPath,
938
- REDUCER_TESTING_TYPES_WRAPPER_CONTENT
939
- );
940
- }
941
- const rejectionCodes = await collectKnownRejectionCodes(projectRoot);
942
- await writeTextFile(
943
- testingContractPath,
944
- buildReducerTestingContractContent({ rejectionCodes })
945
- );
946
- const header = "// Generated by dreamboard test generate. Do not edit by hand.\n";
947
- if (!await exists(baseStatesPath)) {
948
- await writeTextFile(
949
- baseStatesPath,
950
- `${header}export const BASE_STATES = {} as const;
951
- export const BASE_STATES_CONTRACT_FINGERPRINT = undefined;
952
- `
953
- );
954
- } else {
955
- const existingBaseStates = await readTextFileIfExists(baseStatesPath);
956
- if (existingBaseStates && !existingBaseStates.includes("BASE_STATES_CONTRACT_FINGERPRINT")) {
957
- await writeTextFile(
958
- baseStatesPath,
959
- `${existingBaseStates.trimEnd()}
960
- export const BASE_STATES_CONTRACT_FINGERPRINT = undefined;
961
- `
962
- );
963
- }
964
- }
965
- if (!await exists(baseStatesDtsPath)) {
966
- await writeTextFile(
967
- baseStatesDtsPath,
968
- `${header}export declare const BASE_STATES: Record<string, unknown>;
969
- export declare const BASE_STATES_CONTRACT_FINGERPRINT: string | undefined;
970
- `
971
- );
972
- } else {
973
- const existingBaseStatesDts = await readTextFileIfExists(baseStatesDtsPath);
974
- if (existingBaseStatesDts && !existingBaseStatesDts.includes("BASE_STATES_CONTRACT_FINGERPRINT")) {
975
- await writeTextFile(
976
- baseStatesDtsPath,
977
- `${existingBaseStatesDts.trimEnd()}
978
- export declare const BASE_STATES_CONTRACT_FINGERPRINT: string | undefined;
979
- `
980
- );
981
- }
982
- }
983
- if (!await exists(scenarioManifestPath)) {
984
- await writeTextFile(
985
- scenarioManifestPath,
986
- `${header}export const SCENARIO_MANIFEST = [] as const;
987
- `
988
- );
989
- }
990
- }
991
- var DEFAULT_REJECTION_CODES = [
992
- "NOT_YOUR_TURN",
993
- "action-unavailable",
994
- "invalid-action-params",
995
- "prompt-not-owned"
996
- ];
997
- async function collectKnownRejectionCodes(projectRoot) {
998
- const knownCodes = new Set(DEFAULT_REJECTION_CODES);
999
- const gamePath = path2.join(projectRoot, "app", "game.ts");
1000
- if (!await exists(gamePath)) {
1001
- return Array.from(knownCodes).sort(
1002
- (left, right) => left.localeCompare(right)
1003
- );
1004
- }
1005
- try {
1006
- const module = await importTypeScriptModule(gamePath);
1007
- const phases = module.default?.phases ?? {};
1008
- for (const phase of Object.values(phases)) {
1009
- const interactions = phase.interactions ?? {};
1010
- for (const interaction of Object.values(interactions)) {
1011
- for (const errorCode of interaction.errorCodes ?? []) {
1012
- if (typeof errorCode === "string" && errorCode.length > 0) {
1013
- knownCodes.add(errorCode);
1014
- }
1015
- }
1016
- }
1017
- }
1018
- } catch {
1019
- }
1020
- return Array.from(knownCodes).sort(
1021
- (left, right) => left.localeCompare(right)
1022
- );
1023
- }
1024
- async function loadTypedBases(projectRoot) {
1025
- const baseFiles = await discoverFiles(
1026
- path2.join(projectRoot, "test", "bases"),
1027
- BASE_SUFFIX
1028
- );
1029
- const loaded = [];
1030
- for (const filePath of baseFiles) {
1031
- const externals = reducerNativeTestHelperExternals(projectRoot, filePath);
1032
- const [module, bundledText] = await Promise.all([
1033
- importTypeScriptModule(filePath),
1034
- bundleTypeScriptModuleText(filePath, { external: externals })
1035
- ]);
1036
- loaded.push({
1037
- filePath,
1038
- definition: parseTypedBaseDefinition(module.default),
1039
- bundleHash: hashContent(bundledText)
1040
- });
1041
- }
1042
- return loaded;
1043
- }
1044
- async function loadTypedScenarios(projectRoot, options) {
1045
- const scenarioFiles = options.scenarioPath ? [path2.resolve(projectRoot, options.scenarioPath)] : await discoverFiles(
1046
- path2.join(projectRoot, "test", "scenarios"),
1047
- SCENARIO_SUFFIX
1048
- );
1049
- const loaded = [];
1050
- for (const filePath of scenarioFiles) {
1051
- const externals = reducerNativeTestHelperExternals(projectRoot, filePath);
1052
- const [module, bundledText] = await Promise.all([
1053
- importTypeScriptModule(filePath),
1054
- bundleTypeScriptModuleText(filePath, { external: externals })
1055
- ]);
1056
- loaded.push({
1057
- filePath,
1058
- definition: parseTypedScenarioDefinition(module.default),
1059
- bundleHash: hashContent(bundledText)
1060
- });
1061
- }
1062
- return loaded;
1063
- }
1064
- function reducerNativeTestHelperExternals(projectRoot, filePath) {
1065
- const testingTypesPath = path2.join(projectRoot, "test", "testing-types");
1066
- const relative = path2.relative(path2.dirname(filePath), testingTypesPath).replaceAll("\\", "/");
1067
- const specifier = relative.startsWith(".") ? relative : `./${relative}`;
1068
- return [specifier, `${specifier}.ts`, ...SDK_UI_RUNTIME_EXTERNALS];
1069
- }
1070
- var JS_MAX_SAFE_INTEGER = 9007199254740991n;
1071
- var JS_MIN_SAFE_INTEGER = -JS_MAX_SAFE_INTEGER;
1072
- var JS_MAX_SAFE_INTEGER_EXCLUSIVE = JS_MAX_SAFE_INTEGER + 1n;
1073
- var KotlinSeededRandom = class {
1074
- x;
1075
- y;
1076
- z;
1077
- w;
1078
- v;
1079
- addend;
1080
- constructor(seed) {
1081
- const seedBig = BigInt.asIntN(64, BigInt(Math.trunc(seed)));
1082
- const seed1 = Number(BigInt.asIntN(32, seedBig));
1083
- const seed2 = Number(BigInt.asIntN(32, seedBig >> 32n));
1084
- this.x = seed1 | 0;
1085
- this.y = seed2 | 0;
1086
- this.z = 0;
1087
- this.w = 0;
1088
- this.v = ~seed1;
1089
- this.addend = seed1 << 10 ^ seed2 >>> 4 | 0;
1090
- if ((this.x | this.y | this.z | this.w | this.v) === 0) {
1091
- throw new Error(
1092
- "Kotlin random seed must initialize at least one state word."
1093
- );
1094
- }
1095
- for (let index = 0; index < 64; index += 1) {
1096
- this.nextInt();
1097
- }
1098
- }
1099
- nextInt() {
1100
- let next = this.x | 0;
1101
- next ^= next >>> 2;
1102
- this.x = this.y;
1103
- this.y = this.z;
1104
- this.z = this.w;
1105
- const currentV = this.v | 0;
1106
- this.w = currentV;
1107
- next = next ^ next << 1 ^ currentV ^ currentV << 4 | 0;
1108
- this.v = next;
1109
- this.addend = this.addend + 362437 | 0;
1110
- return next + this.addend | 0;
1111
- }
1112
- nextBits(bitCount) {
1113
- if (bitCount <= 0) {
1114
- return 0;
1115
- }
1116
- return this.nextInt() >>> 32 - bitCount;
1117
- }
1118
- nextIntBound(bound) {
1119
- if (bound <= 0) {
1120
- throw new Error("bound must be positive");
1121
- }
1122
- if ((bound & -bound) === bound) {
1123
- const bitCount = 31 - Math.clz32(bound);
1124
- return this.nextBits(bitCount);
1125
- }
1126
- let value;
1127
- while (true) {
1128
- const bits = this.nextInt() >>> 1;
1129
- value = bits % bound;
1130
- if (bits - value + (bound - 1) >= 0) {
1131
- return value;
1132
- }
1133
- }
1134
- }
1135
- nextLong() {
1136
- return Number(this.nextLongBigInt());
1137
- }
1138
- nextLongBigInt() {
1139
- const high = BigInt(this.nextInt());
1140
- const low = BigInt(this.nextInt());
1141
- return BigInt.asIntN(64, (high << 32n) + low);
1142
- }
1143
- nextReducerRuntimeSeed() {
1144
- return Number(
1145
- this.nextLongRange(JS_MIN_SAFE_INTEGER, JS_MAX_SAFE_INTEGER_EXCLUSIVE)
1146
- );
1147
- }
1148
- nextLongRange(from, until) {
1149
- if (until <= from) {
1150
- throw new Error("until must be greater than from");
1151
- }
1152
- const bound = until - from;
1153
- const mask = bound - 1n;
1154
- if ((bound & mask) === 0n) {
1155
- return from + (BigInt.asUintN(64, this.nextLongBigInt()) & mask);
1156
- }
1157
- while (true) {
1158
- const bits = BigInt.asUintN(64, this.nextLongBigInt()) >> 1n;
1159
- const value = bits % bound;
1160
- if (BigInt.asIntN(64, bits + mask - value) >= 0n) {
1161
- return from + value;
1162
- }
1163
- }
1164
- }
1165
- };
1166
- function shuffleWithKotlinRandom(values, random) {
1167
- const next = [...values];
1168
- for (let index = next.length - 1; index > 0; index -= 1) {
1169
- const swapIndex = random.nextIntBound(index + 1);
1170
- const current = next[index];
1171
- next[index] = next[swapIndex];
1172
- next[swapIndex] = current;
1173
- }
1174
- return next;
1175
- }
1176
- function resolveEffectiveBaseSetup(options) {
1177
- const inheritedSetupProfileId = options.base.setupProfileId ?? (options.base.extends && options.basesById ? resolveEffectiveBaseSetup({
1178
- manifest: options.manifest,
1179
- base: options.basesById.get(options.base.extends)?.definition ?? (() => {
1180
- throw new Error(
1181
- `Base '${options.base.id}' extends unknown parent '${options.base.extends}'.`
1182
- );
1183
- })(),
1184
- basesById: options.basesById
1185
- }).setupProfileId : void 0);
1186
- const selection = resolveSetupProfileSelection({
1187
- manifest: options.manifest,
1188
- requestedSetupProfileId: inheritedSetupProfileId ?? void 0
1189
- });
1190
- return {
1191
- setupProfileId: selection.id
1192
- };
1193
- }
1194
- function resolveBaseDefinition(base, basesById) {
1195
- const inherited = base.definition.extends !== void 0 ? resolveBaseDefinition(
1196
- basesById.get(base.definition.extends) ?? (() => {
1197
- throw new Error(
1198
- `Base '${base.definition.id}' extends unknown parent '${base.definition.extends}'.`
1199
- );
1200
- })(),
1201
- basesById
1202
- ) : null;
1203
- const seed = base.definition.seed ?? inherited?.seed;
1204
- const players = base.definition.players ?? inherited?.players;
1205
- if (seed === void 0 || players === void 0) {
1206
- throw new Error(
1207
- `Base '${base.definition.id}' must resolve numeric seed and players.`
1208
- );
1209
- }
1210
- return {
1211
- ...base.definition,
1212
- seed,
1213
- players,
1214
- setupProfileId: base.definition.setupProfileId ?? inherited?.setupProfileId ?? void 0
1215
- };
1216
- }
1217
- function summarizeTableValidationError(manifest, error) {
1218
- const issues = error.message.split("; ").map((issue) => issue.trim()).filter((issue) => issue.length > 0);
1219
- if (issues.length === 0 || !issues.every((issue) => issue.startsWith("table"))) {
1220
- return error.message;
1221
- }
1222
- const boardBaseIdByRuntimeId = new Map(
1223
- (manifest.boards ?? []).flatMap(
1224
- (board) => board.scope === "perPlayer" ? Array.from(
1225
- { length: manifest.players.maxPlayers },
1226
- (_, index) => [`${board.id}:player-${index + 1}`, board.id]
1227
- ) : [[board.id, board.id]]
1228
- )
1229
- );
1230
- const representativeIssues = issues.slice(0, 3).map((issue) => {
1231
- const [, runtimeBoardId = ""] = issue.match(/^table\.boards\.byId\.([^.]+)\./) ?? [];
1232
- const boardBaseId = boardBaseIdByRuntimeId.get(runtimeBoardId);
1233
- if (!boardBaseId) {
1234
- return `- ${issue.replace(/^table\./, "")}`;
1235
- }
1236
- const boardSpecificPath = issue.replace(/^table\.boards\.byId\.[^.]+\./, "").replace(/^table\./, "");
1237
- return `- board '${boardBaseId}' (${runtimeBoardId}): ${boardSpecificPath}`;
1238
- });
1239
- return [
1240
- `Reducer-native table validation failed with ${issues.length} issue${issues.length === 1 ? "" : "s"}.`,
1241
- "Likely root cause: the generated test table shape does not match the authored manifest topology.",
1242
- ...representativeIssues,
1243
- "Pass --debug for the full validation dump."
1244
- ].join("\n");
1245
- }
1246
- var ShadowReducerRuntime = class {
1247
- constructor(manifest, gameModuleDefault, createInitialTable, seed, players, setupProfileId, debug = false) {
1248
- this.manifest = manifest;
1249
- this.gameModuleDefault = gameModuleDefault;
1250
- this.createInitialTable = createInitialTable;
1251
- this.seed = seed;
1252
- this.players = players;
1253
- this.setupProfileId = setupProfileId;
1254
- this.debug = debug;
1255
- if (typeof gameModuleDefault !== "object" || gameModuleDefault === null || !("contract" in gameModuleDefault)) {
1256
- throw new Error(
1257
- "app/game.ts must export a reducer-native game definition."
1258
- );
1259
- }
1260
- this.bundle = createReducerBundle(gameModuleDefault);
1261
- this.runtime = this.bundle.createInProcessRuntime();
1262
- this.playerIds = Array.from(
1263
- { length: players },
1264
- (_, index) => `player-${index + 1}`
1265
- );
1266
- }
1267
- manifest;
1268
- gameModuleDefault;
1269
- createInitialTable;
1270
- seed;
1271
- players;
1272
- setupProfileId;
1273
- debug;
1274
- bundle;
1275
- runtime;
1276
- playerIds;
1277
- historyRecords = [];
1278
- version = 0;
1279
- started = false;
1280
- async start() {
1281
- if (this.started) {
1282
- return;
1283
- }
1284
- const random = new KotlinSeededRandom(this.seed);
1285
- const shuffleItems = (values) => shuffleWithKotlinRandom(values, random);
1286
- const table = JSON.parse(
1287
- JSON.stringify(
1288
- this.createInitialTable?.({
1289
- playerIds: this.playerIds,
1290
- shuffleItems
1291
- }) ?? materializeManifestTable({
1292
- manifest: this.manifest,
1293
- playerIds: this.playerIds,
1294
- shuffleItems
1295
- })
1296
- )
1297
- );
1298
- const rngSeed = random.nextReducerRuntimeSeed();
1299
- try {
1300
- await this.runtime.initialize({
1301
- table,
1302
- playerIds: this.playerIds,
1303
- rngSeed,
1304
- setup: this.setupProfileId ? {
1305
- profileId: this.setupProfileId
1306
- } : null
1307
- });
1308
- } catch (error) {
1309
- if (!this.debug && error instanceof Error) {
1310
- throw new Error(summarizeTableValidationError(this.manifest, error));
1311
- }
1312
- throw error;
1313
- }
1314
- this.version = 0;
1315
- this.started = true;
1316
- }
1317
- phase() {
1318
- return (this.runtime.unsafeState()?.domain?.flow?.currentPhase ?? null) || "";
1319
- }
1320
- playerOrder() {
1321
- return this.playerIds;
1322
- }
1323
- rawState() {
1324
- return this.runtime.snapshot();
1325
- }
1326
- currentVersion() {
1327
- return this.version;
1328
- }
1329
- hydrate(snapshot, version = 0) {
1330
- this.runtime.hydrate({ state: structuredClone(snapshot) });
1331
- this.version = version;
1332
- this.started = true;
1333
- }
1334
- patchState(mutator) {
1335
- const next = structuredClone(this.rawState());
1336
- mutator(next);
1337
- this.hydrate(next, this.version + 1);
1338
- }
1339
- projectAllSeats() {
1340
- const projection = this.runtime.projectSeatsDynamic({
1341
- playerIds: this.playerIds
1342
- });
1343
- const interactionsByRef = projection.interactionsByRef && typeof projection.interactionsByRef === "object" && !Array.isArray(projection.interactionsByRef) ? projection.interactionsByRef : {};
1344
- const seats = Object.fromEntries(
1345
- Object.entries(projection.seats ?? {}).map(([playerId, seat]) => {
1346
- const availableInteractionRefs = Array.isArray(
1347
- seat.availableInteractionRefs
1348
- ) ? seat.availableInteractionRefs.filter(
1349
- (ref) => typeof ref === "string"
1350
- ) : [];
1351
- const availableInteractions = seat.availableInteractions ?? availableInteractionRefs.map((ref) => interactionsByRef[ref]).filter(
1352
- (descriptor) => !!descriptor && typeof descriptor === "object"
1353
- );
1354
- return [
1355
- playerId,
1356
- {
1357
- view: seat.view,
1358
- availableInteractions,
1359
- zones: seat.zones
1360
- }
1361
- ];
1362
- })
1363
- );
1364
- return {
1365
- currentStage: projection.currentStage ?? null,
1366
- stageSeats: projection.stageSeats ?? [],
1367
- seats
1368
- };
1369
- }
1370
- currentStage() {
1371
- return this.projectAllSeats().currentStage;
1372
- }
1373
- view(playerId) {
1374
- return this.runtime.projectSeatViewDynamic({
1375
- playerId
1376
- }) ?? null;
1377
- }
1378
- availableInteractionsForPlayer(playerId) {
1379
- const projection = this.projectAllSeats();
1380
- const seat = projection.seats[playerId];
1381
- const descriptors = seat?.availableInteractions ?? [];
1382
- return descriptors.filter(
1383
- (descriptor) => !!descriptor && typeof descriptor.interactionId === "string"
1384
- ).map((descriptor) => descriptor.interactionId);
1385
- }
1386
- /**
1387
- * Phase-kind interaction descriptors (action + prompt) available to
1388
- * `playerId` in the current phase. Surfaces the same projection the UI
1389
- * SDK consumes, so scenarios can assert addressee authorization and
1390
- * availability without reaching into game-specific state. Returned as
1391
- * loose records — the typed `InteractionDescriptor` shape is narrowed
1392
- * downstream in the workspace-generated `testing-contract.ts`.
1393
- */
1394
- interactionsForPlayer(playerId) {
1395
- return this.projectAllSeats().seats[playerId]?.availableInteractions ?? [];
1396
- }
1397
- explain(playerId, interactionId) {
1398
- const runtime = this.runtime;
1399
- if (typeof runtime.explainInteraction !== "function") {
1400
- throw new Error(
1401
- "The installed @dreamboard-games/sdk does not expose interaction explanations. Update the workspace SDK pin."
1402
- );
1403
- }
1404
- return runtime.explainInteraction({ playerId, interactionId });
1405
- }
1406
- clearHistory() {
1407
- this.historyRecords.length = 0;
1408
- }
1409
- async applyInput(input) {
1410
- const parsedInput = toReducerInput(input);
1411
- const result = await this.runtime.dispatch({ input: parsedInput });
1412
- if (result.kind === "reject") {
1413
- const record = {
1414
- input,
1415
- accepted: false,
1416
- errorCode: result.errorCode,
1417
- message: result.message
1418
- };
1419
- this.historyRecords.push(record);
1420
- throw createSubmissionError(
1421
- result.errorCode,
1422
- result.message,
1423
- "Reducer input rejected."
1424
- );
1425
- }
1426
- this.version += 1;
1427
- this.historyRecords.push({ input, accepted: true });
1428
- }
1429
- async submitInteraction(playerId, interactionId, params) {
1430
- await this.applyInput({
1431
- kind: "interaction",
1432
- playerId,
1433
- interactionId,
1434
- params
1435
- });
1436
- }
1437
- };
1438
- function toReducerInput(input) {
1439
- return {
1440
- kind: "interaction",
1441
- playerId: input.playerId,
1442
- interactionId: input.interactionId,
1443
- params: input.params
1444
- };
1445
- }
1446
- var RemoteGameplayTracker = class {
1447
- constructor(sessionId, playerId) {
1448
- this.sessionId = sessionId;
1449
- this.playerId = playerId;
1450
- this.state.playerId = playerId;
1451
- }
1452
- sessionId;
1453
- playerId;
1454
- state = {
1455
- version: -1,
1456
- actionSetVersion: "",
1457
- currentPhase: null,
1458
- playerId: "player-1",
1459
- view: null,
1460
- availableInteractions: []
1461
- };
1462
- liveBootstrap = false;
1463
- async bootstrap() {
1464
- const { data, error, response } = await getSessionSnapshot({
1465
- path: { sessionId: this.sessionId },
1466
- query: { playerId: this.playerId }
1467
- });
1468
- if (error || !data || data.type !== "gameplay") {
1469
- throw toDreamboardApiError(
1470
- error ?? { detail: "Gameplay snapshot was unavailable." },
1471
- response,
1472
- "Failed to load the remote gameplay bootstrap snapshot"
1473
- );
1474
- }
1475
- if (data.gameplay.actionSetVersion === "authority") {
1476
- return;
1477
- }
1478
- this.liveBootstrap = true;
1479
- this.applyGameplay(data.gameplay);
1480
- }
1481
- hasLiveBootstrap() {
1482
- return this.liveBootstrap;
1483
- }
1484
- snapshot() {
1485
- return {
1486
- ...this.state,
1487
- availableInteractions: [...this.state.availableInteractions]
1488
- };
1489
- }
1490
- applyShadow(shadow) {
1491
- const projection = shadow.projectAllSeats();
1492
- const playerId = this.playerId;
1493
- const seat = projection.seats[playerId];
1494
- this.state.version = shadow.currentVersion();
1495
- this.state.actionSetVersion = "";
1496
- this.state.playerId = playerId;
1497
- this.state.currentPhase = shadow.phase();
1498
- this.state.view = seat?.view ?? null;
1499
- this.state.availableInteractions = shadow.availableInteractionsForPlayer(playerId);
1500
- }
1501
- applyGameplay(gameplay) {
1502
- const playerId = gameplay.perspectivePlayerId;
1503
- const seat = gameplay.seats[playerId];
1504
- this.state.version = gameplay.version;
1505
- this.state.actionSetVersion = gameplay.actionSetVersion;
1506
- this.state.playerId = playerId;
1507
- this.state.currentPhase = gameplay.shared.currentPhase;
1508
- this.state.view = parseSeatView(seat?.view ?? "null");
1509
- this.state.availableInteractions = (seat?.availableInteractionRefs ?? []).map((ref) => gameplay.interactionsByRef[ref]).filter(
1510
- (interaction) => Boolean(interaction)
1511
- ).map((interaction) => interaction.interactionId);
1512
- }
1513
- };
1514
- function parseSeatView(raw) {
1515
- return raw ? parseJsonValue(raw) : null;
1516
- }
1517
- function sleep(ms) {
1518
- return new Promise((resolve) => setTimeout(resolve, ms));
1519
- }
1520
- function formatIssuePath(label, path3) {
1521
- let formatted = label;
1522
- for (const segment of path3) {
1523
- formatted += typeof segment === "number" ? `[${segment}]` : `.${String(segment)}`;
1524
- }
1525
- return formatted;
1526
- }
1527
- function parseWirePayload(methodName, value, label, schema) {
1528
- const parsed = schema.safeParse(value);
1529
- if (parsed.success) {
1530
- return parsed.data;
1531
- }
1532
- const firstIssue = parsed.error.issues[0];
1533
- const formattedPath = formatIssuePath(label, firstIssue?.path ?? []);
1534
- throw new Error(
1535
- `Reducer bundle returned invalid payload for '${methodName}': ${formattedPath} ${firstIssue?.message ?? "failed schema validation"}.`
1536
- );
1537
- }
1538
- function assertDispatchResultWireContract(result) {
1539
- return parseWirePayload(
1540
- "dispatch",
1541
- result,
1542
- "DispatchResult",
1543
- zod_exports.DispatchResultSchema
1544
- );
1545
- }
1546
- async function createBrowserBridgeClient(page) {
1547
- const bridgeExists = async () => page.evaluate(
1548
- () => Boolean(
1549
- window.__dreamboardTestBridge__
1550
- )
1551
- );
1552
- const startedAt = Date.now();
1553
- while (!await bridgeExists()) {
1554
- if (Date.now() - startedAt > DEFAULT_TIMEOUT_MS2) {
1555
- const bodyText = await page.locator("body").innerText().catch(() => "");
1556
- const diagnostic = bodyText.trim().replace(/\s+/g, " ").slice(0, 400);
1557
- throw new Error(
1558
- diagnostic.length > 0 ? `Timed out waiting for browser test bridge. Page text: ${diagnostic}` : "Timed out waiting for browser test bridge."
1559
- );
1560
- }
1561
- await sleep(50);
1562
- }
1563
- return {
1564
- snapshot: () => page.evaluate(
1565
- () => window.__dreamboardTestBridge__.snapshot()
1566
- ),
1567
- submitInteraction: (playerId, interactionId, params) => page.evaluate(
1568
- ([nextPlayerId, nextInteractionId, nextParams]) => window.__dreamboardTestBridge__.submitInteraction(
1569
- nextPlayerId,
1570
- nextInteractionId,
1571
- nextParams
1572
- ),
1573
- [playerId, interactionId, params]
1574
- ),
1575
- waitForVersionChange: async (previousVersion, timeoutMs = DEFAULT_TIMEOUT_MS2) => {
1576
- const started = Date.now();
1577
- while (Date.now() - started < timeoutMs) {
1578
- const snapshot = await page.evaluate(
1579
- () => window.__dreamboardTestBridge__.snapshot()
1580
- );
1581
- if (snapshot.version > previousVersion) {
1582
- return snapshot;
1583
- }
1584
- await sleep(50);
1585
- }
1586
- throw new Error("Timed out waiting for browser gameplay version change.");
1587
- }
1588
- };
1589
- }
1590
- async function assertLiveMatchesShadow(runner, shadow, live) {
1591
- if (runner === "reducer") {
1592
- return;
1593
- }
1594
- const currentPhase = "currentPhase" in live ? live.currentPhase : shadow.phase();
1595
- if (currentPhase !== shadow.phase()) {
1596
- throw new Error(
1597
- `Live phase '${String(currentPhase)}' diverged from reducer phase '${shadow.phase()}'.`
1598
- );
1599
- }
1600
- const livePlayerId = "playerId" in live ? live.playerId : live.controllingPlayerId;
1601
- const liveView = "view" in live ? live.view : null;
1602
- if (livePlayerId && !deepEqual(liveView, await shadow.view(livePlayerId))) {
1603
- throw new Error("Live projected views diverged from reducer shadow views.");
1604
- }
1605
- const liveInteractions = "availableInteractions" in live ? live.availableInteractions : null;
1606
- if (livePlayerId && liveInteractions && !deepEqual(
1607
- liveInteractions,
1608
- shadow.availableInteractionsForPlayer(livePlayerId)
1609
- )) {
1610
- throw new Error(
1611
- "Live available-interactions metadata diverged from reducer shadow state."
1612
- );
1613
- }
1614
- }
1615
- async function loadBrowserDriver(projectRoot) {
1616
- const filePath = path2.join(projectRoot, "test", "browser-driver.ts");
1617
- if (!await exists(filePath)) {
1618
- return null;
1619
- }
1620
- const module = await importTypeScriptModule(filePath);
1621
- return module.default ?? module;
1622
- }
1623
- async function openBrowserPage(playUrl, config) {
1624
- configurePlaywrightBrowsersPath();
1625
- const { chromium } = await import("playwright");
1626
- const browser = await chromium.launch({ headless: true });
1627
- const context = await browser.newContext({
1628
- viewport: { width: 1440, height: 900 }
1629
- });
1630
- const initScript = await buildBrowserAuthInitScript(config);
1631
- if (initScript) {
1632
- await context.addInitScript({ content: initScript });
1633
- }
1634
- const page = await context.newPage();
1635
- await page.goto(playUrl, { waitUntil: "domcontentloaded" });
1636
- return { browser, page };
1637
- }
1638
- function sanitizeSnapshotSegment(value) {
1639
- return value.replace(/[^a-zA-Z0-9._-]+/g, "-");
1640
- }
1641
- function createScenarioSnapshotMatcher(options) {
1642
- return (filename, actual) => {
1643
- const suffix = filename ? `.${sanitizeSnapshotSegment(filename)}` : "";
1644
- const snapshotPath = path2.join(
1645
- options.projectRoot,
1646
- "test",
1647
- "generated",
1648
- "snapshots",
1649
- `${sanitizeSnapshotSegment(options.scenarioId)}${suffix}.snapshot.json`
1650
- );
1651
- const wrappedValue = {
1652
- value: actual
1653
- };
1654
- const serialized = `${JSON.stringify(wrappedValue, null, 2)}
1655
- `;
1656
- mkdirSync(path2.dirname(snapshotPath), { recursive: true });
1657
- if (!existsSync(snapshotPath) || options.updateSnapshots) {
1658
- writeFileSync(snapshotPath, serialized, "utf8");
1659
- return;
1660
- }
1661
- const previous = JSON.parse(readFileSync(snapshotPath, "utf8"));
1662
- if (!deepEqual(previous.value, actual)) {
1663
- throw new Error(
1664
- `Snapshot mismatch for scenario '${options.scenarioId}'. Re-run with --update-snapshots to refresh ${path2.relative(options.projectRoot, snapshotPath)}.`
1665
- );
1666
- }
1667
- };
1668
- }
1669
- async function createScenarioContext(options) {
1670
- const api = {
1671
- start: async () => {
1672
- await options.shadow.start();
1673
- if (options.runner === "remote" && options.remote) {
1674
- if (options.remote.tracker.hasLiveBootstrap()) {
1675
- const live = options.remote.tracker.snapshot();
1676
- await assertLiveMatchesShadow("remote", options.shadow, live);
1677
- } else {
1678
- options.remote.tracker.applyShadow(options.shadow);
1679
- }
1680
- }
1681
- if (options.runner === "browser" && options.browser) {
1682
- const live = await options.browser.bridge.snapshot();
1683
- await assertLiveMatchesShadow("browser", options.shadow, live);
1684
- }
1685
- },
1686
- patchState: async (mutator) => {
1687
- await api.start();
1688
- if (options.live || options.remote || options.browser || options.actionPlan) {
1689
- throw new Error(
1690
- "game.patchState is only supported for reducer snapshot scenarios. Use it to materialize a state before --from-scenario or reducer tests, not inside live replay/browser runners."
1691
- );
1692
- }
1693
- options.shadow.patchState(mutator);
1694
- },
1695
- submit: async (playerId, interactionId, params) => {
1696
- await api.start();
1697
- const interactionParams = params ?? {};
1698
- const normalizedInputs = interactionParams && typeof interactionParams === "object" && !Array.isArray(interactionParams) ? interactionParams : {};
1699
- const previousVersion = options.live ? options.live.version : options.actionPlan ? options.shadow.currentVersion() : options.runner === "remote" ? options.remote?.tracker.snapshot().version ?? 0 : options.runner === "browser" ? (await options.browser?.bridge.snapshot())?.version ?? 0 : 0;
1700
- if (options.live) {
1701
- if (!options.live.actionSetVersion) {
1702
- options.live.actionSetVersion = await fetchLiveActionSetVersion(
1703
- options.live.sessionId,
1704
- playerId
1705
- );
1706
- }
1707
- const submittedActionSetVersion = options.live.actionSetVersion;
1708
- const submitStartedAt = performance.now();
1709
- const clientActionId = randomUUID2();
1710
- const { data, error, response } = await submitGameplayAuthorityAction({
1711
- path: {
1712
- sessionId: options.live.sessionId,
1713
- playerId,
1714
- interactionId
1715
- },
1716
- body: {
1717
- expectedVersion: previousVersion,
1718
- actionSetVersion: options.live.actionSetVersion,
1719
- inputs: normalizedInputs
1720
- },
1721
- headers: { "X-Dreamboard-Client-Action-Id": clientActionId }
1722
- });
1723
- const durationMs = performance.now() - submitStartedAt;
1724
- options.live.diagnostics?.push({
1725
- index: options.live.submitIndex ?? 0,
1726
- playerId,
1727
- interactionId,
1728
- inputs: normalizedInputs,
1729
- expectedVersion: previousVersion,
1730
- actionSetVersion: submittedActionSetVersion,
1731
- responseStatus: response?.status,
1732
- responseBody: options.live.captureResponseBody === false ? void 0 : JSON.stringify(error ?? data ?? null),
1733
- accepted: data?.accepted ?? void 0,
1734
- errorCode: data?.accepted === false ? data.errorCode ?? void 0 : error && typeof error === "object" && "detail" in error ? String(error.detail) : void 0,
1735
- backendTiming: response?.headers?.get("server-timing") ?? response?.headers?.get("x-dreamboard-timing") ?? void 0,
1736
- durationMs
1737
- });
1738
- options.live.submitIndex = (options.live.submitIndex ?? 0) + 1;
1739
- if (error) {
1740
- throw createSubmissionError(
1741
- "api-error",
1742
- void 0,
1743
- "Failed to submit interaction"
1744
- );
1745
- }
1746
- if (data?.accepted === false) {
1747
- const shadowError = await tryShadowSubmit(
1748
- options.shadow,
1749
- playerId,
1750
- interactionId,
1751
- interactionParams
1752
- );
1753
- if (!shadowError || shadowError.errorCode !== data.errorCode) {
1754
- throw new Error(
1755
- "Live session rejection diverged from reducer shadow state."
1756
- );
1757
- }
1758
- throw createSubmissionError(
1759
- data.errorCode ?? void 0,
1760
- data.message ?? void 0,
1761
- "Interaction rejected"
1762
- );
1763
- }
1764
- options.live.version = typeof data?.version === "number" ? data.version : previousVersion + 1;
1765
- options.live.actionSetVersion = data?.actionSetVersion ?? options.live.actionSetVersion;
1766
- } else if (options.runner === "remote" && options.remote) {
1767
- } else if (options.runner === "browser" && options.browser) {
1768
- const handled = await options.browser.driver?.interaction?.(options.browser.bridge, {
1769
- playerId,
1770
- interactionId,
1771
- params: interactionParams
1772
- }) === true;
1773
- if (!handled) {
1774
- await options.browser.bridge.submitInteraction(
1775
- playerId,
1776
- interactionId,
1777
- interactionParams
1778
- );
1779
- }
1780
- }
1781
- await options.shadow.submitInteraction(
1782
- playerId,
1783
- interactionId,
1784
- interactionParams
1785
- );
1786
- if (options.actionPlan) {
1787
- options.actionPlan.diagnostics.push({
1788
- index: options.actionPlan.submitIndex ?? 0,
1789
- playerId,
1790
- interactionId,
1791
- inputs: normalizedInputs,
1792
- expectedVersion: previousVersion,
1793
- actionSetVersion: "",
1794
- accepted: true,
1795
- durationMs: 0
1796
- });
1797
- options.actionPlan.submitIndex = (options.actionPlan.submitIndex ?? 0) + 1;
1798
- }
1799
- if (options.runner === "remote" && options.remote) {
1800
- options.remote.tracker.applyShadow(options.shadow);
1801
- await assertLiveMatchesShadow(
1802
- "remote",
1803
- options.shadow,
1804
- options.remote.tracker.snapshot()
1805
- );
1806
- }
1807
- if (options.runner === "browser" && options.browser) {
1808
- const live = await options.browser.bridge.waitForVersionChange(previousVersion);
1809
- await assertLiveMatchesShadow("browser", options.shadow, live);
1810
- }
1811
- }
1812
- };
1813
- return {
1814
- game: api,
1815
- players: () => options.shadow.playerOrder(),
1816
- seat: (index) => {
1817
- const order = options.shadow.playerOrder();
1818
- if (!Number.isInteger(index) || index < 0 || index >= order.length) {
1819
- throw new Error(
1820
- `seat(${index}) is out of range; scenario has ${order.length} player(s).`
1821
- );
1822
- }
1823
- return order[index];
1824
- },
1825
- state: () => options.shadow.phase(),
1826
- view: (playerId) => options.shadow.view(playerId),
1827
- interactions: (playerId) => options.shadow.interactionsForPlayer(playerId),
1828
- explain: (playerId, interactionId) => options.shadow.explain(playerId, interactionId),
1829
- expect: options.expect
1830
- };
1831
- }
1832
- async function tryShadowSubmit(shadow, playerId, interactionId, params) {
1833
- try {
1834
- await shadow.submitInteraction(playerId, interactionId, params);
1835
- throw new Error("Expected shadow interaction to reject.");
1836
- } catch (error) {
1837
- if (error instanceof Error && error.name === "SubmissionError") {
1838
- return error;
1839
- }
1840
- throw error;
1841
- }
1842
- }
1843
- async function createSessionFromScenario(options) {
1844
- const startedAt = Date.now();
1845
- const materialized = await materializeScenarioReducerState(options);
1846
- const reducerHarnessMs = Date.now() - startedAt;
1847
- const hydrateStartedAt = Date.now();
1848
- const {
1849
- data: snapshot,
1850
- error,
1851
- response
1852
- } = await createProjectSessionFromReducerSnapshot({
1853
- path: { projectId: options.projectId },
1854
- body: {
1855
- compiledResultId: options.compiledResultId,
1856
- seed: materialized.seed,
1857
- playerCount: materialized.playerCount,
1858
- setupProfileId: materialized.setupProfileId ?? void 0,
1859
- baseId: materialized.baseId,
1860
- scenarioId: materialized.scenarioId,
1861
- reducerState: materialized.reducerState,
1862
- reducerStateVersion: materialized.reducerStateVersion,
1863
- fingerprintMetadata: materialized.fingerprintMetadata
1864
- }
1865
- });
1866
- const backendHydrateMs = Date.now() - hydrateStartedAt;
1867
- if (error || !snapshot) {
1868
- throw toDreamboardApiError(
1869
- error,
1870
- response,
1871
- `Failed to materialize scenario '${options.scenarioId}' from reducer snapshot`
1872
- );
1873
- }
1874
- return {
1875
- sessionId: snapshot.context.sessionId,
1876
- shortCode: snapshot.context.shortCode,
1877
- gameId: projectIdFromSessionGameSource(snapshot.context.gameSource),
1878
- seed: materialized.seed,
1879
- playerCount: materialized.playerCount,
1880
- setupProfileId: materialized.setupProfileId,
1881
- scenarioId: materialized.scenarioId,
1882
- phase: materialized.phase,
1883
- stage: materialized.stage,
1884
- materialization: {
1885
- mode: "snapshot",
1886
- totalMs: Date.now() - startedAt,
1887
- reducerHarnessMs,
1888
- backendHydrateMs
1889
- }
1890
- };
1891
- }
1892
- var MATERIALIZED_SCENARIO_CACHE_VERSION = 1;
1893
- function materializedScenarioCachePath(options) {
1894
- return path2.join(
1895
- options.projectRoot,
1896
- PROJECT_DIR_NAME,
1897
- "dev",
1898
- "scenario-cache",
1899
- `${sanitizeSnapshotSegment(options.baseId)}.${sanitizeSnapshotSegment(
1900
- options.scenarioId
1901
- )}.${options.fingerprintHash}.json`
1902
- );
1903
- }
1904
- async function readMaterializedScenarioCache(options) {
1905
- const cachePath = materializedScenarioCachePath(options);
1906
- const text = await readTextFileIfExists(cachePath);
1907
- if (!text) return null;
1908
- try {
1909
- const payload = JSON.parse(
1910
- text
1911
- );
1912
- if (payload.cacheVersion !== MATERIALIZED_SCENARIO_CACHE_VERSION || payload.fingerprintHash !== options.fingerprintHash || !payload.materialized) {
1913
- return null;
1914
- }
1915
- return payload.materialized;
1916
- } catch {
1917
- return null;
1918
- }
1919
- }
1920
- async function writeMaterializedScenarioCache(options) {
1921
- const cachePath = materializedScenarioCachePath(options);
1922
- await ensureDir(path2.dirname(cachePath));
1923
- await writeJsonFile(cachePath, {
1924
- cacheVersion: MATERIALIZED_SCENARIO_CACHE_VERSION,
1925
- fingerprintHash: options.fingerprintHash,
1926
- materialized: options.materialized
1927
- });
1928
- }
1929
- async function materializeScenarioReducerState(options) {
1930
- await ensureReducerNativeTestingFiles(options.projectRoot);
1931
- let preloadedGeneratedBaseStates;
1932
- if (options.trustGeneratedFingerprint === true) {
1933
- const [generatedBaseStates2, scenarioManifest] = await Promise.all([
1934
- loadGeneratedBaseStates(options.projectRoot),
1935
- loadGeneratedScenarioManifest(options.projectRoot)
1936
- ]);
1937
- preloadedGeneratedBaseStates = generatedBaseStates2;
1938
- const scenarioEntry = scenarioManifest.find(
1939
- (entry) => entry.id === options.scenarioId
1940
- );
1941
- const generatedBase2 = scenarioEntry ? generatedBaseStates2?.[baseStateKey(scenarioEntry.base)] : null;
1942
- if (generatedBase2 && generatedBase2.fingerprint.compiledResultId === options.compiledResultId && generatedBase2.fingerprint.gameId === options.gameId) {
1943
- const cached2 = await readMaterializedScenarioCache({
1944
- projectRoot: options.projectRoot,
1945
- baseId: scenarioEntry.base,
1946
- scenarioId: options.scenarioId,
1947
- fingerprintHash: fingerprintHash(generatedBase2.fingerprint)
1948
- });
1949
- if (cached2) {
1950
- return cached2;
1951
- }
1952
- }
1953
- }
1954
- const [bases, scenarios, generatedBaseStates, manifest] = await Promise.all([
1955
- loadTypedBases(options.projectRoot),
1956
- loadTypedScenarios(options.projectRoot, {}),
1957
- preloadedGeneratedBaseStates === void 0 ? loadGeneratedBaseStates(options.projectRoot) : Promise.resolve(preloadedGeneratedBaseStates),
1958
- loadManifest(options.projectRoot)
1959
- ]);
1960
- const matchingScenarios = scenarios.filter(
1961
- (scenario2) => scenario2.definition.id === options.scenarioId
1962
- );
1963
- if (matchingScenarios.length === 0) {
1964
- throw new Error(
1965
- `Unknown scenario '${options.scenarioId}'. Available scenarios: ${scenarios.map((scenario2) => scenario2.definition.id).sort().join(", ")}`
1966
- );
1967
- }
1968
- if (matchingScenarios.length > 1) {
1969
- throw new Error(
1970
- `Scenario id '${options.scenarioId}' is defined more than once. Keep one scenario per file and one unique id per workspace.`
1971
- );
1972
- }
1973
- const scenario = matchingScenarios[0];
1974
- const basesById = new Map(bases.map((base2) => [base2.definition.id, base2]));
1975
- validateTypedScenarioBases([scenario], basesById);
1976
- const base = basesById.get(scenario.definition.from);
1977
- if (!base) {
1978
- throw new Error(
1979
- `Missing typed base '${scenario.definition.from}' for scenario '${scenario.definition.id}'.`
1980
- );
1981
- }
1982
- const generatedBase = generatedBaseStates?.[baseStateKey(base.definition.id)];
1983
- if (!generatedBase) {
1984
- throw new Error(
1985
- `Missing generated base artifact for '${base.definition.id}'. Run 'dreamboard test generate' before using --from-scenario.`
1986
- );
1987
- }
1988
- if (typeof generatedBase.version !== "number") {
1989
- throw new Error(
1990
- `Generated base artifact for '${base.definition.id}' is stale. Run 'dreamboard test generate' before using --from-scenario.`
1991
- );
1992
- }
1993
- const canTrustGeneratedFingerprint = options.trustGeneratedFingerprint === true && generatedBase.fingerprint.compiledResultId === options.compiledResultId && generatedBase.fingerprint.gameId === options.gameId;
1994
- const current = canTrustGeneratedFingerprint ? generatedBase.fingerprint : await currentFingerprint({
1995
- projectRoot: options.projectRoot,
1996
- base,
1997
- basesById,
1998
- compiledResultId: options.compiledResultId,
1999
- gameId: options.gameId
2000
- });
2001
- if (!canTrustGeneratedFingerprint) {
2002
- validateGeneratedFingerprint({
2003
- generated: generatedBase.fingerprint,
2004
- current
2005
- });
2006
- }
2007
- const currentHash = fingerprintHash(current);
2008
- const cached = await readMaterializedScenarioCache({
2009
- projectRoot: options.projectRoot,
2010
- baseId: base.definition.id,
2011
- scenarioId: scenario.definition.id,
2012
- fingerprintHash: currentHash
2013
- });
2014
- if (cached) {
2015
- return cached;
2016
- }
2017
- const [gameModule, manifestContractModule] = await Promise.all([
2018
- importTypeScriptModule(
2019
- path2.join(options.projectRoot, "app", "game.ts")
2020
- ),
2021
- importTypeScriptModule(
2022
- path2.join(options.projectRoot, "shared", "manifest-contract.ts")
2023
- )
2024
- ]);
2025
- const createInitialTable = typeof manifestContractModule.createInitialTable === "function" ? manifestContractModule.createInitialTable : null;
2026
- const resolvedBase = resolveBaseDefinition(base, basesById);
2027
- const effectiveSetup = resolveEffectiveBaseSetup({
2028
- manifest,
2029
- base: base.definition,
2030
- basesById
2031
- });
2032
- const shadow = new ShadowReducerRuntime(
2033
- manifest,
2034
- gameModule.default,
2035
- createInitialTable,
2036
- resolvedBase.seed,
2037
- resolvedBase.players,
2038
- effectiveSetup.setupProfileId,
2039
- options.debug ?? false
2040
- );
2041
- shadow.hydrate(generatedBase.snapshot, generatedBase.version);
2042
- const context = await createScenarioContext({
2043
- runner: "reducer",
2044
- shadow,
2045
- expect: (await loadTestingExpectApiFactory())()
2046
- });
2047
- shadow.clearHistory();
2048
- await scenario.definition.when(context);
2049
- if (scenario.definition.phase && shadow.phase() !== scenario.definition.phase) {
2050
- throw new Error(
2051
- `Scenario '${scenario.definition.id}' expected phase '${scenario.definition.phase}' but reached '${shadow.phase()}'.`
2052
- );
2053
- }
2054
- if (scenario.definition.stage && shadow.currentStage() !== scenario.definition.stage) {
2055
- throw new Error(
2056
- `Scenario '${scenario.definition.id}' expected stage '${scenario.definition.stage}' but reached '${shadow.currentStage() ?? "null"}'.`
2057
- );
2058
- }
2059
- const materialized = {
2060
- baseId: base.definition.id,
2061
- scenarioId: scenario.definition.id,
2062
- seed: resolvedBase.seed,
2063
- playerCount: resolvedBase.players,
2064
- setupProfileId: effectiveSetup.setupProfileId,
2065
- reducerState: shadow.rawState(),
2066
- reducerStateVersion: shadow.currentVersion(),
2067
- fingerprintMetadata: {
2068
- baseFingerprintHash: fingerprintHash(generatedBase.fingerprint),
2069
- currentFingerprintHash: fingerprintHash(current),
2070
- baseBundleHash: generatedBase.fingerprint.baseBundleHash,
2071
- manifestHash: generatedBase.fingerprint.manifestHash,
2072
- appBundleHash: generatedBase.fingerprint.appBundleHash,
2073
- uiContractHash: generatedBase.fingerprint.uiContractHash,
2074
- baseId: base.definition.id,
2075
- scenarioId: scenario.definition.id
2076
- },
2077
- phase: shadow.phase(),
2078
- stage: shadow.currentStage()
2079
- };
2080
- await writeMaterializedScenarioCache({
2081
- projectRoot: options.projectRoot,
2082
- baseId: base.definition.id,
2083
- scenarioId: scenario.definition.id,
2084
- fingerprintHash: currentHash,
2085
- materialized
2086
- });
2087
- return materialized;
2088
- }
2089
- async function replayScenarioThroughBackend(options) {
2090
- await ensureReducerNativeTestingFiles(options.projectRoot);
2091
- const [
2092
- bases,
2093
- scenarios,
2094
- generatedBaseStates,
2095
- manifest,
2096
- gameModule,
2097
- manifestContractModule
2098
- ] = await Promise.all([
2099
- loadTypedBases(options.projectRoot),
2100
- loadTypedScenarios(options.projectRoot, {}),
2101
- loadGeneratedBaseStates(options.projectRoot),
2102
- loadManifest(options.projectRoot),
2103
- importTypeScriptModule(
2104
- path2.join(options.projectRoot, "app", "game.ts")
2105
- ),
2106
- importTypeScriptModule(
2107
- path2.join(options.projectRoot, "shared", "manifest-contract.ts")
2108
- )
2109
- ]);
2110
- const matchingScenarios = scenarios.filter(
2111
- (scenario2) => scenario2.definition.id === options.scenarioId
2112
- );
2113
- if (matchingScenarios.length !== 1) {
2114
- throw new Error(
2115
- matchingScenarios.length === 0 ? `Unknown scenario '${options.scenarioId}'. Available scenarios: ${scenarios.map((scenario2) => scenario2.definition.id).sort().join(", ")}` : `Scenario id '${options.scenarioId}' is defined more than once. Keep one scenario per file and one unique id per workspace.`
2116
- );
2117
- }
2118
- const scenario = matchingScenarios[0];
2119
- const basesById = new Map(bases.map((base2) => [base2.definition.id, base2]));
2120
- validateTypedScenarioBases([scenario], basesById);
2121
- const base = basesById.get(scenario.definition.from);
2122
- if (!base) {
2123
- throw new Error(
2124
- `Missing typed base '${scenario.definition.from}' for scenario '${scenario.definition.id}'.`
2125
- );
2126
- }
2127
- const generatedBase = generatedBaseStates?.[baseStateKey(base.definition.id)];
2128
- if (!generatedBase) {
2129
- throw new Error(
2130
- `Missing generated base artifact for '${base.definition.id}'. Run 'dreamboard test generate' before replaying a scenario.`
2131
- );
2132
- }
2133
- validateGeneratedFingerprint({
2134
- generated: generatedBase.fingerprint,
2135
- current: await currentFingerprint({
2136
- projectRoot: options.projectRoot,
2137
- base,
2138
- basesById,
2139
- compiledResultId: options.compiledResultId,
2140
- gameId: options.gameId
2141
- })
2142
- });
2143
- const createInitialTable = typeof manifestContractModule.createInitialTable === "function" ? manifestContractModule.createInitialTable : null;
2144
- const resolvedBase = resolveBaseDefinition(base, basesById);
2145
- const effectiveSetup = resolveEffectiveBaseSetup({
2146
- manifest,
2147
- base: base.definition,
2148
- basesById
2149
- });
2150
- const shadow = new ShadowReducerRuntime(
2151
- manifest,
2152
- gameModule.default,
2153
- createInitialTable,
2154
- resolvedBase.seed,
2155
- resolvedBase.players,
2156
- effectiveSetup.setupProfileId,
2157
- options.debug ?? false
2158
- );
2159
- if (base.definition.extends) {
2160
- const parentArtifact = generatedBaseStates?.[baseStateKey(base.definition.extends)];
2161
- if (!parentArtifact || typeof parentArtifact.version !== "number") {
2162
- throw new Error(
2163
- `Base '${base.definition.id}' extends '${base.definition.extends}', but the parent artifact is missing or stale. Run 'dreamboard test generate' first.`
2164
- );
2165
- }
2166
- shadow.hydrate(parentArtifact.snapshot, parentArtifact.version);
2167
- }
2168
- const {
2169
- data: session,
2170
- error: sessionError,
2171
- response: sessionResponse
2172
- } = await createProjectSession({
2173
- path: { projectId: options.gameId },
2174
- body: {
2175
- compiledResultId: options.compiledResultId,
2176
- seed: resolvedBase.seed,
2177
- playerCount: resolvedBase.players,
2178
- autoAssignSeats: true,
2179
- setupProfileId: effectiveSetup.setupProfileId ?? void 0
2180
- }
2181
- });
2182
- if (!session || sessionError) {
2183
- throw toDreamboardApiError(
2184
- sessionError,
2185
- sessionResponse,
2186
- `Failed to create replay session for '${scenario.definition.id}'`
2187
- );
2188
- }
2189
- const { error: startError, response: startResponse } = await startGame({
2190
- path: { sessionId: session.sessionId }
2191
- });
2192
- if (startError) {
2193
- throw toDreamboardApiError(
2194
- startError,
2195
- startResponse,
2196
- `Failed to start replay session for '${scenario.definition.id}'`
2197
- );
2198
- }
2199
- const diagnostics = [];
2200
- const context = await createScenarioContext({
2201
- runner: "reducer",
2202
- shadow,
2203
- expect: (await loadTestingExpectApiFactory())(),
2204
- live: {
2205
- sessionId: session.sessionId,
2206
- version: 0,
2207
- actionSetVersion: "",
2208
- submitIndex: 0,
2209
- diagnostics,
2210
- captureResponseBody: options.captureResponseBody
2211
- }
2212
- });
2213
- await context.game.start();
2214
- await base.definition.setup({
2215
- game: context.game,
2216
- players: context.players,
2217
- seat: context.seat
2218
- });
2219
- shadow.clearHistory();
2220
- await scenario.definition.when(context);
2221
- if (scenario.definition.phase && shadow.phase() !== scenario.definition.phase) {
2222
- throw new Error(
2223
- `Scenario '${scenario.definition.id}' expected phase '${scenario.definition.phase}' but reached '${shadow.phase()}'.`
2224
- );
2225
- }
2226
- if (scenario.definition.stage && shadow.currentStage() !== scenario.definition.stage) {
2227
- throw new Error(
2228
- `Scenario '${scenario.definition.id}' expected stage '${scenario.definition.stage}' but reached '${shadow.currentStage() ?? "null"}'.`
2229
- );
2230
- }
2231
- return {
2232
- sessionId: session.sessionId,
2233
- shortCode: session.shortCode,
2234
- gameId: projectIdFromSessionGameSource(session.gameSource),
2235
- seed: resolvedBase.seed,
2236
- playerCount: resolvedBase.players,
2237
- setupProfileId: effectiveSetup.setupProfileId,
2238
- scenarioId: scenario.definition.id,
2239
- phase: shadow.phase(),
2240
- stage: shadow.currentStage(),
2241
- diagnostics
2242
- };
2243
- }
2244
- async function createScenarioActionPlan(options) {
2245
- await ensureReducerNativeTestingFiles(options.projectRoot);
2246
- const [
2247
- bases,
2248
- scenarios,
2249
- generatedBaseStates,
2250
- manifest,
2251
- gameModule,
2252
- manifestContractModule
2253
- ] = await Promise.all([
2254
- loadTypedBases(options.projectRoot),
2255
- loadTypedScenarios(options.projectRoot, {}),
2256
- loadGeneratedBaseStates(options.projectRoot),
2257
- loadManifest(options.projectRoot),
2258
- importTypeScriptModule(
2259
- path2.join(options.projectRoot, "app", "game.ts")
2260
- ),
2261
- importTypeScriptModule(
2262
- path2.join(options.projectRoot, "shared", "manifest-contract.ts")
2263
- )
2264
- ]);
2265
- const matchingScenarios = scenarios.filter(
2266
- (scenario2) => scenario2.definition.id === options.scenarioId
2267
- );
2268
- if (matchingScenarios.length !== 1) {
2269
- throw new Error(
2270
- matchingScenarios.length === 0 ? `Unknown scenario '${options.scenarioId}'. Available scenarios: ${scenarios.map((scenario2) => scenario2.definition.id).sort().join(", ")}` : `Scenario id '${options.scenarioId}' is defined more than once. Keep one scenario per file and one unique id per workspace.`
2271
- );
2272
- }
2273
- const scenario = matchingScenarios[0];
2274
- const basesById = new Map(bases.map((base2) => [base2.definition.id, base2]));
2275
- validateTypedScenarioBases([scenario], basesById);
2276
- const base = basesById.get(scenario.definition.from);
2277
- if (!base) {
2278
- throw new Error(
2279
- `Missing typed base '${scenario.definition.from}' for scenario '${scenario.definition.id}'.`
2280
- );
2281
- }
2282
- const generatedBase = generatedBaseStates?.[baseStateKey(base.definition.id)];
2283
- if (!generatedBase) {
2284
- throw new Error(
2285
- `Missing generated base artifact for '${base.definition.id}'. Run 'dreamboard test generate' before replaying a scenario.`
2286
- );
2287
- }
2288
- validateGeneratedFingerprint({
2289
- generated: generatedBase.fingerprint,
2290
- current: await currentFingerprint({
2291
- projectRoot: options.projectRoot,
2292
- base,
2293
- basesById,
2294
- compiledResultId: options.compiledResultId,
2295
- gameId: options.gameId
2296
- })
2297
- });
2298
- const createInitialTable = typeof manifestContractModule.createInitialTable === "function" ? manifestContractModule.createInitialTable : null;
2299
- const resolvedBase = resolveBaseDefinition(base, basesById);
2300
- const effectiveSetup = resolveEffectiveBaseSetup({
2301
- manifest,
2302
- base: base.definition,
2303
- basesById
2304
- });
2305
- const shadow = new ShadowReducerRuntime(
2306
- manifest,
2307
- gameModule.default,
2308
- createInitialTable,
2309
- resolvedBase.seed,
2310
- resolvedBase.players,
2311
- effectiveSetup.setupProfileId,
2312
- options.debug ?? false
2313
- );
2314
- if (base.definition.extends) {
2315
- const parentArtifact = generatedBaseStates?.[baseStateKey(base.definition.extends)];
2316
- if (!parentArtifact || typeof parentArtifact.version !== "number") {
2317
- throw new Error(
2318
- `Base '${base.definition.id}' extends '${base.definition.extends}', but the parent artifact is missing or stale. Run 'dreamboard test generate' first.`
2319
- );
2320
- }
2321
- shadow.hydrate(parentArtifact.snapshot, parentArtifact.version);
2322
- }
2323
- const diagnostics = [];
2324
- const context = await createScenarioContext({
2325
- runner: "reducer",
2326
- shadow,
2327
- expect: (await loadTestingExpectApiFactory())(),
2328
- actionPlan: {
2329
- submitIndex: 0,
2330
- diagnostics
2331
- }
2332
- });
2333
- await context.game.start();
2334
- await base.definition.setup({
2335
- game: context.game,
2336
- players: context.players,
2337
- seat: context.seat
2338
- });
2339
- shadow.clearHistory();
2340
- await scenario.definition.when(context);
2341
- if (scenario.definition.phase && shadow.phase() !== scenario.definition.phase) {
2342
- throw new Error(
2343
- `Scenario '${scenario.definition.id}' expected phase '${scenario.definition.phase}' but reached '${shadow.phase()}'.`
2344
- );
2345
- }
2346
- if (scenario.definition.stage && shadow.currentStage() !== scenario.definition.stage) {
2347
- throw new Error(
2348
- `Scenario '${scenario.definition.id}' expected stage '${scenario.definition.stage}' but reached '${shadow.currentStage() ?? "null"}'.`
2349
- );
2350
- }
2351
- return {
2352
- gameId: options.gameId,
2353
- seed: resolvedBase.seed,
2354
- playerCount: resolvedBase.players,
2355
- setupProfileId: effectiveSetup.setupProfileId,
2356
- scenarioId: scenario.definition.id,
2357
- phase: shadow.phase(),
2358
- stage: shadow.currentStage(),
2359
- diagnostics
2360
- };
2361
- }
2362
- async function replayActionPlanThroughBackend(options) {
2363
- const session = await createActionPlanReplaySession(options);
2364
- return replayActionPlanInSession({
2365
- session,
2366
- actions: options.actions,
2367
- captureResponseBody: options.captureResponseBody
2368
- });
2369
- }
2370
- async function createActionPlanReplaySession(options) {
2371
- const {
2372
- data: session,
2373
- error: sessionError,
2374
- response: sessionResponse
2375
- } = await createProjectSession({
2376
- path: { projectId: options.gameId },
2377
- body: {
2378
- compiledResultId: options.compiledResultId,
2379
- seed: options.seed,
2380
- playerCount: options.playerCount,
2381
- autoAssignSeats: true,
2382
- setupProfileId: options.setupProfileId ?? void 0
2383
- }
2384
- });
2385
- if (!session || sessionError) {
2386
- throw toDreamboardApiError(
2387
- sessionError,
2388
- sessionResponse,
2389
- "Failed to create replay session from action plan"
2390
- );
2391
- }
2392
- const { error: startError, response: startResponse } = await startGame({
2393
- path: { sessionId: session.sessionId }
2394
- });
2395
- if (startError) {
2396
- throw toDreamboardApiError(
2397
- startError,
2398
- startResponse,
2399
- "Failed to start replay session from action plan"
2400
- );
2401
- }
2402
- return {
2403
- sessionId: session.sessionId,
2404
- shortCode: session.shortCode,
2405
- gameId: projectIdFromSessionGameSource(session.gameSource),
2406
- seed: options.seed,
2407
- playerCount: options.playerCount,
2408
- setupProfileId: options.setupProfileId ?? null,
2409
- scenarioId: options.scenarioId ?? "action-plan",
2410
- phase: null,
2411
- stage: null
2412
- };
2413
- }
2414
- async function replayActionPlanInSession(options) {
2415
- let version = 0;
2416
- let actionSetVersion = "";
2417
- const diagnostics = [];
2418
- for (const [index, action] of options.actions.entries()) {
2419
- if (index > 0) {
2420
- await waitForActionPacing({
2421
- actionIntervalMs: options.actionIntervalMs ?? 0,
2422
- actionJitterMs: options.actionJitterMs ?? 0
2423
- });
2424
- }
2425
- if (!actionSetVersion) {
2426
- actionSetVersion = await fetchLiveActionSetVersion(
2427
- options.session.sessionId,
2428
- action.playerId
2429
- );
2430
- }
2431
- const submittedActionSetVersion = actionSetVersion;
2432
- const submitStartedAt = performance.now();
2433
- const clientActionId = randomUUID2();
2434
- const { data, error, response } = await submitGameplayAuthorityAction({
2435
- path: {
2436
- sessionId: options.session.sessionId,
2437
- playerId: action.playerId,
2438
- interactionId: action.interactionId
2439
- },
2440
- body: {
2441
- expectedVersion: version,
2442
- actionSetVersion,
2443
- inputs: action.inputs ?? {}
2444
- },
2445
- headers: { "X-Dreamboard-Client-Action-Id": clientActionId }
2446
- });
2447
- const durationMs = performance.now() - submitStartedAt;
2448
- diagnostics.push({
2449
- index,
2450
- playerId: action.playerId,
2451
- interactionId: action.interactionId,
2452
- inputs: action.inputs ?? {},
2453
- expectedVersion: version,
2454
- actionSetVersion: submittedActionSetVersion,
2455
- responseStatus: response?.status,
2456
- responseBody: options.captureResponseBody === false ? void 0 : JSON.stringify(error ?? data ?? null),
2457
- accepted: data?.accepted ?? void 0,
2458
- errorCode: data?.accepted === false ? data.errorCode ?? void 0 : error && typeof error === "object" && "detail" in error ? String(error.detail) : void 0,
2459
- backendTiming: response?.headers?.get("server-timing") ?? response?.headers?.get("x-dreamboard-timing") ?? void 0,
2460
- durationMs
2461
- });
2462
- if (error) {
2463
- throw createSubmissionError(
2464
- "api-error",
2465
- void 0,
2466
- "Failed to submit interaction from action plan"
2467
- );
2468
- }
2469
- if (data?.accepted === false) {
2470
- throw createSubmissionError(
2471
- data.errorCode ?? void 0,
2472
- data.message ?? void 0,
2473
- "Interaction rejected while replaying action plan"
2474
- );
2475
- }
2476
- version = typeof data?.version === "number" ? data.version : version + 1;
2477
- actionSetVersion = data?.actionSetVersion ?? actionSetVersion;
2478
- }
2479
- return {
2480
- ...options.session,
2481
- diagnostics
2482
- };
2483
- }
2484
- async function fetchLiveActionSetVersion(sessionId, playerId) {
2485
- const { data, error, response } = await getSessionSnapshot({
2486
- path: { sessionId },
2487
- query: { playerId }
2488
- });
2489
- if (error || !data || data.type !== "gameplay") {
2490
- throw toDreamboardApiError(
2491
- error ?? { detail: "Gameplay snapshot was unavailable." },
2492
- response,
2493
- "Failed to read live gameplay action set version"
2494
- );
2495
- }
2496
- return data.gameplay.actionSetVersion;
2497
- }
2498
- async function waitForActionPacing(options) {
2499
- const intervalMs = Math.max(0, options.actionIntervalMs);
2500
- const jitterMs = Math.max(0, options.actionJitterMs);
2501
- if (intervalMs === 0 && jitterMs === 0) {
2502
- return;
2503
- }
2504
- const offsetMs = jitterMs === 0 ? 0 : Math.floor((Math.random() * 2 - 1) * jitterMs);
2505
- const delayMs = Math.max(0, intervalMs + offsetMs);
2506
- if (delayMs === 0) {
2507
- return;
2508
- }
2509
- await new Promise((resolve) => setTimeout(resolve, delayMs));
2510
- }
2511
- function baseStateKey(baseId) {
2512
- return baseId;
2513
- }
2514
- function fingerprintHash(fingerprint) {
2515
- return hashContent(JSON.stringify(fingerprint));
2516
- }
2517
- function sortBasesForArtifacts(bases, basesById) {
2518
- const ordered = [];
2519
- const visiting = /* @__PURE__ */ new Set();
2520
- const visited = /* @__PURE__ */ new Set();
2521
- const visit = (base) => {
2522
- if (visited.has(base.definition.id)) {
2523
- return;
2524
- }
2525
- if (visiting.has(base.definition.id)) {
2526
- throw new Error(
2527
- `Cyclic reducer-native base inheritance detected at '${base.definition.id}'.`
2528
- );
2529
- }
2530
- visiting.add(base.definition.id);
2531
- if (base.definition.extends) {
2532
- const parent = basesById.get(base.definition.extends);
2533
- if (!parent) {
2534
- throw new Error(
2535
- `Base '${base.definition.id}' extends unknown parent '${base.definition.extends}'.`
2536
- );
2537
- }
2538
- visit(parent);
2539
- }
2540
- visiting.delete(base.definition.id);
2541
- visited.add(base.definition.id);
2542
- ordered.push(base);
2543
- };
2544
- for (const base of bases) {
2545
- visit(base);
2546
- }
2547
- return ordered;
2548
- }
2549
- function writeProjectionSnapshots(options) {
2550
- const projectionDir = path2.join(
2551
- options.projectRoot,
2552
- "test",
2553
- "generated",
2554
- "bases",
2555
- options.baseId
2556
- );
2557
- rmSync(projectionDir, { recursive: true, force: true });
2558
- mkdirSync(projectionDir, { recursive: true });
2559
- const projection = options.shadow.projectAllSeats();
2560
- for (const [playerId, seatProjection] of Object.entries(projection.seats)) {
2561
- const outputPath = path2.join(projectionDir, `${playerId}.projection.json`);
2562
- writeFileSync(
2563
- outputPath,
2564
- `${JSON.stringify(
2565
- {
2566
- currentStage: projection.currentStage,
2567
- stageSeats: projection.stageSeats,
2568
- ...seatProjection
2569
- },
2570
- null,
2571
- 2
2572
- )}
2573
- `,
2574
- "utf8"
2575
- );
2576
- }
2577
- }
2578
- function validateTypedScenarioBases(scenarios, basesById) {
2579
- const available = new Set(basesById.keys());
2580
- const invalid = scenarios.filter((scenario) => !available.has(scenario.definition.from)).map(
2581
- (scenario) => path2.relative(process.cwd(), scenario.filePath) + ` -> '${scenario.definition.from}'`
2582
- );
2583
- if (invalid.length > 0) {
2584
- throw new Error(
2585
- `Unknown scenario base(s): ${invalid.join(", ")}. Available bases: ${Array.from(available).sort().join(", ")}`
2586
- );
2587
- }
2588
- }
2589
- async function writeReducerNativeGeneratedFiles(options) {
2590
- await ensureReducerNativeTestingFiles(options.projectRoot);
2591
- const manifest = await loadManifest(options.projectRoot);
2592
- const manifestHash = hashContent(JSON.stringify(manifest));
2593
- const appBundleHash = hashContent(
2594
- await bundleTypeScriptModuleText(
2595
- path2.join(options.projectRoot, "app", "game.ts")
2596
- )
2597
- );
2598
- const uiContractHash = hashContent(
2599
- await bundleTypeScriptModuleText(
2600
- path2.join(options.projectRoot, "shared", "generated", "ui-contract.ts"),
2601
- { external: SDK_UI_RUNTIME_EXTERNALS }
2602
- )
2603
- );
2604
- const generatedDir = path2.join(options.projectRoot, "test", "generated");
2605
- await ensureDir(generatedDir);
2606
- const baseStates = {};
2607
- const [gameModule, manifestContractModule] = await Promise.all([
2608
- importTypeScriptModule(
2609
- path2.join(options.projectRoot, "app", "game.ts")
2610
- ),
2611
- importTypeScriptModule(
2612
- path2.join(options.projectRoot, "shared", "manifest-contract.ts")
2613
- )
2614
- ]);
2615
- const createInitialTable = typeof manifestContractModule.createInitialTable === "function" ? manifestContractModule.createInitialTable : null;
2616
- const contractFingerprintValue = contractFingerprint(
2617
- gameModule.default
2618
- ).value;
2619
- const basesById = new Map(
2620
- options.bases.map((base) => [base.definition.id, base])
2621
- );
2622
- for (const base of sortBasesForArtifacts(options.bases, basesById)) {
2623
- const resolvedBase = resolveBaseDefinition(base, basesById);
2624
- const effectiveSetup = resolveEffectiveBaseSetup({
2625
- manifest,
2626
- base: base.definition,
2627
- basesById
2628
- });
2629
- const shadow = new ShadowReducerRuntime(
2630
- manifest,
2631
- gameModule.default,
2632
- createInitialTable,
2633
- resolvedBase.seed,
2634
- resolvedBase.players,
2635
- effectiveSetup.setupProfileId,
2636
- options.debug ?? false
2637
- );
2638
- let parentFingerprintHash = null;
2639
- if (base.definition.extends) {
2640
- const parentArtifact = baseStates[baseStateKey(base.definition.extends)];
2641
- if (!parentArtifact) {
2642
- throw new Error(
2643
- `Base '${base.definition.id}' extends '${base.definition.extends}', but the parent artifact does not exist yet.`
2644
- );
2645
- }
2646
- if (parentArtifact.fingerprint.seed !== resolvedBase.seed) {
2647
- throw new Error(
2648
- `Base '${base.definition.id}' extends '${base.definition.extends}' but changes seed from ${parentArtifact.fingerprint.seed} to ${resolvedBase.seed}.`
2649
- );
2650
- }
2651
- if (parentArtifact.fingerprint.players !== resolvedBase.players) {
2652
- throw new Error(
2653
- `Base '${base.definition.id}' extends '${base.definition.extends}' but changes players from ${parentArtifact.fingerprint.players} to ${resolvedBase.players}.`
2654
- );
2655
- }
2656
- if ((parentArtifact.fingerprint.setupProfileId ?? null) !== (effectiveSetup.setupProfileId ?? null)) {
2657
- throw new Error(
2658
- `Base '${base.definition.id}' extends '${base.definition.extends}' but changes setup profile from ${parentArtifact.fingerprint.setupProfileId ?? "none"} to ${effectiveSetup.setupProfileId ?? "none"}.`
2659
- );
2660
- }
2661
- parentFingerprintHash = fingerprintHash(parentArtifact.fingerprint);
2662
- shadow.hydrate(parentArtifact.snapshot, parentArtifact.version);
2663
- }
2664
- const context = await createScenarioContext({
2665
- runner: "reducer",
2666
- shadow,
2667
- expect: (await loadTestingExpectApiFactory())()
2668
- });
2669
- await context.game.start();
2670
- await base.definition.setup({
2671
- game: context.game,
2672
- players: context.players,
2673
- seat: context.seat
2674
- });
2675
- baseStates[baseStateKey(base.definition.id)] = {
2676
- key: baseStateKey(base.definition.id),
2677
- base: base.definition.id,
2678
- snapshot: shadow.rawState(),
2679
- version: shadow.currentVersion(),
2680
- fingerprint: {
2681
- base: base.definition.id,
2682
- seed: resolvedBase.seed,
2683
- players: resolvedBase.players,
2684
- baseBundleHash: base.bundleHash,
2685
- manifestHash,
2686
- appBundleHash,
2687
- uiContractHash,
2688
- setupProfileId: effectiveSetup.setupProfileId,
2689
- compiledResultId: options.compiledResultId,
2690
- gameId: options.gameId,
2691
- parentBaseId: base.definition.extends ?? null,
2692
- parentFingerprintHash,
2693
- contractFingerprint: contractFingerprintValue
2694
- }
2695
- };
2696
- writeProjectionSnapshots({
2697
- projectRoot: options.projectRoot,
2698
- baseId: base.definition.id,
2699
- shadow
2700
- });
2701
- }
2702
- const header = "// Generated by dreamboard test generate. Do not edit by hand.\n";
2703
- await writeTextFile(
2704
- path2.join(generatedDir, "base-states.generated.ts"),
2705
- `${header}export const BASE_STATES = ${JSON.stringify(baseStates, null, 2)} as const;
2706
- export const BASE_STATES_CONTRACT_FINGERPRINT = ${JSON.stringify(contractFingerprintValue)};
2707
- `
2708
- );
2709
- await writeTextFile(
2710
- path2.join(generatedDir, "base-states.generated.d.ts"),
2711
- `${header}export declare const BASE_STATES: Record<string, unknown>;
2712
- export declare const BASE_STATES_CONTRACT_FINGERPRINT: string | undefined;
2713
- `
2714
- );
2715
- await writeTextFile(
2716
- path2.join(generatedDir, "scenario-manifest.generated.ts"),
2717
- `${header}export const SCENARIO_MANIFEST = ${JSON.stringify(
2718
- options.scenarios.map((scenario) => ({
2719
- id: scenario.definition.id,
2720
- filePath: path2.relative(generatedDir, scenario.filePath),
2721
- base: scenario.definition.from,
2722
- runners: normalizeScenarioRunners(scenario.definition.runners)
2723
- })),
2724
- null,
2725
- 2
2726
- )} as const;
2727
- `
2728
- );
2729
- await writeJsonFile(path2.join(generatedDir, ".generation-meta.json"), {
2730
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2731
- contractFingerprint: contractFingerprintValue,
2732
- scenarioCount: options.scenarios.length,
2733
- baseStateCount: options.bases.length
2734
- });
2735
- }
2736
- async function loadGeneratedBaseStates(projectRoot) {
2737
- const filePath = path2.join(
2738
- projectRoot,
2739
- "test",
2740
- "generated",
2741
- "base-states.generated.ts"
2742
- );
2743
- if (!await exists(filePath)) {
2744
- return null;
2745
- }
2746
- const module = await importTypeScriptModule(filePath);
2747
- return module.BASE_STATES ?? null;
2748
- }
2749
- async function loadGeneratedScenarioManifest(projectRoot) {
2750
- const filePath = path2.join(
2751
- projectRoot,
2752
- "test",
2753
- "generated",
2754
- "scenario-manifest.generated.ts"
2755
- );
2756
- if (!await exists(filePath)) {
2757
- return [];
2758
- }
2759
- const module = await importTypeScriptModule(filePath);
2760
- const entries = module.SCENARIO_MANIFEST;
2761
- if (!Array.isArray(entries)) {
2762
- return [];
2763
- }
2764
- return entries.flatMap((entry) => {
2765
- if (entry && typeof entry === "object" && "id" in entry && "base" in entry && typeof entry.id === "string" && typeof entry.base === "string") {
2766
- return [{ id: entry.id, base: entry.base }];
2767
- }
2768
- return [];
2769
- });
2770
- }
2771
- function validateGeneratedFingerprint(options) {
2772
- const mismatches = [];
2773
- if (options.generated.contractFingerprint && options.current.contractFingerprint && options.generated.contractFingerprint !== options.current.contractFingerprint) {
2774
- const error = new Error(
2775
- `Base states were generated for contract ${options.generated.contractFingerprint} but the current contract is ${options.current.contractFingerprint}. Remedy: run \`dreamboard test generate\`, then re-run the tests.`
2776
- );
2777
- error.code = STALE_CONTRACT_ARTIFACT_CODE;
2778
- throw error;
2779
- }
2780
- if (options.generated.baseBundleHash !== options.current.baseBundleHash) {
2781
- mismatches.push("base module changed");
2782
- }
2783
- if (options.generated.manifestHash !== options.current.manifestHash) {
2784
- mismatches.push("manifest changed");
2785
- }
2786
- if (options.generated.appBundleHash !== options.current.appBundleHash) {
2787
- mismatches.push("game reducer bundle changed");
2788
- }
2789
- if (options.generated.uiContractHash !== options.current.uiContractHash) {
2790
- mismatches.push("ui contract changed");
2791
- }
2792
- if (options.generated.seed !== options.current.seed || options.generated.players !== options.current.players) {
2793
- mismatches.push("base seed/player count changed");
2794
- }
2795
- if ((options.generated.setupProfileId ?? null) !== (options.current.setupProfileId ?? null)) {
2796
- mismatches.push("base setup profile changed");
2797
- }
2798
- if ((options.generated.parentBaseId ?? null) !== (options.current.parentBaseId ?? null)) {
2799
- mismatches.push("base parent changed");
2800
- }
2801
- if ((options.generated.parentFingerprintHash ?? null) !== (options.current.parentFingerprintHash ?? null)) {
2802
- mismatches.push("parent base artifact changed");
2803
- }
2804
- if (mismatches.length > 0) {
2805
- throw new Error(
2806
- `${mismatches.join("; ")}. Run 'dreamboard test generate' to refresh reducer-native base artifacts.`
2807
- );
2808
- }
2809
- }
2810
- async function currentFingerprint(options) {
2811
- const manifest = await loadManifest(options.projectRoot);
2812
- const gameModule = await importTypeScriptModule(
2813
- path2.join(options.projectRoot, "app", "game.ts")
2814
- );
2815
- const contractFingerprintValue = contractFingerprint(
2816
- gameModule.default
2817
- ).value;
2818
- const resolvedBase = resolveBaseDefinition(
2819
- options.base,
2820
- options.basesById ?? /* @__PURE__ */ new Map([[options.base.definition.id, options.base]])
2821
- );
2822
- const effectiveSetup = resolveEffectiveBaseSetup({
2823
- manifest,
2824
- base: options.base.definition,
2825
- basesById: options.basesById
2826
- });
2827
- let parentFingerprintHash = null;
2828
- if (options.base.definition.extends) {
2829
- const parentBase = options.basesById?.get(options.base.definition.extends);
2830
- if (!parentBase) {
2831
- throw new Error(
2832
- `Base '${options.base.definition.id}' extends unknown parent '${options.base.definition.extends}'.`
2833
- );
2834
- }
2835
- parentFingerprintHash = fingerprintHash(
2836
- await currentFingerprint({
2837
- projectRoot: options.projectRoot,
2838
- base: parentBase,
2839
- basesById: options.basesById,
2840
- compiledResultId: options.compiledResultId,
2841
- gameId: options.gameId
2842
- })
2843
- );
2844
- }
2845
- return {
2846
- base: options.base.definition.id,
2847
- seed: resolvedBase.seed,
2848
- players: resolvedBase.players,
2849
- baseBundleHash: options.base.bundleHash,
2850
- manifestHash: hashContent(JSON.stringify(manifest)),
2851
- appBundleHash: hashContent(
2852
- await bundleTypeScriptModuleText(
2853
- path2.join(options.projectRoot, "app", "game.ts")
2854
- )
2855
- ),
2856
- uiContractHash: hashContent(
2857
- await bundleTypeScriptModuleText(
2858
- path2.join(options.projectRoot, "shared", "generated", "ui-contract.ts"),
2859
- { external: SDK_UI_RUNTIME_EXTERNALS }
2860
- )
2861
- ),
2862
- setupProfileId: effectiveSetup.setupProfileId,
2863
- compiledResultId: options.compiledResultId,
2864
- gameId: options.gameId,
2865
- parentBaseId: options.base.definition.extends ?? null,
2866
- parentFingerprintHash,
2867
- contractFingerprint: contractFingerprintValue
2868
- };
2869
- }
2870
- async function generateReducerNativeArtifacts(options) {
2871
- await ensureReducerNativeTestingFiles(options.projectRoot);
2872
- const [bases, scenarios] = await Promise.all([
2873
- loadTypedBases(options.projectRoot),
2874
- loadTypedScenarios(options.projectRoot, {
2875
- scenarioPath: options.scenarioPath
2876
- })
2877
- ]);
2878
- const basesById = new Map(bases.map((base) => [base.definition.id, base]));
2879
- validateTypedScenarioBases(scenarios, basesById);
2880
- await writeReducerNativeGeneratedFiles({
2881
- projectRoot: options.projectRoot,
2882
- bases,
2883
- scenarios,
2884
- compiledResultId: options.compiledResultId,
2885
- gameId: options.gameId,
2886
- debug: options.debug
2887
- });
2888
- return { bases, scenarios };
2889
- }
2890
- async function runReducerNativeScenarios(options) {
2891
- await ensureReducerNativeTestingFiles(options.projectRoot);
2892
- const [
2893
- bases,
2894
- scenarios,
2895
- initialGeneratedBaseStates,
2896
- manifest,
2897
- gameModule,
2898
- manifestContractModule
2899
- ] = await Promise.all([
2900
- loadTypedBases(options.projectRoot),
2901
- loadTypedScenarios(options.projectRoot, {
2902
- scenarioPath: options.scenarioPath
2903
- }),
2904
- loadGeneratedBaseStates(options.projectRoot),
2905
- loadManifest(options.projectRoot),
2906
- importTypeScriptModule(
2907
- path2.join(options.projectRoot, "app", "game.ts")
2908
- ),
2909
- importTypeScriptModule(
2910
- path2.join(options.projectRoot, "shared", "manifest-contract.ts")
2911
- )
2912
- ]);
2913
- let generatedBaseStates = initialGeneratedBaseStates;
2914
- const createInitialTable = typeof manifestContractModule.createInitialTable === "function" ? manifestContractModule.createInitialTable : null;
2915
- const basesById = new Map(bases.map((base) => [base.definition.id, base]));
2916
- validateTypedScenarioBases(scenarios, basesById);
2917
- if (options.updateSnapshots) {
2918
- await writeReducerNativeGeneratedFiles({
2919
- projectRoot: options.projectRoot,
2920
- bases,
2921
- scenarios,
2922
- compiledResultId: options.compiledResultId,
2923
- gameId: options.gameId,
2924
- debug: options.debug
2925
- });
2926
- generatedBaseStates = await loadGeneratedBaseStates(options.projectRoot);
2927
- }
2928
- const runnerScenarios = scenarios.filter(
2929
- (scenario) => normalizeScenarioRunners(scenario.definition.runners).includes(
2930
- options.runner
2931
- )
2932
- );
2933
- let browser = null;
2934
- let page = null;
2935
- let browserBridge = null;
2936
- let browserDriver = null;
2937
- if (options.runner === "browser") {
2938
- const webBaseUrl = options.webBaseUrl ?? options.projectConfig.webBaseUrl;
2939
- if (!webBaseUrl) {
2940
- throw new Error(
2941
- "Browser runner requires a local webBaseUrl. Start the local web stack and configure the project for env local."
2942
- );
2943
- }
2944
- browserDriver = await loadBrowserDriver(options.projectRoot);
2945
- const opened = await openBrowserPage(
2946
- `${webBaseUrl}/_dev`,
2947
- options.resolvedConfig
2948
- );
2949
- browser = opened.browser;
2950
- page = opened.page;
2951
- }
2952
- try {
2953
- let passed = 0;
2954
- let failed = 0;
2955
- const results = [];
2956
- for (const scenario of runnerScenarios) {
2957
- const base = basesById.get(scenario.definition.from);
2958
- if (!base) {
2959
- throw new Error(
2960
- `Missing typed base '${scenario.definition.from}' for scenario '${scenario.definition.id}'.`
2961
- );
2962
- }
2963
- if (generatedBaseStates?.[baseStateKey(base.definition.id)]) {
2964
- validateGeneratedFingerprint({
2965
- generated: generatedBaseStates[baseStateKey(base.definition.id)].fingerprint,
2966
- current: await currentFingerprint({
2967
- projectRoot: options.projectRoot,
2968
- base,
2969
- basesById,
2970
- compiledResultId: options.compiledResultId,
2971
- gameId: options.gameId
2972
- })
2973
- });
2974
- } else if (options.runner !== "reducer") {
2975
- throw new Error(
2976
- "Missing reducer-native generated base artifacts. Run 'dreamboard test generate' first."
2977
- );
2978
- }
2979
- try {
2980
- const resolvedBase = resolveBaseDefinition(base, basesById);
2981
- const effectiveSetup = resolveEffectiveBaseSetup({
2982
- manifest,
2983
- base: base.definition,
2984
- basesById
2985
- });
2986
- const shadow = new ShadowReducerRuntime(
2987
- manifest,
2988
- gameModule.default,
2989
- createInitialTable,
2990
- resolvedBase.seed,
2991
- resolvedBase.players,
2992
- effectiveSetup.setupProfileId,
2993
- options.debug ?? false
2994
- );
2995
- if (base.definition.extends) {
2996
- const parentArtifact = generatedBaseStates?.[baseStateKey(base.definition.extends)];
2997
- if (!parentArtifact) {
2998
- throw new Error(
2999
- `Base '${base.definition.id}' extends '${base.definition.extends}', but the parent artifact is missing. Run 'dreamboard test generate' first.`
3000
- );
3001
- }
3002
- shadow.hydrate(parentArtifact.snapshot, parentArtifact.version);
3003
- }
3004
- let remote;
3005
- if (options.runner === "remote") {
3006
- const compiledResultId = options.compiledResultId ?? options.projectConfig.compile?.latestSuccessful?.resultId;
3007
- if (!compiledResultId) {
3008
- throw new Error(
3009
- "Remote runner requires a compiled result. Compile the workspace first."
3010
- );
3011
- }
3012
- const {
3013
- data: session,
3014
- error: sessionError,
3015
- response: sessionResponse
3016
- } = await createProjectSession({
3017
- path: { projectId: options.projectConfig.projectId },
3018
- body: {
3019
- compiledResultId,
3020
- seed: resolvedBase.seed,
3021
- playerCount: resolvedBase.players,
3022
- autoAssignSeats: true,
3023
- setupProfileId: effectiveSetup.setupProfileId ?? void 0
3024
- }
3025
- });
3026
- if (!session || sessionError) {
3027
- throw toDreamboardApiError(
3028
- sessionError,
3029
- sessionResponse,
3030
- "Failed to create remote-runner session."
3031
- );
3032
- }
3033
- const tracker = new RemoteGameplayTracker(
3034
- session.sessionId,
3035
- "player-1"
3036
- );
3037
- const { error: startError, response: startResponse } = await startGame({
3038
- path: { sessionId: session.sessionId }
3039
- });
3040
- if (startError) {
3041
- throw toDreamboardApiError(
3042
- startError,
3043
- startResponse,
3044
- "Failed to start remote-runner session."
3045
- );
3046
- }
3047
- await tracker.bootstrap();
3048
- remote = {
3049
- sessionId: session.sessionId,
3050
- tracker
3051
- };
3052
- }
3053
- if (options.runner === "browser" && page) {
3054
- const compiledResultId = options.compiledResultId ?? options.projectConfig.compile?.latestSuccessful?.resultId;
3055
- if (!compiledResultId) {
3056
- throw new Error(
3057
- "Browser runner requires a compiled result. Compile the workspace first."
3058
- );
3059
- }
3060
- const {
3061
- data: session,
3062
- error: sessionError,
3063
- response: sessionResponse
3064
- } = await createProjectSession({
3065
- path: { projectId: options.projectConfig.projectId },
3066
- body: {
3067
- compiledResultId,
3068
- seed: resolvedBase.seed,
3069
- playerCount: resolvedBase.players,
3070
- autoAssignSeats: true,
3071
- setupProfileId: effectiveSetup.setupProfileId ?? void 0
3072
- }
3073
- });
3074
- if (!session || sessionError) {
3075
- throw toDreamboardApiError(
3076
- sessionError,
3077
- sessionResponse,
3078
- "Failed to create browser-runner session."
3079
- );
3080
- }
3081
- const { error: startError, data: started } = await startGame({
3082
- path: { sessionId: session.sessionId }
3083
- });
3084
- if (startError || !started) {
3085
- throw new Error("Failed to start browser-runner session.");
3086
- }
3087
- await page.goto(
3088
- `${options.webBaseUrl ?? options.projectConfig.webBaseUrl}/_dev/play/${started.context.shortCode}`,
3089
- { waitUntil: "domcontentloaded" }
3090
- );
3091
- await waitForGameReady(page);
3092
- browserBridge = await createBrowserBridgeClient(page);
3093
- await browserDriver?.onReady?.(browserBridge);
3094
- }
3095
- const context = await createScenarioContext({
3096
- runner: options.runner,
3097
- shadow,
3098
- expect: (await loadTestingExpectApiFactory())({
3099
- matchSnapshot: createScenarioSnapshotMatcher({
3100
- projectRoot: options.projectRoot,
3101
- scenarioId: scenario.definition.id,
3102
- updateSnapshots: options.updateSnapshots ?? false
3103
- })
3104
- }),
3105
- remote,
3106
- browser: options.runner === "browser" && browserBridge ? {
3107
- bridge: browserBridge,
3108
- driver: browserDriver
3109
- } : void 0
3110
- });
3111
- await context.game.start();
3112
- await base.definition.setup({
3113
- game: context.game,
3114
- players: context.players,
3115
- seat: context.seat
3116
- });
3117
- shadow.clearHistory();
3118
- await scenario.definition.when(context);
3119
- if (scenario.definition.phase && shadow.phase() !== scenario.definition.phase) {
3120
- throw new Error(
3121
- `Scenario '${scenario.definition.id}' expected phase '${scenario.definition.phase}' but reached '${shadow.phase()}'.`
3122
- );
3123
- }
3124
- if (scenario.definition.stage && shadow.currentStage() !== scenario.definition.stage) {
3125
- throw new Error(
3126
- `Scenario '${scenario.definition.id}' expected stage '${scenario.definition.stage}' but reached '${shadow.currentStage() ?? "null"}'.`
3127
- );
3128
- }
3129
- await scenario.definition.then(context);
3130
- passed += 1;
3131
- results.push({
3132
- id: scenario.definition.id,
3133
- success: true
3134
- });
3135
- } catch (error) {
3136
- failed += 1;
3137
- results.push({
3138
- id: scenario.definition.id,
3139
- success: false,
3140
- errorCode: isStaleContractArtifactError(error) ? STALE_CONTRACT_ARTIFACT_CODE : void 0,
3141
- error: error instanceof Error ? formatScenarioErrorForDisplay({
3142
- error,
3143
- projectRoot: options.projectRoot,
3144
- scenarioFilePath: scenario.filePath
3145
- }) : `Scenario '${scenario.definition.id}' failed.`
3146
- });
3147
- }
3148
- }
3149
- return { passed, failed, results };
3150
- } finally {
3151
- if (browser) {
3152
- await browser.close();
3153
- }
3154
- }
3155
- }
3156
-
3157
- export {
3158
- formatScenarioErrorForDisplay,
3159
- isReducerNativeTestingWorkspace,
3160
- ensureReducerNativeTestingFiles,
3161
- loadTypedBases,
3162
- loadTypedScenarios,
3163
- assertDispatchResultWireContract,
3164
- createSessionFromScenario,
3165
- materializeScenarioReducerState,
3166
- replayScenarioThroughBackend,
3167
- createScenarioActionPlan,
3168
- replayActionPlanThroughBackend,
3169
- createActionPlanReplaySession,
3170
- replayActionPlanInSession,
3171
- writeReducerNativeGeneratedFiles,
3172
- generateReducerNativeArtifacts,
3173
- runReducerNativeScenarios
3174
- };