@dreamboard-games/sdk 0.5.0-alpha.2 → 0.5.0-alpha.21

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 (150) hide show
  1. package/README.md +462 -53
  2. package/dist/board-element-CqtxU5VZ.d.ts +13 -0
  3. package/dist/chunk-3RFGPBES.js +1792 -0
  4. package/dist/chunk-3RFGPBES.js.map +1 -0
  5. package/dist/chunk-CZRPUWCT.js +1602 -0
  6. package/dist/chunk-CZRPUWCT.js.map +1 -0
  7. package/dist/chunk-DMSXVMYY.js +7199 -0
  8. package/dist/chunk-DMSXVMYY.js.map +1 -0
  9. package/dist/chunk-U6R7FAH4.js +630 -0
  10. package/dist/chunk-U6R7FAH4.js.map +1 -0
  11. package/dist/chunk-YT3RPO5J.js +635 -0
  12. package/dist/chunk-YT3RPO5J.js.map +1 -0
  13. package/dist/diagnostics-ZAPEBlJP.d.ts +64 -0
  14. package/dist/extract-CwJWDrSD.d.ts +6850 -0
  15. package/dist/index.d.ts +5177 -1
  16. package/dist/index.js +1990 -6
  17. package/dist/index.js.map +1 -1
  18. package/dist/react.d.ts +158 -0
  19. package/dist/react.js +1119 -0
  20. package/dist/react.js.map +1 -0
  21. package/dist/reducer.d.ts +1028 -97
  22. package/dist/reducer.js +4892 -125
  23. package/dist/reducer.js.map +1 -1
  24. package/dist/shortcuts-B_RrufOM.d.ts +822 -0
  25. package/dist/testing.d.ts +529 -569
  26. package/dist/testing.js +2588 -934
  27. package/dist/testing.js.map +1 -1
  28. package/dist/types-BxXRINRF.d.ts +118 -0
  29. package/package.json +39 -144
  30. package/dist/HandView-Bi2lxzQP.d.ts +0 -79
  31. package/dist/ResourceCounter-BFxJknqp.d.ts +0 -98
  32. package/dist/ThemeProvider-CElaL2ik.d.ts +0 -99
  33. package/dist/attributes-DbvyMbXw.d.ts +0 -68
  34. package/dist/browser-interaction.d.ts +0 -910
  35. package/dist/browser-interaction.js +0 -119
  36. package/dist/browser-interaction.js.map +0 -1
  37. package/dist/chunk-32PFKDV7.js +0 -11705
  38. package/dist/chunk-32PFKDV7.js.map +0 -1
  39. package/dist/chunk-3BAD4VC2.js +0 -263
  40. package/dist/chunk-3BAD4VC2.js.map +0 -1
  41. package/dist/chunk-3SKDNDPG.js +0 -104
  42. package/dist/chunk-3SKDNDPG.js.map +0 -1
  43. package/dist/chunk-5YV2WHJ4.js +0 -25
  44. package/dist/chunk-5YV2WHJ4.js.map +0 -1
  45. package/dist/chunk-ANRB4XKA.js +0 -90
  46. package/dist/chunk-ANRB4XKA.js.map +0 -1
  47. package/dist/chunk-BI4G3PM2.js +0 -1864
  48. package/dist/chunk-BI4G3PM2.js.map +0 -1
  49. package/dist/chunk-EVCLTY4Q.js +0 -935
  50. package/dist/chunk-EVCLTY4Q.js.map +0 -1
  51. package/dist/chunk-GDPKTBUR.js +0 -564
  52. package/dist/chunk-GDPKTBUR.js.map +0 -1
  53. package/dist/chunk-H6VDGFL5.js +0 -2903
  54. package/dist/chunk-H6VDGFL5.js.map +0 -1
  55. package/dist/chunk-LR3ZTWQF.js +0 -5960
  56. package/dist/chunk-LR3ZTWQF.js.map +0 -1
  57. package/dist/chunk-PF7L4BMG.js +0 -117
  58. package/dist/chunk-PF7L4BMG.js.map +0 -1
  59. package/dist/chunk-PN5O6GG2.js +0 -32
  60. package/dist/chunk-PN5O6GG2.js.map +0 -1
  61. package/dist/chunk-PZ5AY32C.js +0 -10
  62. package/dist/chunk-PZ5AY32C.js.map +0 -1
  63. package/dist/chunk-S2JEDVWH.js +0 -7165
  64. package/dist/chunk-S2JEDVWH.js.map +0 -1
  65. package/dist/chunk-T3ZKNUZ7.js +0 -1
  66. package/dist/chunk-T3ZKNUZ7.js.map +0 -1
  67. package/dist/chunk-UGH54WZ2.js +0 -223
  68. package/dist/chunk-UGH54WZ2.js.map +0 -1
  69. package/dist/chunk-VCDWP5VL.js +0 -746
  70. package/dist/chunk-VCDWP5VL.js.map +0 -1
  71. package/dist/chunk-VDXOF4FW.js +0 -69
  72. package/dist/chunk-VDXOF4FW.js.map +0 -1
  73. package/dist/chunk-WHR5UW3F.js +0 -1988
  74. package/dist/chunk-WHR5UW3F.js.map +0 -1
  75. package/dist/chunk-Y75CFE77.js +0 -17
  76. package/dist/chunk-Y75CFE77.js.map +0 -1
  77. package/dist/chunk-Z7QCREWI.js +0 -3435
  78. package/dist/chunk-Z7QCREWI.js.map +0 -1
  79. package/dist/chunk-ZZGN2ATS.js +0 -1941
  80. package/dist/chunk-ZZGN2ATS.js.map +0 -1
  81. package/dist/components-gAbIz7hK.d.ts +0 -1284
  82. package/dist/definitions-BgmV_GhC.d.ts +0 -300
  83. package/dist/diagnostics-DIi3ina7.d.ts +0 -50
  84. package/dist/digest.d.ts +0 -8
  85. package/dist/game-CsZScpcn.d.ts +0 -763
  86. package/dist/hex-board-view-aKkblDp6.d.ts +0 -1228
  87. package/dist/index-CcT9Q7N7.d.ts +0 -193
  88. package/dist/index.d-DjzoK7zn.d.ts +0 -2101
  89. package/dist/package-set.d.ts +0 -13
  90. package/dist/package-set.js +0 -12
  91. package/dist/package-set.js.map +0 -1
  92. package/dist/player-state-Cqpyeql0.d.ts +0 -371
  93. package/dist/plugin-runtime-contract.d.ts +0 -17
  94. package/dist/plugin-runtime-contract.js +0 -92
  95. package/dist/plugin-runtime-contract.js.map +0 -1
  96. package/dist/primitive-props-BNHDkgd7.d.ts +0 -16
  97. package/dist/protocol-dYgafTYY.d.ts +0 -314
  98. package/dist/reducer/advanced.d.ts +0 -82
  99. package/dist/reducer/advanced.js +0 -51
  100. package/dist/reducer/advanced.js.map +0 -1
  101. package/dist/reducer-contract.d.ts +0 -11
  102. package/dist/reducer-contract.js +0 -16
  103. package/dist/reducer-contract.js.map +0 -1
  104. package/dist/reference-games/index.d.ts +0 -31
  105. package/dist/reference-games/index.js +0 -48
  106. package/dist/reference-games/index.js.map +0 -1
  107. package/dist/runtime/primitives.d.ts +0 -250
  108. package/dist/runtime/primitives.js +0 -189
  109. package/dist/runtime/primitives.js.map +0 -1
  110. package/dist/runtime/runtime-api.d.ts +0 -2
  111. package/dist/runtime/runtime-api.js +0 -1
  112. package/dist/runtime/runtime-api.js.map +0 -1
  113. package/dist/runtime/workspace-contract.d.ts +0 -392
  114. package/dist/runtime/workspace-contract.js +0 -28
  115. package/dist/runtime/workspace-contract.js.map +0 -1
  116. package/dist/runtime-RJ5orDJU.d.ts +0 -1699
  117. package/dist/runtime-api-Bz5pwNU_.d.ts +0 -296
  118. package/dist/runtime-json-CQ9QbLZ5.d.ts +0 -5
  119. package/dist/runtime.d.ts +0 -83
  120. package/dist/runtime.js +0 -242
  121. package/dist/runtime.js.map +0 -1
  122. package/dist/schema.d.ts +0 -379
  123. package/dist/stale-contract-artifact-error-XLaweZtF.d.ts +0 -18
  124. package/dist/testing-compiler.d.ts +0 -37
  125. package/dist/testing-compiler.js +0 -272
  126. package/dist/testing-compiler.js.map +0 -1
  127. package/dist/testing-runtime.d.ts +0 -119
  128. package/dist/testing-runtime.js +0 -212
  129. package/dist/testing-runtime.js.map +0 -1
  130. package/dist/types-DJj5MJkl.d.ts +0 -256
  131. package/dist/types-DR7DoB1x.d.ts +0 -28
  132. package/dist/types-JWCYHmu7.d.ts +0 -122
  133. package/dist/types.d.ts +0 -1958
  134. package/dist/types.js +0 -14
  135. package/dist/types.js.map +0 -1
  136. package/dist/ui/components.d.ts +0 -17
  137. package/dist/ui/components.js +0 -216
  138. package/dist/ui/components.js.map +0 -1
  139. package/dist/ui/defaults.d.ts +0 -19
  140. package/dist/ui/defaults.js +0 -104
  141. package/dist/ui/defaults.js.map +0 -1
  142. package/dist/ui/player-state.d.ts +0 -2
  143. package/dist/ui/player-state.js +0 -1
  144. package/dist/ui/player-state.js.map +0 -1
  145. package/dist/ui/plugin-styles.css +0 -2
  146. package/dist/ui-contract-GTUPkf8z.d.ts +0 -1168
  147. package/dist/ui.d.ts +0 -317
  148. package/dist/ui.js +0 -277
  149. package/dist/ui.js.map +0 -1
  150. package/dist/views-BpuWcOyN.d.ts +0 -1641
package/dist/testing.js CHANGED
@@ -1,52 +1,1987 @@
1
1
  import {
2
- ScenarioAssertionError,
3
- ScenarioDefinitionValidationError,
4
- ScenarioReplayError,
5
- advanceScenarioReplay,
6
- assertScenario,
7
- compareCanonicalScenarioJson,
8
- createExpectApi,
9
- createScenarioAuthoring,
10
- digestScenarioJson,
11
- enumerateScenarioInteractionParams,
12
- inspectScenarioReplayAuthority,
13
- probeScenarioCommand,
14
- projectScenarioCommandParams,
15
- replayScenario,
16
- resolveScenarioCheckpoint,
17
- toScenarioReplayDefinition
18
- } from "./chunk-BI4G3PM2.js";
19
- import {
20
- StaleContractArtifactError,
21
- isStaleContractArtifactError
22
- } from "./chunk-S2JEDVWH.js";
2
+ SourceSnapshotSchema,
3
+ computePluginActionSetVersion,
4
+ createSourceLifecycle,
5
+ materializePluginGameplayFrame
6
+ } from "./chunk-YT3RPO5J.js";
23
7
  import {
24
- PluginRuntimeBoundary,
25
- createPluginRuntimeClient
26
- } from "./chunk-3BAD4VC2.js";
27
- import "./chunk-PN5O6GG2.js";
28
- import "./chunk-GDPKTBUR.js";
8
+ assertReducerBundleContract,
9
+ collectReducerDefinitionIndex,
10
+ concealCards,
11
+ createAuthorParamSchemasByPhase,
12
+ createIngressRuntimeCodec,
13
+ createInteractionResolver,
14
+ createPendingSelectionReconciler,
15
+ createProjectionBuilder,
16
+ createReducerBundle,
17
+ createSeatDisclosure,
18
+ createTrustedRuntimeScope,
19
+ encodeSeatParams,
20
+ manifestSchemaFamily,
21
+ nextRandomInt
22
+ } from "./chunk-DMSXVMYY.js";
29
23
  import {
30
- BROWSER_INTERACTION_ACTUATOR_KINDS,
31
- DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
32
- browserInteractionEffectSchema
33
- } from "./chunk-WHR5UW3F.js";
24
+ GameOutcomeSchema,
25
+ ReducerSessionStateSchema
26
+ } from "./chunk-U6R7FAH4.js";
34
27
  import {
35
- DREAMBOARD_PLUGIN_PROTOCOL,
36
- DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
37
- PluginProtocolFrameSchema,
38
- PluginProtocolStepSchema,
39
- PluginProtocolTapeSchema,
40
- computePluginActionSetVersion,
28
+ RuntimeJsonSchema,
41
29
  digestPluginRuntimeJson,
42
- materializePluginGameplayFrame
43
- } from "./chunk-VCDWP5VL.js";
44
- import "./chunk-H6VDGFL5.js";
45
- import {
46
- RuntimeJsonSchema
47
- } from "./chunk-PF7L4BMG.js";
48
- import "./chunk-UGH54WZ2.js";
49
- import "./chunk-PZ5AY32C.js";
30
+ encodeCanonicalPluginRuntimeJson,
31
+ immutableCopy
32
+ } from "./chunk-CZRPUWCT.js";
33
+
34
+ // src/testing/player-roster.ts
35
+ function resolvePlayerRoster(game, count) {
36
+ const setup = game.contract.manifest.normalSetup;
37
+ if (!Number.isSafeInteger(count) || count < setup.minPlayers || count > setup.maxPlayers)
38
+ throw new Error("Player count is outside manifest limits.");
39
+ return Array.from({ length: count }, (_, seat) => `player-${seat + 1}`);
40
+ }
41
+
42
+ // src/testing/scenario-definition-validation.ts
43
+ import * as z2 from "zod";
44
+
45
+ // src/testing/scenario-schema.ts
46
+ import * as z from "zod";
47
+ var ScenarioSchemaValueError = class extends Error {
48
+ path;
49
+ constructor(path, message) {
50
+ super(message);
51
+ this.name = "ScenarioSchemaValueError";
52
+ this.path = path;
53
+ }
54
+ };
55
+ function appendScenarioPath(path, key) {
56
+ if (typeof key === "number") {
57
+ return `${path}[${key}]`;
58
+ }
59
+ if (typeof key === "symbol") {
60
+ return `${path}[${String(key)}]`;
61
+ }
62
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
63
+ }
64
+ function isPlainObject(value) {
65
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
66
+ return false;
67
+ }
68
+ const prototype = Object.getPrototypeOf(value);
69
+ return prototype === Object.prototype || prototype === null;
70
+ }
71
+ function resolvePlayerSeatReference(options) {
72
+ if (!isPlainObject(options.value)) {
73
+ throw new ScenarioSchemaValueError(
74
+ options.path,
75
+ "expected a seat reference shaped as { seat: number }"
76
+ );
77
+ }
78
+ const keys = Object.keys(options.value);
79
+ if (keys.length !== 1 || keys[0] !== "seat") {
80
+ throw new ScenarioSchemaValueError(
81
+ options.path,
82
+ "expected a seat reference with only the 'seat' field"
83
+ );
84
+ }
85
+ const seat = options.value.seat;
86
+ const seatPath = appendScenarioPath(options.path, "seat");
87
+ if (typeof seat !== "number" || !Number.isSafeInteger(seat)) {
88
+ throw new ScenarioSchemaValueError(
89
+ seatPath,
90
+ "expected a finite safe integer"
91
+ );
92
+ }
93
+ if (seat < 0 || seat >= options.playerIds.length) {
94
+ throw new ScenarioSchemaValueError(
95
+ seatPath,
96
+ `expected a seat from 0 through ${Math.max(0, options.playerIds.length - 1)}`
97
+ );
98
+ }
99
+ const playerId = options.playerIds[seat];
100
+ if (playerId === void 0) {
101
+ throw new ScenarioSchemaValueError(
102
+ seatPath,
103
+ `could not resolve seat ${seat}`
104
+ );
105
+ }
106
+ return playerId;
107
+ }
108
+ function projectPlayerSeatReference(options) {
109
+ if (typeof options.value !== "string") {
110
+ throw new ScenarioSchemaValueError(
111
+ options.path,
112
+ "expected a runtime player id string"
113
+ );
114
+ }
115
+ const seat = options.playerIds.indexOf(options.value);
116
+ if (seat < 0) {
117
+ throw new ScenarioSchemaValueError(
118
+ options.path,
119
+ `runtime player id '${options.value}' is not assigned to a scenario seat`
120
+ );
121
+ }
122
+ return { seat };
123
+ }
124
+ function resolveScenarioSeatReferences(options) {
125
+ return transformScenarioSchemaValue({ ...options, direction: "resolve" });
126
+ }
127
+ function projectScenarioSeatReferences(options) {
128
+ return transformScenarioSchemaValue({ ...options, direction: "project" });
129
+ }
130
+ function transformScenarioSchemaValue(options) {
131
+ const { schema, value, playerIds, path } = options;
132
+ if (manifestSchemaFamily(schema) === "playerId") {
133
+ return options.direction === "resolve" ? resolvePlayerSeatReference({ value, playerIds, path }) : projectPlayerSeatReference({ value, playerIds, path });
134
+ }
135
+ if (schema instanceof z.ZodOptional || schema instanceof z.ZodExactOptional) {
136
+ return value === void 0 ? value : transformScenarioSchemaValue({
137
+ schema: schema.unwrap(),
138
+ value,
139
+ playerIds,
140
+ path,
141
+ direction: options.direction
142
+ });
143
+ }
144
+ if (schema instanceof z.ZodNullable) {
145
+ return value === null ? value : transformScenarioSchemaValue({
146
+ schema: schema.unwrap(),
147
+ value,
148
+ playerIds,
149
+ path,
150
+ direction: options.direction
151
+ });
152
+ }
153
+ if (schema instanceof z.ZodDefault || schema instanceof z.ZodPrefault || schema instanceof z.ZodCatch || schema instanceof z.ZodReadonly || schema instanceof z.ZodNonOptional) {
154
+ return transformScenarioSchemaValue({
155
+ schema: schema.unwrap(),
156
+ value,
157
+ playerIds,
158
+ path,
159
+ direction: options.direction
160
+ });
161
+ }
162
+ if (schema instanceof z.ZodArray) {
163
+ if (!Array.isArray(value)) return value;
164
+ return value.map(
165
+ (item, index) => transformScenarioSchemaValue({
166
+ schema: schema.element,
167
+ value: item,
168
+ playerIds,
169
+ path: appendScenarioPath(path, index),
170
+ direction: options.direction
171
+ })
172
+ );
173
+ }
174
+ if (schema instanceof z.ZodTuple) {
175
+ if (!Array.isArray(value)) return value;
176
+ const definition = schema._zod.def;
177
+ const items = definition.items ?? [];
178
+ return value.map((item, index) => {
179
+ const itemSchema = items[index] ?? definition.rest;
180
+ return itemSchema ? transformScenarioSchemaValue({
181
+ schema: itemSchema,
182
+ value: item,
183
+ playerIds,
184
+ path: appendScenarioPath(path, index),
185
+ direction: options.direction
186
+ }) : item;
187
+ });
188
+ }
189
+ if (schema instanceof z.ZodObject) {
190
+ if (!isPlainObject(value)) return value;
191
+ const output = { ...value };
192
+ for (const [key, childSchema] of Object.entries(
193
+ schema.shape
194
+ )) {
195
+ if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
196
+ output[key] = transformScenarioSchemaValue({
197
+ schema: childSchema,
198
+ value: value[key],
199
+ playerIds,
200
+ path: appendScenarioPath(path, key),
201
+ direction: options.direction
202
+ });
203
+ }
204
+ return output;
205
+ }
206
+ if (schema instanceof z.ZodRecord) {
207
+ if (!isPlainObject(value)) return value;
208
+ if (manifestSchemaFamily(schema.keyType) === "playerId") {
209
+ throw new ScenarioSchemaValueError(
210
+ path,
211
+ "player-valued record keys cannot be represented by scenario seat references"
212
+ );
213
+ }
214
+ return Object.fromEntries(
215
+ Object.entries(value).map(([key, item]) => [
216
+ key,
217
+ transformScenarioSchemaValue({
218
+ schema: schema.valueType,
219
+ value: item,
220
+ playerIds,
221
+ path: appendScenarioPath(path, key),
222
+ direction: options.direction
223
+ })
224
+ ])
225
+ );
226
+ }
227
+ if (schema instanceof z.ZodUnion) {
228
+ let firstSeatError;
229
+ for (const option of schema.options) {
230
+ try {
231
+ if (options.direction === "project" && !z.core.safeParse(option, value).success) {
232
+ continue;
233
+ }
234
+ const resolved = transformScenarioSchemaValue({
235
+ schema: option,
236
+ value,
237
+ playerIds,
238
+ path,
239
+ direction: options.direction
240
+ });
241
+ if (options.direction === "project" || z.core.safeParse(option, resolved).success) {
242
+ return resolved;
243
+ }
244
+ } catch (error) {
245
+ if (firstSeatError === void 0 && error instanceof ScenarioSchemaValueError) {
246
+ firstSeatError = error;
247
+ }
248
+ }
249
+ }
250
+ if (firstSeatError) throw firstSeatError;
251
+ return value;
252
+ }
253
+ if (schema instanceof z.ZodIntersection) {
254
+ const definition = schema._zod.def;
255
+ const leftResolved = transformScenarioSchemaValue({
256
+ schema: definition.left,
257
+ value,
258
+ playerIds,
259
+ path,
260
+ direction: options.direction
261
+ });
262
+ return transformScenarioSchemaValue({
263
+ schema: definition.right,
264
+ value: leftResolved,
265
+ playerIds,
266
+ path,
267
+ direction: options.direction
268
+ });
269
+ }
270
+ if (schema instanceof z.ZodPipe) {
271
+ return transformScenarioSchemaValue({
272
+ schema: schema.in,
273
+ value,
274
+ playerIds,
275
+ path,
276
+ direction: options.direction
277
+ });
278
+ }
279
+ if (schema instanceof z.ZodLazy) {
280
+ return transformScenarioSchemaValue({
281
+ schema: schema.unwrap(),
282
+ value,
283
+ playerIds,
284
+ path,
285
+ direction: options.direction
286
+ });
287
+ }
288
+ return value;
289
+ }
290
+
291
+ // src/testing/scenario-definition-validation.ts
292
+ var ScenarioDefinitionValidationError = class extends Error {
293
+ code;
294
+ path;
295
+ constructor(options) {
296
+ super(
297
+ `Invalid scenario definition at '${options.path}': ${options.reason}.`
298
+ );
299
+ this.name = "ScenarioDefinitionValidationError";
300
+ this.code = options.code;
301
+ this.path = options.path;
302
+ }
303
+ };
304
+ var checkpointIdPattern = /^[a-z0-9][a-z0-9-]*$/u;
305
+ function validateCheckpoints(definition) {
306
+ if (!Object.prototype.hasOwnProperty.call(definition, "checkpoints")) return;
307
+ const checkpoints = requireObject(
308
+ definition.checkpoints,
309
+ "scenario.checkpoints"
310
+ );
311
+ assertJsonSerializable(checkpoints, "scenario.checkpoints");
312
+ const occupied = /* @__PURE__ */ new Map();
313
+ for (const [id, candidate] of Object.entries(checkpoints)) {
314
+ const path = appendScenarioPath("scenario.checkpoints", id);
315
+ if (!checkpointIdPattern.test(id) || id === "setup") {
316
+ fail({
317
+ code: "INVALID_VALUE",
318
+ path,
319
+ reason: "checkpoint ids must be lowercase kebab-case and cannot be 'setup'"
320
+ });
321
+ }
322
+ const checkpoint = requireObject(candidate, path);
323
+ assertExactKeys(checkpoint, ["segment", "completed"], path);
324
+ assertOwn(checkpoint, "segment", path);
325
+ assertOwn(checkpoint, "completed", path);
326
+ if (!["setup", "given", "when"].includes(String(checkpoint.segment))) {
327
+ fail({
328
+ code: "INVALID_VALUE",
329
+ path: appendScenarioPath(path, "segment"),
330
+ reason: "expected setup, given, or when"
331
+ });
332
+ }
333
+ const completed = assertSafeInteger(
334
+ checkpoint.completed,
335
+ appendScenarioPath(path, "completed")
336
+ );
337
+ const segment = checkpoint.segment;
338
+ const maximum = segment === "setup" ? 0 : definition[segment].length;
339
+ if (completed < 0 || completed > maximum || segment === "setup" && completed !== 0) {
340
+ fail({
341
+ code: "OUT_OF_RANGE",
342
+ path: appendScenarioPath(path, "completed"),
343
+ reason: `expected 0 through ${maximum} for ${segment}`
344
+ });
345
+ }
346
+ const coordinate = `${segment}:${completed}`;
347
+ const previous = occupied.get(coordinate);
348
+ if (previous !== void 0) {
349
+ fail({
350
+ code: "INVALID_VALUE",
351
+ path,
352
+ reason: `duplicates checkpoint '${previous}' at ${coordinate}`
353
+ });
354
+ }
355
+ occupied.set(coordinate, id);
356
+ }
357
+ }
358
+ function fail(options) {
359
+ throw new ScenarioDefinitionValidationError(options);
360
+ }
361
+ function isPlainObject2(value) {
362
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
363
+ return false;
364
+ }
365
+ const prototype = Object.getPrototypeOf(value);
366
+ return prototype === Object.prototype || prototype === null;
367
+ }
368
+ function requireObject(value, path) {
369
+ if (!isPlainObject2(value)) {
370
+ fail({ code: "INVALID_TYPE", path, reason: "expected a plain object" });
371
+ }
372
+ return value;
373
+ }
374
+ function assertExactKeys(value, allowed, path) {
375
+ const allowedSet = new Set(allowed);
376
+ for (const key of Object.keys(value)) {
377
+ if (!allowedSet.has(key)) {
378
+ fail({
379
+ code: "UNKNOWN_FIELD",
380
+ path: appendScenarioPath(path, key),
381
+ reason: "field is not part of the scenario contract"
382
+ });
383
+ }
384
+ }
385
+ }
386
+ function assertOwn(value, key, path) {
387
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
388
+ fail({
389
+ code: "INVALID_TYPE",
390
+ path: appendScenarioPath(path, key),
391
+ reason: "required field is missing"
392
+ });
393
+ }
394
+ }
395
+ function assertSafeInteger(value, path) {
396
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) {
397
+ fail({
398
+ code: "UNSAFE_INTEGER",
399
+ path,
400
+ reason: "expected a finite safe integer"
401
+ });
402
+ }
403
+ return value;
404
+ }
405
+ function assertJsonSerializable(value, path, ancestors = /* @__PURE__ */ new Set()) {
406
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
407
+ return;
408
+ }
409
+ if (typeof value === "number") {
410
+ if (!Number.isFinite(value)) {
411
+ fail({
412
+ code: "NON_SERIALIZABLE",
413
+ path,
414
+ reason: "non-finite numbers are not JSON-serializable"
415
+ });
416
+ }
417
+ return;
418
+ }
419
+ if (typeof value !== "object") {
420
+ fail({
421
+ code: "NON_SERIALIZABLE",
422
+ path,
423
+ reason: `${typeof value} values are not JSON-serializable`
424
+ });
425
+ }
426
+ if (ancestors.has(value)) {
427
+ fail({
428
+ code: "NON_SERIALIZABLE",
429
+ path,
430
+ reason: "cyclic values are not JSON-serializable"
431
+ });
432
+ }
433
+ if (!Array.isArray(value) && !isPlainObject2(value)) {
434
+ fail({
435
+ code: "NON_SERIALIZABLE",
436
+ path,
437
+ reason: "class instances are not JSON-serializable scenario data"
438
+ });
439
+ }
440
+ ancestors.add(value);
441
+ if (Array.isArray(value)) {
442
+ for (let index = 0; index < value.length; index += 1) {
443
+ if (!Object.prototype.hasOwnProperty.call(value, index)) {
444
+ fail({
445
+ code: "NON_SERIALIZABLE",
446
+ path: appendScenarioPath(path, index),
447
+ reason: "sparse array entries are not allowed"
448
+ });
449
+ }
450
+ assertJsonSerializable(
451
+ value[index],
452
+ appendScenarioPath(path, index),
453
+ ancestors
454
+ );
455
+ }
456
+ const extraKeys = Reflect.ownKeys(value).filter((key) => {
457
+ if (key === "length") return false;
458
+ return typeof key !== "string" || !/^(0|[1-9][0-9]*)$/.test(key);
459
+ });
460
+ if (extraKeys.length > 0) {
461
+ const key = extraKeys[0];
462
+ fail({
463
+ code: "NON_SERIALIZABLE",
464
+ path: appendScenarioPath(path, key),
465
+ reason: "array metadata is not JSON-serializable"
466
+ });
467
+ }
468
+ ancestors.delete(value);
469
+ return;
470
+ }
471
+ const descriptors = Object.getOwnPropertyDescriptors(value);
472
+ for (const key of Reflect.ownKeys(value)) {
473
+ if (typeof key === "symbol") {
474
+ fail({
475
+ code: "NON_SERIALIZABLE",
476
+ path: appendScenarioPath(path, key),
477
+ reason: "symbol keys are not JSON-serializable"
478
+ });
479
+ }
480
+ const descriptor = descriptors[key];
481
+ if (descriptor === void 0 || !descriptor.enumerable || !("value" in descriptor)) {
482
+ fail({
483
+ code: "NON_SERIALIZABLE",
484
+ path: appendScenarioPath(path, key),
485
+ reason: "only enumerable data properties are allowed"
486
+ });
487
+ }
488
+ assertJsonSerializable(
489
+ descriptor.value,
490
+ appendScenarioPath(path, key),
491
+ ancestors
492
+ );
493
+ }
494
+ ancestors.delete(value);
495
+ }
496
+ function commandSchemas(game, interactionId) {
497
+ const schemasByPhase = createAuthorParamSchemasByPhase(game);
498
+ const schemas = Object.values(schemasByPhase).flatMap((schemas2) => {
499
+ const schema = schemas2[interactionId];
500
+ return schema ? [schema] : [];
501
+ });
502
+ const index = collectReducerDefinitionIndex(game);
503
+ for (const phase of index.phasesByName.values()) {
504
+ const steps = phase.interactions.find(([id]) => id === interactionId)?.[1].steps;
505
+ if (steps)
506
+ schemas.push(
507
+ z2.partialRecord(
508
+ z2.enum(steps.entries.map((step) => step.key)),
509
+ z2.unknown()
510
+ ).refine((params) => Object.keys(params).length === 1)
511
+ );
512
+ }
513
+ return schemas;
514
+ }
515
+ function issuePath(path, issue) {
516
+ return issue.path.reduce(appendScenarioPath, path);
517
+ }
518
+ function validateCommand(options) {
519
+ const command = requireObject(options.command, options.path);
520
+ assertJsonSerializable(command, options.path);
521
+ assertExactKeys(command, ["actor", "interactionId", "params"], options.path);
522
+ for (const field of ["actor", "interactionId", "params"]) {
523
+ assertOwn(command, field, options.path);
524
+ }
525
+ const actorPath = appendScenarioPath(options.path, "actor");
526
+ const actor = requireObject(command.actor, actorPath);
527
+ assertExactKeys(actor, ["seat"], actorPath);
528
+ assertOwn(actor, "seat", actorPath);
529
+ const actorSeat = assertSafeInteger(
530
+ actor.seat,
531
+ appendScenarioPath(actorPath, "seat")
532
+ );
533
+ if (actorSeat < 0 || actorSeat >= options.playerIds.length) {
534
+ fail({
535
+ code: "OUT_OF_RANGE",
536
+ path: appendScenarioPath(actorPath, "seat"),
537
+ reason: `expected a seat from 0 through ${Math.max(0, options.playerIds.length - 1)}`
538
+ });
539
+ }
540
+ const interactionPath = appendScenarioPath(options.path, "interactionId");
541
+ if (typeof command.interactionId !== "string" || command.interactionId === "") {
542
+ fail({
543
+ code: "INVALID_TYPE",
544
+ path: interactionPath,
545
+ reason: "expected a non-empty string"
546
+ });
547
+ }
548
+ const paramsPath = appendScenarioPath(options.path, "params");
549
+ const schemas = commandSchemas(options.game, command.interactionId);
550
+ if (schemas.length === 0) {
551
+ fail({
552
+ code: "UNKNOWN_INTERACTION",
553
+ path: interactionPath,
554
+ reason: `interaction '${command.interactionId}' is not declared by the game`
555
+ });
556
+ }
557
+ let firstSeatError;
558
+ let firstIssue;
559
+ for (const schema of schemas) {
560
+ try {
561
+ const resolved = resolveScenarioSeatReferences({
562
+ schema,
563
+ value: command.params,
564
+ playerIds: options.playerIds,
565
+ path: paramsPath
566
+ });
567
+ const parsed = schema.safeParse(resolved);
568
+ if (parsed.success) return;
569
+ firstIssue ??= parsed.error.issues[0];
570
+ } catch (error) {
571
+ if (error instanceof ScenarioSchemaValueError) {
572
+ firstSeatError ??= error;
573
+ continue;
574
+ }
575
+ throw error;
576
+ }
577
+ }
578
+ if (firstSeatError) {
579
+ fail({
580
+ code: firstSeatError.message.includes("seat from") ? "OUT_OF_RANGE" : "INVALID_COMMAND_PARAMS",
581
+ path: firstSeatError.path,
582
+ reason: firstSeatError.message
583
+ });
584
+ }
585
+ fail({
586
+ code: "INVALID_COMMAND_PARAMS",
587
+ path: firstIssue ? issuePath(paramsPath, firstIssue) : paramsPath,
588
+ reason: firstIssue?.message ?? "parameters do not match the interaction contract"
589
+ });
590
+ }
591
+ function validateScenarioDefinition(game, value) {
592
+ const definition = requireObject(value, "scenario");
593
+ assertExactKeys(
594
+ definition,
595
+ ["id", "description", "setup", "given", "when", "checkpoints", "then"],
596
+ "scenario"
597
+ );
598
+ for (const field of ["id", "setup", "given", "when", "then"]) {
599
+ assertOwn(definition, field, "scenario");
600
+ }
601
+ if (typeof definition.id !== "string" || definition.id === "") {
602
+ fail({
603
+ code: "INVALID_TYPE",
604
+ path: "scenario.id",
605
+ reason: "expected a non-empty string"
606
+ });
607
+ }
608
+ if (Object.prototype.hasOwnProperty.call(definition, "description") && typeof definition.description !== "string") {
609
+ fail({
610
+ code: "INVALID_TYPE",
611
+ path: "scenario.description",
612
+ reason: "expected a string when provided"
613
+ });
614
+ }
615
+ const setup = requireObject(definition.setup, "scenario.setup");
616
+ assertExactKeys(setup, ["players", "seed", "options"], "scenario.setup");
617
+ assertOwn(setup, "players", "scenario.setup");
618
+ assertOwn(setup, "seed", "scenario.setup");
619
+ const players = assertSafeInteger(setup.players, "scenario.setup.players");
620
+ const normalSetup = game.contract.manifest.normalSetup;
621
+ if (players < normalSetup.minPlayers || players > normalSetup.maxPlayers) {
622
+ fail({
623
+ code: "OUT_OF_RANGE",
624
+ path: "scenario.setup.players",
625
+ reason: `expected ${normalSetup.minPlayers} through ${normalSetup.maxPlayers} players`
626
+ });
627
+ }
628
+ assertSafeInteger(setup.seed, "scenario.setup.seed");
629
+ assertJsonSerializable(definition.id, "scenario.id");
630
+ if (Object.prototype.hasOwnProperty.call(definition, "description")) {
631
+ assertJsonSerializable(definition.description, "scenario.description");
632
+ }
633
+ assertJsonSerializable(setup, "scenario.setup");
634
+ if (!Array.isArray(definition.given)) {
635
+ fail({
636
+ code: "INVALID_TYPE",
637
+ path: "scenario.given",
638
+ reason: "expected an array"
639
+ });
640
+ }
641
+ if (!Array.isArray(definition.when)) {
642
+ fail({
643
+ code: "INVALID_TYPE",
644
+ path: "scenario.when",
645
+ reason: "expected an array"
646
+ });
647
+ }
648
+ if (typeof definition.then !== "function") {
649
+ fail({
650
+ code: "INVALID_TYPE",
651
+ path: "scenario.then",
652
+ reason: "expected an assertion function"
653
+ });
654
+ }
655
+ validateCheckpoints(definition);
656
+ const playerIds = resolvePlayerRoster(game, players);
657
+ definition.given.forEach(
658
+ (command, index) => validateCommand({
659
+ game,
660
+ command,
661
+ path: `scenario.given[${index}]`,
662
+ playerIds
663
+ })
664
+ );
665
+ definition.when.forEach(
666
+ (command, index) => validateCommand({
667
+ game,
668
+ command,
669
+ path: `scenario.when[${index}]`,
670
+ playerIds
671
+ })
672
+ );
673
+ }
674
+
675
+ // src/testing/definitions.ts
676
+ function resolveScenarioCheckpoint(definition, selector) {
677
+ if (typeof selector !== "string") {
678
+ return structuredClone(selector);
679
+ }
680
+ const checkpoint = definition.checkpoints?.[selector];
681
+ if (checkpoint === void 0) {
682
+ const available = Object.keys(definition.checkpoints ?? {}).sort();
683
+ throw new ScenarioDefinitionValidationError({
684
+ code: "INVALID_VALUE",
685
+ path: "scenario.checkpoints",
686
+ reason: `checkpoint '${selector}' is not declared by scenario '${definition.id}'` + (available.length === 0 ? "; no named checkpoints are available" : `; available checkpoints: ${available.join(", ")}`)
687
+ });
688
+ }
689
+ return structuredClone(checkpoint);
690
+ }
691
+ var ScenarioReplayError = class extends Error {
692
+ scenarioId;
693
+ segment;
694
+ index;
695
+ interactionId;
696
+ errorCode;
697
+ reducerMessage;
698
+ trace;
699
+ constructor(options) {
700
+ super(
701
+ `Scenario '${options.scenarioId}' rejected ${options.segment}[${options.index}] interaction '${options.interactionId}' with ${options.errorCode}` + (options.reducerMessage ? `: ${options.reducerMessage}` : ".")
702
+ );
703
+ this.name = "ScenarioReplayError";
704
+ this.scenarioId = options.scenarioId;
705
+ this.segment = options.segment;
706
+ this.index = options.index;
707
+ this.interactionId = options.interactionId;
708
+ this.errorCode = options.errorCode;
709
+ this.reducerMessage = options.reducerMessage;
710
+ this.trace = options.trace ?? [];
711
+ }
712
+ };
713
+ function createScenarioAuthoring(game) {
714
+ return {
715
+ defineScenario(definition) {
716
+ validateScenarioDefinition(game, definition);
717
+ return definition;
718
+ }
719
+ };
720
+ }
721
+ function toScenarioReplayDefinition(definition) {
722
+ const replay = {
723
+ id: definition.id,
724
+ ...definition.description === void 0 ? {} : { description: definition.description },
725
+ setup: definition.setup,
726
+ given: definition.given,
727
+ when: definition.when
728
+ };
729
+ return structuredClone(replay);
730
+ }
731
+
732
+ // src/testing/scenario-assertion-error.ts
733
+ var SCENARIO_ASSERTION_ERROR_CODE = "SCENARIO_ASSERTION_FAILED";
734
+ var ScenarioAssertionError = class extends Error {
735
+ code = SCENARIO_ASSERTION_ERROR_CODE;
736
+ constructor(message) {
737
+ super(message);
738
+ this.name = "ScenarioAssertionError";
739
+ }
740
+ };
741
+ function scenarioAssertionFailure(message) {
742
+ throw new ScenarioAssertionError(message);
743
+ }
744
+
745
+ // src/testing/create-expect-api.ts
746
+ function isRecord(value) {
747
+ return typeof value === "object" && value !== null && !Array.isArray(value);
748
+ }
749
+ function isDeepStrictEqual(actual, expected) {
750
+ if (Object.is(actual, expected)) return true;
751
+ if (typeof actual !== typeof expected) return false;
752
+ if (actual === null || expected === null) return false;
753
+ if (typeof actual !== "object" || typeof expected !== "object") return false;
754
+ if (actual instanceof Date || expected instanceof Date) {
755
+ return actual instanceof Date && expected instanceof Date && Object.is(actual.getTime(), expected.getTime());
756
+ }
757
+ if (actual instanceof RegExp || expected instanceof RegExp) {
758
+ return actual instanceof RegExp && expected instanceof RegExp && actual.source === expected.source && actual.flags === expected.flags;
759
+ }
760
+ if (Array.isArray(actual) || Array.isArray(expected)) {
761
+ return Array.isArray(actual) && Array.isArray(expected) && actual.length === expected.length && actual.every((value, index) => isDeepStrictEqual(value, expected[index]));
762
+ }
763
+ const actualPrototype = Object.getPrototypeOf(actual);
764
+ const expectedPrototype = Object.getPrototypeOf(expected);
765
+ if (actualPrototype !== expectedPrototype) return false;
766
+ const actualRecord = actual;
767
+ const expectedRecord = expected;
768
+ const actualKeys = Object.keys(actualRecord);
769
+ const expectedKeys = Object.keys(expectedRecord);
770
+ return actualKeys.length === expectedKeys.length && actualKeys.every(
771
+ (key) => Object.prototype.hasOwnProperty.call(expectedRecord, key) && isDeepStrictEqual(actualRecord[key], expectedRecord[key])
772
+ );
773
+ }
774
+ function matchPartial(actual, expected, path = "value") {
775
+ if (!isRecord(expected)) {
776
+ return isDeepStrictEqual(actual, expected) ? null : `${path} does not match`;
777
+ }
778
+ if (!isRecord(actual)) {
779
+ return `${path} is not an object`;
780
+ }
781
+ for (const [key, expectedValue] of Object.entries(expected)) {
782
+ const actualValue = actual[key];
783
+ if (!(key in actual)) {
784
+ return `${path}.${key} is missing`;
785
+ }
786
+ const mismatch = matchPartial(actualValue, expectedValue, `${path}.${key}`);
787
+ if (mismatch) {
788
+ return mismatch;
789
+ }
790
+ }
791
+ return null;
792
+ }
793
+ function asDescriptorList(actual) {
794
+ if (!Array.isArray(actual)) {
795
+ scenarioAssertionFailure("Expected interaction descriptor array.");
796
+ }
797
+ return actual;
798
+ }
799
+ function findInteraction(descriptors, interactionId) {
800
+ return descriptors.find(
801
+ (descriptor) => descriptor.interactionId === interactionId
802
+ ) ?? null;
803
+ }
804
+ function assertDescriptorMatches(descriptor, opts) {
805
+ if (!opts) {
806
+ return;
807
+ }
808
+ const mismatch = matchPartial(descriptor, opts, "interaction");
809
+ if (mismatch) {
810
+ scenarioAssertionFailure(mismatch);
811
+ }
812
+ }
813
+ function createSubmissionErrorMatcher(actual, expected, options) {
814
+ if (isRecord(actual) && (actual.kind === "accepted" || actual.kind === "rejected")) {
815
+ if (actual.kind === "accepted") {
816
+ scenarioAssertionFailure(
817
+ "Expected probe command to reject, but it was accepted."
818
+ );
819
+ }
820
+ const errorCode = typeof actual.errorCode === "string" ? actual.errorCode : void 0;
821
+ const message = typeof actual.message === "string" ? actual.message : "";
822
+ if (expected.errorCode !== void 0 && errorCode !== expected.errorCode) {
823
+ scenarioAssertionFailure(
824
+ `Expected rejection errorCode '${expected.errorCode}', received '${errorCode ?? "undefined"}'.`
825
+ );
826
+ }
827
+ if (typeof expected.message === "string" && message !== expected.message) {
828
+ scenarioAssertionFailure(
829
+ `Expected rejection message '${expected.message}', received '${message}'.`
830
+ );
831
+ }
832
+ if (expected.message instanceof RegExp && !expected.message.test(message)) {
833
+ scenarioAssertionFailure(
834
+ `Expected rejection message '${message}' to match ${String(expected.message)}.`
835
+ );
836
+ }
837
+ return Promise.resolve();
838
+ }
839
+ const resolvePromise = () => {
840
+ if (typeof actual === "function") {
841
+ return Promise.resolve().then(() => actual());
842
+ }
843
+ return Promise.resolve(actual);
844
+ };
845
+ return resolvePromise().then(
846
+ () => scenarioAssertionFailure("Expected promise to reject."),
847
+ (error) => {
848
+ if (!(error instanceof Error)) {
849
+ scenarioAssertionFailure("Expected rejection to be an Error.");
850
+ }
851
+ if (expected.errorCode !== void 0 && error.errorCode !== expected.errorCode) {
852
+ scenarioAssertionFailure(
853
+ `Expected rejection errorCode '${expected.errorCode}', received '${error.errorCode ?? "undefined"}'.${formatLastDiagnosticRejection(options)}`
854
+ );
855
+ }
856
+ if (typeof expected.message === "string" && error.message !== expected.message) {
857
+ scenarioAssertionFailure(
858
+ `Expected rejection message '${expected.message}', received '${error.message}'.${formatLastDiagnosticRejection(
859
+ options
860
+ )}`
861
+ );
862
+ }
863
+ if (expected.message instanceof RegExp && !expected.message.test(error.message)) {
864
+ scenarioAssertionFailure(
865
+ `Expected rejection message '${error.message}' to match ${String(
866
+ expected.message
867
+ )}.${formatLastDiagnosticRejection(options)}`
868
+ );
869
+ }
870
+ }
871
+ );
872
+ }
873
+ function formatLastDiagnosticRejection(options) {
874
+ const rejection = options.lastDiagnosticRejection?.();
875
+ if (!rejection) return "";
876
+ const rule = rejection.ruleId ? ` ruleId=${rejection.ruleId}` : "";
877
+ const message = rejection.message ? ` message=${rejection.message}` : "";
878
+ return `
879
+ last rejection: errorCode=${rejection.errorCode}${rule}${message}`;
880
+ }
881
+ function assertLength(actual, expected) {
882
+ if (actual === null || actual === void 0 || typeof actual.length !== "number") {
883
+ scenarioAssertionFailure(
884
+ "toHaveLength expects a value with a numeric length."
885
+ );
886
+ }
887
+ const length = actual.length;
888
+ if (length !== expected) {
889
+ scenarioAssertionFailure(
890
+ `Expected length ${expected}, received ${length}.`
891
+ );
892
+ }
893
+ }
894
+ function formatInteractionExplanation(explanation) {
895
+ if (!explanation) {
896
+ return "";
897
+ }
898
+ const lines = [
899
+ `availability: ${explanation.availability}`,
900
+ ...explanation.rules.map((rule) => {
901
+ const code = rule.errorCode ? ` (${rule.errorCode})` : "";
902
+ const message = rule.message ? `: ${rule.message}` : "";
903
+ return `rule ${rule.ruleId} ${rule.outcome}${code}${message}`;
904
+ }),
905
+ ...explanation.inputs.map(
906
+ (input) => `input ${input.key} eligibleCount=${input.eligibleCount}`
907
+ )
908
+ ];
909
+ return `
910
+ ${lines.join("\n")}`;
911
+ }
912
+ function assertThrown(actual, predicate) {
913
+ if (typeof actual !== "function") {
914
+ scenarioAssertionFailure("toThrow expects a function.");
915
+ }
916
+ try {
917
+ actual();
918
+ } catch (error) {
919
+ if (!(error instanceof Error)) {
920
+ scenarioAssertionFailure("Thrown value is not an Error.");
921
+ }
922
+ if (predicate === void 0) {
923
+ return;
924
+ }
925
+ if (typeof predicate === "string" && error.message !== predicate) {
926
+ scenarioAssertionFailure(
927
+ `Expected thrown message '${predicate}', received '${error.message}'.`
928
+ );
929
+ }
930
+ if (predicate instanceof RegExp && !predicate.test(error.message)) {
931
+ scenarioAssertionFailure(
932
+ `Expected thrown message '${error.message}' to match ${String(predicate)}.`
933
+ );
934
+ }
935
+ if (typeof predicate === "function" && !predicate(error)) {
936
+ scenarioAssertionFailure("Thrown error did not satisfy predicate.");
937
+ }
938
+ return;
939
+ }
940
+ scenarioAssertionFailure("Expected function to throw.");
941
+ }
942
+ function createExpectApi(options = {}) {
943
+ const buildMatchers = (actual) => ({
944
+ toBe: (expected) => {
945
+ if (actual !== expected) {
946
+ scenarioAssertionFailure(
947
+ `Expected '${String(actual)}' to be '${String(expected)}'.`
948
+ );
949
+ }
950
+ },
951
+ toEqual: (expected) => {
952
+ if (!isDeepStrictEqual(actual, expected)) {
953
+ scenarioAssertionFailure("Expected values to be deeply equal.");
954
+ }
955
+ },
956
+ toMatchObject: (expected) => {
957
+ const mismatch = matchPartial(actual, expected);
958
+ if (mismatch) {
959
+ scenarioAssertionFailure(mismatch);
960
+ }
961
+ },
962
+ toBeDefined: () => {
963
+ if (actual === void 0) {
964
+ scenarioAssertionFailure("Expected value to be defined.");
965
+ }
966
+ },
967
+ toBeUndefined: () => {
968
+ if (actual !== void 0) {
969
+ scenarioAssertionFailure(
970
+ `Expected value to be undefined, but received '${String(actual)}'.`
971
+ );
972
+ }
973
+ },
974
+ toBeNull: () => {
975
+ if (actual !== null) {
976
+ scenarioAssertionFailure(
977
+ `Expected value to be null, but received '${String(actual)}'.`
978
+ );
979
+ }
980
+ },
981
+ toContain: (expected) => {
982
+ if (Array.isArray(actual)) {
983
+ if (!actual.includes(expected)) {
984
+ scenarioAssertionFailure("Expected array to contain value.");
985
+ }
986
+ return;
987
+ }
988
+ if (typeof actual === "string") {
989
+ if (!actual.includes(String(expected))) {
990
+ scenarioAssertionFailure("Expected string to contain value.");
991
+ }
992
+ return;
993
+ }
994
+ scenarioAssertionFailure(
995
+ "toContain expects an array or string actual value."
996
+ );
997
+ },
998
+ toContainEqual: (expected) => {
999
+ if (!Array.isArray(actual)) {
1000
+ scenarioAssertionFailure(
1001
+ "toContainEqual expects an array actual value."
1002
+ );
1003
+ }
1004
+ if (!actual.some((value) => isDeepStrictEqual(value, expected))) {
1005
+ scenarioAssertionFailure("Expected array to contain an equal value.");
1006
+ }
1007
+ },
1008
+ toHaveLength: (expected) => {
1009
+ assertLength(actual, expected);
1010
+ },
1011
+ toBeGreaterThan: (expected) => {
1012
+ if (typeof actual !== "number") {
1013
+ scenarioAssertionFailure(
1014
+ "toBeGreaterThan expects a number actual value."
1015
+ );
1016
+ }
1017
+ if (actual <= expected) {
1018
+ scenarioAssertionFailure(`Expected ${actual} to be > ${expected}.`);
1019
+ }
1020
+ },
1021
+ toBeGreaterThanOrEqual: (expected) => {
1022
+ if (typeof actual !== "number") {
1023
+ scenarioAssertionFailure(
1024
+ "toBeGreaterThanOrEqual expects a number actual value."
1025
+ );
1026
+ }
1027
+ if (actual < expected) {
1028
+ scenarioAssertionFailure(`Expected ${actual} to be >= ${expected}.`);
1029
+ }
1030
+ },
1031
+ toThrow: (predicate) => {
1032
+ assertThrown(actual, predicate);
1033
+ },
1034
+ toMatchSnapshot: (filename) => {
1035
+ if (!options.matchSnapshot) {
1036
+ scenarioAssertionFailure(
1037
+ "Snapshot matching is not configured for this expect API."
1038
+ );
1039
+ }
1040
+ options.matchSnapshot(filename, actual);
1041
+ },
1042
+ toRejectWith: (expected) => createSubmissionErrorMatcher(actual, expected, options),
1043
+ toHaveInteraction: (interactionId, opts) => {
1044
+ const descriptors = asDescriptorList(actual);
1045
+ const descriptor = findInteraction(descriptors, interactionId);
1046
+ if (!descriptor) {
1047
+ scenarioAssertionFailure(
1048
+ `Expected interaction '${interactionId}' to exist.`
1049
+ );
1050
+ }
1051
+ assertDescriptorMatches(descriptor, opts);
1052
+ },
1053
+ toBeGatedBy: (reason, opts) => {
1054
+ const descriptor = Array.isArray(actual) ? (() => {
1055
+ if (!opts?.interactionId) {
1056
+ scenarioAssertionFailure(
1057
+ "toBeGatedBy on a descriptor array requires opts.interactionId."
1058
+ );
1059
+ }
1060
+ return findInteraction(
1061
+ asDescriptorList(actual),
1062
+ opts.interactionId
1063
+ );
1064
+ })() : actual;
1065
+ if (!descriptor) {
1066
+ scenarioAssertionFailure("Expected interaction descriptor to exist.");
1067
+ }
1068
+ if (descriptor.availability?.status === "available") {
1069
+ scenarioAssertionFailure("Expected interaction to be unavailable.");
1070
+ }
1071
+ const actualReason = descriptor.availability?.status === "notYourTurn" || descriptor.availability?.status === "blocked" ? descriptor.availability.reason : void 0;
1072
+ if (actualReason !== reason) {
1073
+ scenarioAssertionFailure(
1074
+ `Expected unavailable reason '${reason}', received '${actualReason ?? "undefined"}'.`
1075
+ );
1076
+ }
1077
+ },
1078
+ toBeAvailable: (explanation) => {
1079
+ const descriptor = Array.isArray(actual) ? (() => {
1080
+ if (!explanation?.interactionId) {
1081
+ scenarioAssertionFailure(
1082
+ "toBeAvailable on a descriptor array requires an explanation."
1083
+ );
1084
+ }
1085
+ return findInteraction(
1086
+ asDescriptorList(actual),
1087
+ explanation.interactionId
1088
+ );
1089
+ })() : actual;
1090
+ if (!descriptor) {
1091
+ scenarioAssertionFailure(
1092
+ `Expected interaction descriptor to exist.${formatInteractionExplanation(
1093
+ explanation
1094
+ )}`
1095
+ );
1096
+ }
1097
+ if (descriptor.availability?.status !== "available") {
1098
+ scenarioAssertionFailure(
1099
+ `Expected interaction '${descriptor.interactionId ?? "unknown"}' to be available.${formatInteractionExplanation(
1100
+ explanation
1101
+ )}`
1102
+ );
1103
+ }
1104
+ },
1105
+ not: {
1106
+ toHaveInteraction: (interactionId) => {
1107
+ const descriptors = asDescriptorList(actual);
1108
+ if (findInteraction(descriptors, interactionId)) {
1109
+ scenarioAssertionFailure(
1110
+ `Expected interaction '${interactionId}' to be absent.`
1111
+ );
1112
+ }
1113
+ }
1114
+ }
1115
+ });
1116
+ return (actual) => buildMatchers(actual);
1117
+ }
1118
+
1119
+ // src/testing/reducer-runtime.ts
1120
+ function createReducerTestingRuntime(definition, options = {}, bundle = createReducerBundle(definition, options)) {
1121
+ const codec = createIngressRuntimeCodec(definition);
1122
+ const parseState = (state) => (
1123
+ // eslint-disable-next-line no-restricted-syntax -- This codec parses the same Contract schemas and current phase before the testing runtime receives its trusted session state.
1124
+ codec.parseState(state)
1125
+ );
1126
+ const scope = createTrustedRuntimeScope(definition);
1127
+ const interactions = createInteractionResolver(scope, {
1128
+ diagnostics: options.descriptorDiagnostics ?? (options.diagnostics === "verbose" ? "verbose" : void 0)
1129
+ });
1130
+ const projection = createProjectionBuilder(scope, interactions);
1131
+ const reconcilePending = createPendingSelectionReconciler(
1132
+ scope,
1133
+ interactions
1134
+ );
1135
+ function inspect({
1136
+ state,
1137
+ playerId,
1138
+ interactionId,
1139
+ referenceBasis
1140
+ }) {
1141
+ if (typeof playerId !== "string")
1142
+ throw new Error("Expected a string playerId.");
1143
+ const [perspective] = codec.parseStatePerspectives(state, [playerId]);
1144
+ return {
1145
+ state: reconcilePending(
1146
+ scope.toCombinedState(parseState(state)),
1147
+ referenceBasis
1148
+ ),
1149
+ playerId: perspective,
1150
+ interactionId
1151
+ };
1152
+ }
1153
+ async function dispatch({
1154
+ state,
1155
+ input,
1156
+ referenceBasis
1157
+ }) {
1158
+ if (input.kind !== "interaction" || !codec.isStatePlayer(state, input.playerId))
1159
+ return bundle.dispatch({ state, input, referenceBasis });
1160
+ const combinedState = reconcilePending(
1161
+ scope.toCombinedState(parseState(state)),
1162
+ referenceBasis
1163
+ );
1164
+ const playerId = codec.parsePlayerId(input.playerId);
1165
+ const disclosure = createSeatDisclosure(
1166
+ combinedState.table,
1167
+ scope.manifest,
1168
+ playerId,
1169
+ referenceBasis
1170
+ );
1171
+ return bundle.dispatch({
1172
+ state: ReducerSessionStateSchema.parse(
1173
+ scope.toSessionState(combinedState)
1174
+ ),
1175
+ referenceBasis,
1176
+ input: {
1177
+ ...input,
1178
+ params: RuntimeJsonSchema.parse(
1179
+ encodeSeatParams(
1180
+ input.params,
1181
+ interactions.inputCollectors(
1182
+ combinedState,
1183
+ playerId,
1184
+ input.interactionId
1185
+ ),
1186
+ disclosure,
1187
+ concealCards(combinedState.table, playerId, disclosure)
1188
+ )
1189
+ )
1190
+ }
1191
+ });
1192
+ }
1193
+ return {
1194
+ ...bundle,
1195
+ dispatch,
1196
+ dispatchClient: (request) => Promise.resolve(bundle.dispatch(request)),
1197
+ async validateInput(request) {
1198
+ const result = await dispatch({
1199
+ ...request,
1200
+ state: structuredClone(request.state)
1201
+ });
1202
+ return result.kind === "reject" ? {
1203
+ valid: false,
1204
+ errorCode: result.errorCode,
1205
+ ...result.message === void 0 ? {} : { message: result.message }
1206
+ } : { valid: true };
1207
+ },
1208
+ async reduce(input) {
1209
+ const result = await dispatch(input);
1210
+ if (result.kind === "reject") return result;
1211
+ return {
1212
+ kind: result.kind,
1213
+ state: result.state,
1214
+ events: result.events,
1215
+ ...result.terminal ? { terminal: result.terminal } : {}
1216
+ };
1217
+ },
1218
+ project({ state, playerIds, projectionMode, referenceBasis }) {
1219
+ if (projectionMode !== "actionsOnly")
1220
+ return bundle.project({ state, playerIds, referenceBasis });
1221
+ const perspectives = codec.parseStatePerspectives(state, playerIds);
1222
+ return projection.project({
1223
+ state: scope.toSessionState(
1224
+ reconcilePending(
1225
+ scope.toCombinedState(parseState(state)),
1226
+ referenceBasis
1227
+ )
1228
+ ),
1229
+ playerIds: perspectives,
1230
+ projectionMode,
1231
+ referenceBasis
1232
+ });
1233
+ },
1234
+ explainInteraction(input) {
1235
+ return interactions.explainInteraction(inspect(input));
1236
+ },
1237
+ currentAuthorParamSchema(input) {
1238
+ return interactions.currentAuthorParamSchema(inspect(input));
1239
+ },
1240
+ currentClientParamSchema(input) {
1241
+ return interactions.currentClientParamSchema(inspect(input));
1242
+ },
1243
+ resolveInteractionActionability(input) {
1244
+ return interactions.resolveInteractionActionability(inspect(input));
1245
+ },
1246
+ enumerateInteractionParams(input) {
1247
+ return interactions.enumerateInteractionParams({
1248
+ ...inspect(input),
1249
+ maxEvaluations: input.maxEvaluations
1250
+ });
1251
+ }
1252
+ };
1253
+ }
1254
+
1255
+ // src/testing/scenario-player-refs.ts
1256
+ var ScenarioCommandParamsError = class extends Error {
1257
+ path;
1258
+ constructor(path, message) {
1259
+ super(`${path}: ${message}`);
1260
+ this.name = "ScenarioCommandParamsError";
1261
+ this.path = path;
1262
+ }
1263
+ };
1264
+ function isScenarioSeatRef(value) {
1265
+ if (!isPlainRecord(value)) return false;
1266
+ const keys = Object.keys(value);
1267
+ return keys.length === 1 && keys[0] === "seat" && Number.isSafeInteger(value.seat) && value.seat >= 0;
1268
+ }
1269
+ function resolveScenarioSeatRef(options) {
1270
+ if (!isScenarioSeatRef(options.ref)) {
1271
+ throw new ScenarioCommandParamsError(
1272
+ options.path,
1273
+ "expected a scenario seat reference"
1274
+ );
1275
+ }
1276
+ const playerId = options.playerIds[options.ref.seat];
1277
+ if (playerId === void 0) {
1278
+ throw new ScenarioCommandParamsError(
1279
+ `${options.path}.seat`,
1280
+ `seat ${options.ref.seat} is out of range for ${options.playerIds.length} player(s)`
1281
+ );
1282
+ }
1283
+ return playerId;
1284
+ }
1285
+ function resolveScenarioCommandParams(options) {
1286
+ const candidateSchemas = authorParamSchemaCandidates(options);
1287
+ let firstSeatError;
1288
+ let firstIssue;
1289
+ for (const schema of candidateSchemas) {
1290
+ try {
1291
+ const resolved = resolveScenarioSeatReferences({
1292
+ schema,
1293
+ value: options.params,
1294
+ playerIds: options.playerIds,
1295
+ path: `${options.path}.params`
1296
+ });
1297
+ const parsed = schema.safeParse(resolved);
1298
+ if (parsed.success) {
1299
+ return resolved;
1300
+ }
1301
+ firstIssue ??= parsed.error.issues[0];
1302
+ } catch (error) {
1303
+ if (error instanceof ScenarioSchemaValueError) {
1304
+ firstSeatError ??= error;
1305
+ continue;
1306
+ }
1307
+ throw error;
1308
+ }
1309
+ }
1310
+ if (firstSeatError) {
1311
+ throw new ScenarioCommandParamsError(
1312
+ firstSeatError.path,
1313
+ firstSeatError.message
1314
+ );
1315
+ }
1316
+ const suffix = firstIssue?.path.map(
1317
+ (part) => typeof part === "number" ? `[${part}]` : `.${String(part)}`
1318
+ ).join("") ?? "";
1319
+ throw new ScenarioCommandParamsError(
1320
+ `${options.path}.params${suffix}`,
1321
+ firstIssue?.message ?? "invalid interaction parameters"
1322
+ );
1323
+ }
1324
+ function projectScenarioCommandParams(options) {
1325
+ const candidateSchemas = authorParamSchemaCandidates(options);
1326
+ let firstSeatError;
1327
+ for (const schema of candidateSchemas) {
1328
+ if (!schema.safeParse(options.params).success) continue;
1329
+ try {
1330
+ const projected = projectScenarioSeatReferences({
1331
+ schema,
1332
+ value: options.params,
1333
+ playerIds: options.playerIds,
1334
+ path: `${options.path}.params`
1335
+ });
1336
+ if (isPlainRecord(projected)) return projected;
1337
+ } catch (error) {
1338
+ if (error instanceof ScenarioSchemaValueError) {
1339
+ firstSeatError ??= error;
1340
+ continue;
1341
+ }
1342
+ throw error;
1343
+ }
1344
+ }
1345
+ if (firstSeatError) {
1346
+ throw new ScenarioCommandParamsError(
1347
+ firstSeatError.path,
1348
+ firstSeatError.message
1349
+ );
1350
+ }
1351
+ throw new ScenarioCommandParamsError(
1352
+ `${options.path}.params`,
1353
+ "runtime interaction parameters do not match an authoring parameter schema"
1354
+ );
1355
+ }
1356
+ function authorParamSchemaCandidates(options) {
1357
+ if (options.currentSchema) return [options.currentSchema];
1358
+ const schemas = createAuthorParamSchemasByPhase(options.game);
1359
+ const currentPhaseSchema = schemas[options.phase]?.[options.interactionId];
1360
+ const candidateSchemas = [
1361
+ ...currentPhaseSchema ? [currentPhaseSchema] : [],
1362
+ ...Object.entries(schemas).flatMap(([phase, schemasForPhase]) => {
1363
+ if (phase === options.phase) return [];
1364
+ const schema = schemasForPhase[options.interactionId];
1365
+ return schema ? [schema] : [];
1366
+ })
1367
+ ];
1368
+ if (candidateSchemas.length === 0) {
1369
+ throw new ScenarioCommandParamsError(
1370
+ `${options.path}.interactionId`,
1371
+ `interaction '${options.interactionId}' has no authoring parameter schema`
1372
+ );
1373
+ }
1374
+ return candidateSchemas;
1375
+ }
1376
+ function isPlainRecord(value) {
1377
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1378
+ return false;
1379
+ }
1380
+ const prototype = Object.getPrototypeOf(value);
1381
+ return prototype === Object.prototype || prototype === null;
1382
+ }
1383
+
1384
+ // src/testing/scenario-replay.ts
1385
+ async function replayScenario(options) {
1386
+ validateReplayDefinition(options.game, options.scenario);
1387
+ const scenario = cloneReplayDefinition(options.scenario);
1388
+ const checkpoint = normalizeCheckpoint(scenario, options.at);
1389
+ const playerIds = resolvePlayerRoster(options.game, scenario.setup.players);
1390
+ const events = [];
1391
+ const operations = {
1392
+ createBundle: (events2) => createScenarioBundle(options.game, events2, options.bundle),
1393
+ resolveCommandParams: (params) => resolveScenarioCommandParams({ ...params, game: options.game })
1394
+ };
1395
+ const bundle = operations.createBundle(events);
1396
+ const normalSetup = options.game.contract.manifest.normalSetup;
1397
+ const table = normalSetup.createInitialTable({ playerIds });
1398
+ const initialized = await bundle.initialize({
1399
+ table: RuntimeJsonSchema.parse(table),
1400
+ playerIds: [...playerIds],
1401
+ rngSeed: scenario.setup.seed,
1402
+ options: scenario.setup.options
1403
+ });
1404
+ const runtime = new ScenarioReplayImplementation({
1405
+ ...operations,
1406
+ game: options.game,
1407
+ scenario,
1408
+ playerIds,
1409
+ reducerState: initialized.state,
1410
+ terminal: initialized.terminal ?? null,
1411
+ checkpoint: { segment: "setup", completed: 0 },
1412
+ trace: [],
1413
+ events
1414
+ });
1415
+ await runtime.replayTo(checkpoint);
1416
+ return runtime;
1417
+ }
1418
+ async function materializeScenarioRuntimeCheckpoint(options) {
1419
+ const replay = requireScenarioReplayImplementation(
1420
+ await replayScenario(options)
1421
+ );
1422
+ return {
1423
+ checkpoint: structuredClone(replay.checkpoint),
1424
+ checkpointDigest: replay.checkpointDigest,
1425
+ playerIds: [...replay.playerIds],
1426
+ state: structuredClone(replay.reducerState),
1427
+ terminal: structuredClone(replay.terminal)
1428
+ };
1429
+ }
1430
+ function cloneReplayDefinition(scenario) {
1431
+ return structuredClone({
1432
+ id: scenario.id,
1433
+ ...scenario.description === void 0 ? {} : { description: scenario.description },
1434
+ setup: scenario.setup,
1435
+ given: scenario.given,
1436
+ when: scenario.when
1437
+ });
1438
+ }
1439
+ async function assertScenario(options) {
1440
+ const replay = requireScenarioReplayImplementation(options.replay);
1441
+ if (!replay.complete) {
1442
+ throw new Error(
1443
+ `Scenario '${replay.scenarioId}' assertions require a full replay; checkpoint is ${replay.checkpoint.segment}:${replay.checkpoint.completed}.`
1444
+ );
1445
+ }
1446
+ const context = {
1447
+ expect: createExpectApi({
1448
+ lastDiagnosticRejection: () => lastRejectedEvent(replay.diagnostics)
1449
+ }),
1450
+ state: replay.state,
1451
+ view: replay.view,
1452
+ interactions: replay.interactions,
1453
+ explain: replay.explain,
1454
+ get diagnostics() {
1455
+ return replay.diagnostics;
1456
+ },
1457
+ probe: (command) => replay.probe(command)
1458
+ };
1459
+ await options.assertion(context);
1460
+ }
1461
+ async function probeScenarioCommand(options) {
1462
+ return requireScenarioReplayImplementation(options.replay).probe(
1463
+ options.command
1464
+ );
1465
+ }
1466
+ var ScenarioReplayImplementation = class _ScenarioReplayImplementation {
1467
+ authoritySessionId = crypto.randomUUID();
1468
+ authorityVersion = 1;
1469
+ referenceBasis() {
1470
+ return {
1471
+ sessionId: this.authoritySessionId,
1472
+ version: this.authorityVersion
1473
+ };
1474
+ }
1475
+ operations;
1476
+ scenarioId;
1477
+ scenario;
1478
+ game;
1479
+ playerIds;
1480
+ bundle;
1481
+ events;
1482
+ checkpoint;
1483
+ reducerState;
1484
+ terminal;
1485
+ trace;
1486
+ constructor(state) {
1487
+ this.scenarioId = state.scenario.id;
1488
+ this.scenario = state.scenario;
1489
+ this.game = state.game;
1490
+ this.playerIds = [...state.playerIds];
1491
+ this.reducerState = structuredClone(state.reducerState);
1492
+ this.terminal = structuredClone(state.terminal);
1493
+ this.checkpoint = structuredClone(state.checkpoint);
1494
+ this.trace = structuredClone(
1495
+ state.trace
1496
+ );
1497
+ this.events = structuredClone(state.events);
1498
+ this.operations = {
1499
+ createBundle: state.createBundle,
1500
+ resolveCommandParams: state.resolveCommandParams
1501
+ };
1502
+ this.bundle = this.operations.createBundle(this.events);
1503
+ this.state = this.state.bind(this);
1504
+ this.view = this.view.bind(this);
1505
+ this.interactions = this.interactions.bind(this);
1506
+ this.explain = this.explain.bind(this);
1507
+ }
1508
+ get checkpointDigest() {
1509
+ return digestPluginRuntimeJson({
1510
+ digestVersion: "scenario-checkpoint@1",
1511
+ scenarioId: this.scenarioId,
1512
+ checkpoint: this.checkpoint,
1513
+ reducerState: this.reducerState,
1514
+ terminal: this.terminal,
1515
+ trace: this.trace
1516
+ });
1517
+ }
1518
+ get complete() {
1519
+ return this.checkpoint.segment === "when" && this.checkpoint.completed === this.scenario.when.length;
1520
+ }
1521
+ get diagnostics() {
1522
+ const flow = readFlowState(this.reducerState);
1523
+ return {
1524
+ events: structuredClone(this.events),
1525
+ lastDispatch: lastAcceptedDispatch(this.events),
1526
+ flow: {
1527
+ currentPhase: flow.currentPhase,
1528
+ activeSeats: flow.activePlayers.flatMap((playerId) => {
1529
+ const seat = this.playerIds.indexOf(playerId);
1530
+ return seat < 0 ? [] : [{ seat }];
1531
+ })
1532
+ }
1533
+ };
1534
+ }
1535
+ state() {
1536
+ return structuredClone(this.reducerState.domain);
1537
+ }
1538
+ view(seat) {
1539
+ const playerId = this.playerId(seat, "seat");
1540
+ const projection = this.project(playerId);
1541
+ return structuredClone(
1542
+ projection.seats?.[playerId]?.view ?? null
1543
+ );
1544
+ }
1545
+ interactions(seat) {
1546
+ const playerId = this.playerId(seat, "seat");
1547
+ const projection = this.project(playerId);
1548
+ const refs = projection.seats?.[playerId]?.availableInteractionRefs ?? [];
1549
+ const interactionsByRef = projection.interactionsByRef ?? {};
1550
+ return structuredClone(
1551
+ refs.flatMap((ref) => {
1552
+ const descriptor = interactionsByRef[ref];
1553
+ return descriptor ? [descriptor] : [];
1554
+ })
1555
+ );
1556
+ }
1557
+ explain(seat, interactionId) {
1558
+ return structuredClone(
1559
+ this.bundle.explainInteraction({
1560
+ referenceBasis: this.referenceBasis(),
1561
+ state: this.reducerState,
1562
+ playerId: this.playerId(seat, "seat"),
1563
+ interactionId
1564
+ })
1565
+ );
1566
+ }
1567
+ clone() {
1568
+ return new _ScenarioReplayImplementation({
1569
+ ...this.operations,
1570
+ game: this.game,
1571
+ scenario: this.scenario,
1572
+ playerIds: this.playerIds,
1573
+ reducerState: this.reducerState,
1574
+ terminal: this.terminal,
1575
+ checkpoint: this.checkpoint,
1576
+ trace: this.trace,
1577
+ events: this.events
1578
+ });
1579
+ }
1580
+ inspectionAuthority(perspective) {
1581
+ const selectedPlayerId = perspective.kind === "player" ? this.playerId({ seat: perspective.seat }, "perspective.seat") : null;
1582
+ const projection = this.bundle.project({
1583
+ state: this.reducerState,
1584
+ referenceBasis: this.referenceBasis(),
1585
+ playerIds: selectedPlayerId ? [selectedPlayerId] : []
1586
+ });
1587
+ const descriptors = selectedPlayerId ? descriptorsForPlayer(projection, selectedPlayerId) : [];
1588
+ const flow = readFlowState(this.reducerState);
1589
+ return {
1590
+ game: this.game,
1591
+ scenario: structuredClone(this.scenario),
1592
+ playerIds: [...this.playerIds],
1593
+ checkpoint: structuredClone(this.checkpoint),
1594
+ checkpointDigest: this.checkpointDigest,
1595
+ publicState: structuredClone(this.reducerState.domain.publicState),
1596
+ view: structuredClone(
1597
+ selectedPlayerId ? projection.seats[selectedPlayerId]?.view ?? null : null
1598
+ ),
1599
+ scheduler: {
1600
+ phase: flow.currentPhase ?? "",
1601
+ step: readPhaseStep(this.reducerState.domain.phase),
1602
+ activePlayerIds: [...projection.schedulerFlow?.activePlayerIds ?? []],
1603
+ pendingPlayerIds: [
1604
+ ...projection.schedulerFlow?.pendingPlayerIds ?? projection.simultaneousPhase?.pendingPlayerIds ?? []
1605
+ ],
1606
+ continuationDependencies: structuredClone(
1607
+ projection.schedulerFlow?.continuationDependencies ?? []
1608
+ )
1609
+ },
1610
+ interactions: descriptors.map((descriptor) => ({
1611
+ descriptor: structuredClone(descriptor),
1612
+ explanation: structuredClone(
1613
+ this.bundle.explainInteraction({
1614
+ referenceBasis: this.referenceBasis(),
1615
+ state: this.reducerState,
1616
+ playerId: selectedPlayerId,
1617
+ interactionId: String(descriptor.interactionId ?? "")
1618
+ })
1619
+ ),
1620
+ actionability: structuredClone(
1621
+ this.bundle.resolveInteractionActionability({
1622
+ referenceBasis: this.referenceBasis(),
1623
+ state: this.reducerState,
1624
+ playerId: selectedPlayerId,
1625
+ interactionId: String(descriptor.interactionId ?? "")
1626
+ })
1627
+ )
1628
+ })),
1629
+ entropy: {
1630
+ seed: this.reducerState.runtime.rng.seed,
1631
+ draws: structuredClone(this.reducerState.runtime.rng.draws ?? [])
1632
+ },
1633
+ commandTrace: structuredClone(this.trace)
1634
+ };
1635
+ }
1636
+ currentAuthorParamSchema(seat, interactionId) {
1637
+ return this.bundle.currentAuthorParamSchema({
1638
+ referenceBasis: this.referenceBasis(),
1639
+ state: this.reducerState,
1640
+ playerId: this.playerId({ seat }, "actor"),
1641
+ interactionId
1642
+ });
1643
+ }
1644
+ currentClientParamSchema(seat, interactionId) {
1645
+ return this.bundle.currentClientParamSchema({
1646
+ referenceBasis: this.referenceBasis(),
1647
+ state: this.reducerState,
1648
+ playerId: this.playerId({ seat }, "actor"),
1649
+ interactionId
1650
+ });
1651
+ }
1652
+ enumerateInteractionParams(options) {
1653
+ return structuredClone(
1654
+ this.bundle.enumerateInteractionParams({
1655
+ referenceBasis: this.referenceBasis(),
1656
+ state: this.reducerState,
1657
+ playerId: this.playerId({ seat: options.seat }, "perspective.seat"),
1658
+ interactionId: options.interactionId,
1659
+ maxEvaluations: options.maxEvaluations
1660
+ })
1661
+ );
1662
+ }
1663
+ async advance(command) {
1664
+ const clone = new _ScenarioReplayImplementation({
1665
+ ...this.operations,
1666
+ game: this.game,
1667
+ scenario: this.scenario,
1668
+ playerIds: this.playerIds,
1669
+ reducerState: this.reducerState,
1670
+ terminal: this.terminal,
1671
+ checkpoint: this.checkpoint,
1672
+ trace: this.trace,
1673
+ events: this.events
1674
+ });
1675
+ const result = await clone.dispatch(command, "probe", 0);
1676
+ if (result.kind === "accept") {
1677
+ clone.trace.push({
1678
+ segment: this.checkpoint.segment === "when" ? "when" : "given",
1679
+ index: this.checkpoint.completed,
1680
+ command: structuredClone(command),
1681
+ trace: structuredClone(result.trace)
1682
+ });
1683
+ return {
1684
+ kind: "accepted",
1685
+ replay: clone,
1686
+ trace: structuredClone(result.trace)
1687
+ };
1688
+ }
1689
+ return {
1690
+ kind: "rejected",
1691
+ errorCode: result.errorCode,
1692
+ ...result.message === void 0 ? {} : { message: result.message },
1693
+ trace: structuredClone(result.trace)
1694
+ };
1695
+ }
1696
+ async replayTo(target) {
1697
+ const givenCount = target.segment === "setup" ? 0 : target.segment === "given" ? target.completed : this.scenario.given.length;
1698
+ const whenCount = target.segment === "when" ? target.completed : 0;
1699
+ for (let index = 0; index < givenCount; index += 1) {
1700
+ await this.dispatchRequired("given", index, this.scenario.given[index]);
1701
+ }
1702
+ this.checkpoint = { segment: "given", completed: givenCount };
1703
+ for (let index = 0; index < whenCount; index += 1) {
1704
+ await this.dispatchRequired("when", index, this.scenario.when[index]);
1705
+ }
1706
+ this.checkpoint = structuredClone(target);
1707
+ }
1708
+ async probe(command) {
1709
+ const clone = requireScenarioReplayImplementation(this.clone());
1710
+ const result = await clone.dispatch(command, "probe", 0);
1711
+ if (result.kind === "accept") {
1712
+ const accepted = {
1713
+ kind: "accepted",
1714
+ command,
1715
+ checkpointDigest: digestPluginRuntimeJson({
1716
+ digestVersion: "scenario-probe@1",
1717
+ from: this.checkpointDigest,
1718
+ command,
1719
+ reducerState: clone.reducerState,
1720
+ trace: result.trace
1721
+ }),
1722
+ trace: result.trace,
1723
+ toBeAccepted() {
1724
+ },
1725
+ toRejectWith(expected) {
1726
+ scenarioAssertionFailure(
1727
+ `Expected probe to reject${expected.errorCode ? ` with ${expected.errorCode}` : ""}, but it was accepted.`
1728
+ );
1729
+ }
1730
+ };
1731
+ return accepted;
1732
+ }
1733
+ const rejected = {
1734
+ kind: "rejected",
1735
+ command,
1736
+ errorCode: result.errorCode,
1737
+ ...result.message === void 0 ? {} : { message: result.message },
1738
+ trace: result.trace,
1739
+ toBeAccepted() {
1740
+ scenarioAssertionFailure(
1741
+ `Expected probe to be accepted, but it rejected with ${result.errorCode}.`
1742
+ );
1743
+ },
1744
+ toRejectWith(expected) {
1745
+ assertRejectionMatches(result, expected);
1746
+ }
1747
+ };
1748
+ return rejected;
1749
+ }
1750
+ async dispatchRequired(segment, index, command) {
1751
+ const result = await this.dispatch(command, segment, index);
1752
+ if (result.kind === "reject") {
1753
+ throw new ScenarioReplayError({
1754
+ scenarioId: this.scenarioId,
1755
+ segment,
1756
+ index,
1757
+ interactionId: command.interactionId,
1758
+ errorCode: result.errorCode,
1759
+ reducerMessage: result.message,
1760
+ trace: result.trace
1761
+ });
1762
+ }
1763
+ this.trace.push({
1764
+ segment,
1765
+ index,
1766
+ command: structuredClone(command),
1767
+ trace: result.trace
1768
+ });
1769
+ }
1770
+ async dispatch(command, segment, index) {
1771
+ if (this.terminal)
1772
+ return { kind: "reject", errorCode: "game-ended", trace: [] };
1773
+ const phase = readFlowState(this.reducerState).currentPhase ?? "";
1774
+ const playerId = resolveScenarioSeatRef({
1775
+ ref: command.actor,
1776
+ playerIds: this.playerIds,
1777
+ path: `scenario.${segment}[${index}].actor`
1778
+ });
1779
+ const params = this.operations.resolveCommandParams({
1780
+ currentSchema: this.currentAuthorParamSchema(
1781
+ command.actor.seat,
1782
+ command.interactionId
1783
+ ),
1784
+ phase,
1785
+ interactionId: command.interactionId,
1786
+ params: command.params,
1787
+ playerIds: this.playerIds,
1788
+ path: `scenario.${segment}[${index}]`
1789
+ });
1790
+ const result = await this.bundle.dispatch({
1791
+ state: this.reducerState,
1792
+ referenceBasis: this.referenceBasis(),
1793
+ input: {
1794
+ kind: "interaction",
1795
+ basis: {
1796
+ ...this.referenceBasis(),
1797
+ perspectivePlayerId: playerId,
1798
+ actionSetVersion: "scenario"
1799
+ },
1800
+ playerId,
1801
+ interactionId: command.interactionId,
1802
+ params
1803
+ }
1804
+ });
1805
+ if (result.kind === "reject") {
1806
+ return {
1807
+ kind: "reject",
1808
+ errorCode: result.errorCode,
1809
+ ...result.message === void 0 ? {} : { message: result.message },
1810
+ trace: []
1811
+ };
1812
+ }
1813
+ this.reducerState = structuredClone(result.state);
1814
+ this.authorityVersion++;
1815
+ this.terminal = result.terminal ?? null;
1816
+ return {
1817
+ kind: "accept",
1818
+ trace: summarizeWireTrace(result.trace)
1819
+ };
1820
+ }
1821
+ playerId(seat, path) {
1822
+ return resolveScenarioSeatRef({
1823
+ ref: seat,
1824
+ playerIds: this.playerIds,
1825
+ path
1826
+ });
1827
+ }
1828
+ project(playerId) {
1829
+ return this.bundle.project({
1830
+ state: this.reducerState,
1831
+ referenceBasis: this.referenceBasis(),
1832
+ playerIds: [playerId]
1833
+ });
1834
+ }
1835
+ };
1836
+ function inspectScenarioReplayAuthority(options) {
1837
+ return requireScenarioReplayImplementation(
1838
+ options.replay
1839
+ ).inspectionAuthority(options.perspective);
1840
+ }
1841
+ function currentScenarioClientParamSchema(replay, seat, interactionId) {
1842
+ return requireScenarioReplayImplementation(replay).currentClientParamSchema(
1843
+ seat,
1844
+ interactionId
1845
+ );
1846
+ }
1847
+ function enumerateScenarioInteractionParams(options) {
1848
+ return requireScenarioReplayImplementation(
1849
+ options.replay
1850
+ ).enumerateInteractionParams(options);
1851
+ }
1852
+ function advanceScenarioReplay(options) {
1853
+ return requireScenarioReplayImplementation(options.replay).advance(
1854
+ options.command
1855
+ );
1856
+ }
1857
+ function descriptorsForPlayer(projection, playerId) {
1858
+ const refs = projection.seats[playerId]?.availableInteractionRefs ?? [];
1859
+ const interactionsByRef = projection.interactionsByRef ?? {};
1860
+ return refs.flatMap((ref) => {
1861
+ const descriptor = interactionsByRef[ref];
1862
+ return descriptor ? [descriptor] : [];
1863
+ });
1864
+ }
1865
+ function createScenarioBundle(game, events, bundle) {
1866
+ return createReducerTestingRuntime(
1867
+ game,
1868
+ {
1869
+ diagnostics: {
1870
+ event(event) {
1871
+ events.push(structuredClone(event));
1872
+ }
1873
+ }
1874
+ },
1875
+ bundle
1876
+ );
1877
+ }
1878
+ function validateReplayDefinition(game, replay) {
1879
+ validateScenarioDefinition(game, {
1880
+ ...replay,
1881
+ then: () => void 0
1882
+ });
1883
+ }
1884
+ function normalizeCheckpoint(scenario, checkpoint) {
1885
+ if (checkpoint === void 0) {
1886
+ return { segment: "when", completed: scenario.when.length };
1887
+ }
1888
+ const maximum = checkpoint.segment === "setup" ? 0 : checkpoint.segment === "given" ? scenario.given.length : scenario.when.length;
1889
+ if (!Number.isSafeInteger(checkpoint.completed) || checkpoint.completed < 0 || checkpoint.completed > maximum || checkpoint.segment === "setup" && checkpoint.completed !== 0) {
1890
+ throw new ScenarioDefinitionValidationError({
1891
+ code: "OUT_OF_RANGE",
1892
+ path: "at.completed",
1893
+ reason: `expected 0 through ${maximum} for ${checkpoint.segment}`
1894
+ });
1895
+ }
1896
+ return structuredClone(checkpoint);
1897
+ }
1898
+ function readFlowState(state) {
1899
+ const flow = state.domain?.flow;
1900
+ if (typeof flow !== "object" || flow === null) {
1901
+ return { currentPhase: null, activePlayers: [] };
1902
+ }
1903
+ const record = flow;
1904
+ return {
1905
+ currentPhase: typeof record.currentPhase === "string" ? record.currentPhase : null,
1906
+ activePlayers: Array.isArray(record.activePlayers) ? record.activePlayers.filter(
1907
+ (playerId) => typeof playerId === "string"
1908
+ ) : []
1909
+ };
1910
+ }
1911
+ function readPhaseStep(phase) {
1912
+ if (!phase || typeof phase !== "object") return null;
1913
+ const step = phase.step;
1914
+ return typeof step === "string" ? step : null;
1915
+ }
1916
+ function summarizeWireTrace(trace) {
1917
+ return trace.flatMap((entry) => {
1918
+ switch (entry.kind) {
1919
+ case "acceptedClientInput":
1920
+ return [
1921
+ {
1922
+ kind: "acceptedClientInput",
1923
+ interactionId: entry.input.interactionId,
1924
+ playerId: entry.input.playerId
1925
+ }
1926
+ ];
1927
+ case "phaseEntered":
1928
+ return [{ kind: "phaseEntered", from: entry.from, to: entry.to }];
1929
+ case "rngConsumption":
1930
+ return [
1931
+ {
1932
+ kind: "rngConsumption",
1933
+ version: entry.version,
1934
+ operation: entry.operation,
1935
+ drawIndex: entry.drawIndex
1936
+ }
1937
+ ];
1938
+ }
1939
+ });
1940
+ }
1941
+ function lastAcceptedDispatch(events) {
1942
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1943
+ const event = events[index];
1944
+ if (event?.type === "submitAccepted") {
1945
+ return {
1946
+ submissionId: event.submissionId,
1947
+ trace: structuredClone(event.trace)
1948
+ };
1949
+ }
1950
+ }
1951
+ return null;
1952
+ }
1953
+ function lastRejectedEvent(diagnostics) {
1954
+ for (let index = diagnostics.events.length - 1; index >= 0; index -= 1) {
1955
+ const event = diagnostics.events[index];
1956
+ if (event?.type === "submitRejected") return event;
1957
+ }
1958
+ return null;
1959
+ }
1960
+ function assertRejectionMatches(rejection, expected) {
1961
+ if (expected.errorCode !== void 0 && rejection.errorCode !== expected.errorCode) {
1962
+ scenarioAssertionFailure(
1963
+ `Expected rejection errorCode '${expected.errorCode}', received '${rejection.errorCode}'.`
1964
+ );
1965
+ }
1966
+ if (typeof expected.message === "string" && rejection.message !== expected.message) {
1967
+ scenarioAssertionFailure(
1968
+ `Expected rejection message '${expected.message}', received '${rejection.message ?? "undefined"}'.`
1969
+ );
1970
+ }
1971
+ if (expected.message instanceof RegExp && !expected.message.test(rejection.message ?? "")) {
1972
+ scenarioAssertionFailure(
1973
+ `Expected rejection message '${rejection.message ?? ""}' to match ${String(expected.message)}.`
1974
+ );
1975
+ }
1976
+ }
1977
+ function requireScenarioReplayImplementation(replay) {
1978
+ if (!(replay instanceof ScenarioReplayImplementation)) {
1979
+ throw new Error(
1980
+ "Scenario replay was not created by replayScenario(); alternate replay implementations are not supported."
1981
+ );
1982
+ }
1983
+ return replay;
1984
+ }
50
1985
 
51
1986
  // src/testing/inspection/flow-diagnostic.ts
52
1987
  function createFlowDiagnostic(options) {
@@ -126,7 +2061,6 @@ function inspectScenarioReplayNode(options) {
126
2061
  });
127
2062
  const perspective = resolvePerspective(
128
2063
  options.perspective,
129
- baseAuthority.scenario.setup.players,
130
2064
  baseAuthority.playerIds
131
2065
  );
132
2066
  const authority = inspectScenarioReplayAuthority({
@@ -178,17 +2112,17 @@ function inspectScenarioReplayNode(options) {
178
2112
  };
179
2113
  return node;
180
2114
  }
181
- function resolvePerspective(selector, playerCount, declaredPlayerIds) {
2115
+ function resolvePerspective(selector, playerIds) {
182
2116
  if (selector.kind === "spectator") return { kind: "spectator" };
183
2117
  const seat = selector.seat;
184
- if (!Number.isSafeInteger(seat) || seat < 0 || seat >= playerCount) {
2118
+ if (!Number.isSafeInteger(seat) || seat < 0 || seat >= playerIds.length) {
185
2119
  throw new ScenarioInspectionError({
186
2120
  code: "TEST_PERSPECTIVE_INVALID",
187
- message: `Perspective seat ${String(seat)} is outside this ${playerCount}-player scenario.`,
2121
+ message: `Perspective seat ${String(seat)} is outside this ${playerIds.length}-player scenario.`,
188
2122
  context: {
189
2123
  requestedSeat: seat,
190
2124
  minimumSeat: 0,
191
- maximumSeat: Math.max(0, playerCount - 1)
2125
+ maximumSeat: Math.max(0, playerIds.length - 1)
192
2126
  }
193
2127
  });
194
2128
  }
@@ -196,7 +2130,7 @@ function resolvePerspective(selector, playerCount, declaredPlayerIds) {
196
2130
  kind: "player",
197
2131
  actor: {
198
2132
  seat,
199
- playerId: declaredPlayerIds[seat] ?? `player-${seat + 1}`
2133
+ playerId: playerIds[seat]
200
2134
  }
201
2135
  };
202
2136
  }
@@ -234,11 +2168,12 @@ function projectDispatchTrace(commandTrace, playerIds, perspective) {
234
2168
  }
235
2169
  ];
236
2170
  }
237
- case "appliedInstruction":
2171
+ case "phaseEntered":
238
2172
  return [
239
2173
  {
240
- kind: "appliedInstruction",
241
- instructionKind: trace.instruction
2174
+ kind: "phaseEntered",
2175
+ from: trace.from,
2176
+ to: trace.to
242
2177
  }
243
2178
  ];
244
2179
  case "rngConsumption":
@@ -266,9 +2201,39 @@ function assertSeed(seed) {
266
2201
  }
267
2202
  }
268
2203
 
2204
+ // src/testing/canonical.ts
2205
+ function canonicalScenarioJson(value) {
2206
+ return encodeCanonicalPluginRuntimeJson(value);
2207
+ }
2208
+ function digestScenarioJson(value) {
2209
+ return digestPluginRuntimeJson(value);
2210
+ }
2211
+ function compareCanonicalScenarioJson(left, right) {
2212
+ const leftJson = canonicalScenarioJson(left);
2213
+ const rightJson = canonicalScenarioJson(right);
2214
+ return leftJson === rightJson ? 0 : leftJson < rightJson ? -1 : 1;
2215
+ }
2216
+
269
2217
  // src/testing/exploration/cursor.ts
270
- import { Buffer } from "buffer";
2218
+ import * as z3 from "zod";
271
2219
  var CURSOR_VERSION = 1;
2220
+ var cursorPayloadSchema = z3.strictObject({
2221
+ version: z3.literal(CURSOR_VERSION),
2222
+ scenarioSourceDigest: z3.templateLiteral(["sha256:", z3.string()]),
2223
+ checkpointDigest: z3.templateLiteral(["sha256:", z3.string()]),
2224
+ perspective: z3.discriminatedUnion("kind", [
2225
+ z3.strictObject({
2226
+ kind: z3.literal("player"),
2227
+ actor: z3.strictObject({
2228
+ seat: z3.number().int().nonnegative(),
2229
+ playerId: z3.string()
2230
+ })
2231
+ }),
2232
+ z3.strictObject({ kind: z3.literal("spectator") })
2233
+ ]),
2234
+ seedOverride: z3.number().nullable(),
2235
+ nextOrdinal: z3.number().int().nonnegative()
2236
+ });
272
2237
  var ExploreCursorError = class extends Error {
273
2238
  code = "TEST_EXPLORE_CURSOR_STALE";
274
2239
  scenarioSourceDigest;
@@ -290,9 +2255,12 @@ function createExploreCursor(options) {
290
2255
  seedOverride: options.seedOverride ?? null,
291
2256
  nextOrdinal: options.nextOrdinal
292
2257
  };
293
- const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString(
294
- "base64url"
295
- );
2258
+ const encoded = btoa(
2259
+ Array.from(
2260
+ new TextEncoder().encode(JSON.stringify(payload)),
2261
+ (byte) => String.fromCharCode(byte)
2262
+ ).join("")
2263
+ ).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
296
2264
  const digest = digestPluginRuntimeJson(payload).slice("sha256:".length);
297
2265
  return `dbx1.${encoded}.${digest}`;
298
2266
  }
@@ -307,16 +2275,22 @@ function readExploreCursor(options) {
307
2275
  if (prefix !== "dbx1" || !encoded || !digest || extra !== void 0) stale();
308
2276
  let payload;
309
2277
  try {
310
- payload = JSON.parse(
311
- Buffer.from(encoded, "base64url").toString("utf8")
2278
+ payload = cursorPayloadSchema.parse(
2279
+ JSON.parse(
2280
+ new TextDecoder("utf-8", { fatal: true }).decode(
2281
+ Uint8Array.from(
2282
+ atob(encoded.replaceAll("-", "+").replaceAll("_", "/")),
2283
+ (character) => character.charCodeAt(0)
2284
+ )
2285
+ )
2286
+ )
312
2287
  );
313
2288
  } catch {
314
2289
  return stale();
315
2290
  }
316
- if (digestPluginRuntimeJson(payload).slice("sha256:".length) !== digest || payload.version !== CURSOR_VERSION || payload.scenarioSourceDigest !== options.scenario.sourceDigest || payload.checkpointDigest !== options.checkpointDigest || payload.seedOverride !== (options.seedOverride ?? null) || JSON.stringify(payload.perspective) !== JSON.stringify(options.perspective)) {
2291
+ if (digestPluginRuntimeJson(payload).slice("sha256:".length) !== digest || payload.scenarioSourceDigest !== options.scenario.sourceDigest || payload.checkpointDigest !== options.checkpointDigest || payload.seedOverride !== (options.seedOverride ?? null) || JSON.stringify(payload.perspective) !== JSON.stringify(options.perspective)) {
317
2292
  return stale();
318
2293
  }
319
- assertOrdinal(payload.nextOrdinal);
320
2294
  return payload.nextOrdinal;
321
2295
  }
322
2296
  function assertOrdinal(value) {
@@ -436,18 +2410,22 @@ async function exploreTransitions(options) {
436
2410
  interactionId: action.interactionId,
437
2411
  params: projectScenarioCommandParams({
438
2412
  game: options.game,
2413
+ currentSchema: currentScenarioClientParamSchema(
2414
+ replay,
2415
+ action.actor.seat,
2416
+ action.interactionId
2417
+ ),
439
2418
  phase: node.flow.phase,
440
2419
  interactionId: action.interactionId,
441
2420
  params: assignment,
442
- playerIds: node.setup.players ? Array.from(
443
- { length: node.setup.players },
444
- (_, seat) => options.game.contract.manifest.literals.playerIds[seat] ?? `player-${seat + 1}`
445
- ) : [],
2421
+ playerIds: replay.state().table.playerOrder,
446
2422
  path: `explore.${action.interactionId}`
447
2423
  })
448
2424
  };
449
2425
  const advanced = await advanceScenarioReplay({
450
2426
  replay,
2427
+ // Both the action and assignment came from this game's authoritative
2428
+ // collector enumeration; bind their dynamic keys at the replay boundary.
451
2429
  command
452
2430
  });
453
2431
  if (advanced.kind !== "accepted") continue;
@@ -461,9 +2439,7 @@ async function exploreTransitions(options) {
461
2439
  after: {
462
2440
  checkpointDigest: afterNode.checkpointDigest,
463
2441
  flow: afterNode.flow,
464
- publicStateDigest: digestScenarioJson(
465
- afterNode.publicState
466
- ),
2442
+ publicStateDigest: digestScenarioJson(afterNode.publicState),
467
2443
  viewDigest: digestScenarioJson(afterNode.view),
468
2444
  actions: afterNode.actions.map(({ actor, interactionId }) => ({
469
2445
  actor,
@@ -539,7 +2515,7 @@ async function exploreSeeds(options, range) {
539
2515
  actions: inspected.node.actions.map(({ actor, interactionId }) => ({
540
2516
  seat: actor.seat,
541
2517
  interactionId,
542
- concreteOptionCount: counts.get(`${actor.seat}:${interactionId}`) ?? "lazy"
2518
+ concreteOptionCount: counts.get(`${actor.seat}:${interactionId}`) ?? "unknown"
543
2519
  }))
544
2520
  }
545
2521
  });
@@ -582,7 +2558,7 @@ async function concreteOptionCounts(options) {
582
2558
  for (const action of options.node.actions) {
583
2559
  const remaining = options.maxEvaluations - evaluated;
584
2560
  if (remaining <= 0) {
585
- counts.set(`${action.actor.seat}:${action.interactionId}`, "lazy");
2561
+ counts.set(`${action.actor.seat}:${action.interactionId}`, "unknown");
586
2562
  continue;
587
2563
  }
588
2564
  const result = enumerateScenarioInteractionParams({
@@ -595,7 +2571,7 @@ async function concreteOptionCounts(options) {
595
2571
  evaluated += result.enumeration.evaluated;
596
2572
  counts.set(
597
2573
  `${action.actor.seat}:${action.interactionId}`,
598
- result.enumeration.status === "enumerated" ? result.enumeration.assignments.length : "lazy"
2574
+ result.enumeration.status === "enumerated" ? result.enumeration.assignments.length : "unknown"
599
2575
  );
600
2576
  }
601
2577
  return counts;
@@ -644,112 +2620,29 @@ function assertSeedRange(range) {
644
2620
  }
645
2621
  }
646
2622
 
647
- // src/testing/ui-fixture/canonical.ts
648
- import { createHash } from "crypto";
649
- var sha256DigestPattern = /^sha256:[a-f0-9]{64}$/;
650
- function compareCanonicalStrings(left, right) {
651
- return left === right ? 0 : left < right ? -1 : 1;
652
- }
653
- function isSha256Digest(value) {
654
- return sha256DigestPattern.test(value);
655
- }
656
- function canonicalizeUIFixtureJson(value) {
657
- if (value === null) {
658
- return null;
659
- }
660
- switch (typeof value) {
661
- case "boolean":
662
- case "string":
663
- return value;
664
- case "number":
665
- if (Number.isFinite(value)) {
666
- return value;
667
- }
668
- break;
669
- case "object": {
670
- if (Array.isArray(value)) {
671
- return value.map((item) => canonicalizeUIFixtureJson(item));
672
- }
673
- if (Object.getPrototypeOf(value) !== Object.prototype) {
674
- break;
675
- }
676
- const entries = Object.entries(value);
677
- return Object.fromEntries(
678
- entries.filter(([, item]) => item !== void 0).sort(([left], [right]) => compareCanonicalStrings(left, right)).map(([key, item]) => [key, canonicalizeUIFixtureJson(item)])
679
- );
680
- }
681
- }
682
- throw new Error(
683
- `UI fixture values must be deterministic JSON. Received ${typeof value}.`
684
- );
685
- }
686
- function canonicalUIFixtureJson(value) {
687
- return JSON.stringify(canonicalizeUIFixtureJson(value));
688
- }
689
- function digestUIFixtureJson(value) {
690
- return sha256(canonicalUIFixtureJson(value));
691
- }
692
- function digestUIFixtureRequest(request) {
693
- return digestUIFixtureJson({
694
- digestVersion: "ui-replay-request@2",
695
- request
696
- });
697
- }
698
- function digestUIFixtureTransportRequest(request) {
699
- return digestUIFixtureJson({
700
- digestVersion: "ui-fixture-runtime-command@3",
701
- request
702
- });
703
- }
704
- function digestPluginProtocolTape(tape) {
705
- return digestUIFixtureJson({
706
- digestVersion: "plugin-protocol-tape@2",
707
- tape
708
- });
709
- }
710
- function canonicalizeUIScenarioFixture(fixture) {
711
- return canonicalizeUIFixtureJson({
712
- ...fixture,
713
- tags: [...fixture.tags].sort(),
714
- environment: {
715
- ...fixture.environment,
716
- viewportTags: [...fixture.environment.viewportTags].sort()
717
- }
718
- });
719
- }
720
- function serializeUIScenarioFixture(fixture) {
721
- return `${JSON.stringify(canonicalizeUIScenarioFixture(fixture), null, 2)}
722
- `;
723
- }
724
- function digestUIScenarioFixture(fixture) {
725
- return sha256(serializeUIScenarioFixture(fixture));
726
- }
727
- function sha256(value) {
728
- return `sha256:${createHash("sha256").update(value).digest("hex")}`;
729
- }
730
-
731
2623
  // src/testing/reducer-scenario/create-reducer-scenario-runner.ts
732
2624
  function createReducerScenarioRunner(options) {
733
2625
  return {
734
2626
  async run(operations) {
735
2627
  let state = structuredClone(options.initialState);
736
2628
  let gameVersion = options.initialGameVersion ?? 1;
2629
+ const sessionId = crypto.randomUUID();
737
2630
  const frames = [];
738
2631
  const exchanges = [];
739
2632
  const appendFrame = (id) => {
740
2633
  const dynamicProjection = options.bundle.project({
741
2634
  state,
2635
+ referenceBasis: { sessionId, version: gameVersion },
742
2636
  playerIds: [options.viewer.playerId]
743
2637
  });
744
- const staticProjection = options.bundle.boardStatic();
745
- const flow = readFlowState(state);
2638
+ const flow = readFlowState2(state);
746
2639
  const materialized = materializePluginGameplayFrame({
747
2640
  currentPhase: flow.currentPhase,
748
2641
  activePlayers: flow.activePlayers,
749
2642
  dynamicProjection,
750
- staticProjection,
751
2643
  perspectivePlayerId: options.viewer.playerId,
752
2644
  version: gameVersion,
2645
+ sessionId,
753
2646
  actionSetVersion: "pending"
754
2647
  });
755
2648
  const actionSetVersion = computePluginActionSetVersion({
@@ -760,15 +2653,13 @@ function createReducerScenarioRunner(options) {
760
2653
  id,
761
2654
  reducerState: structuredClone(state),
762
2655
  dynamicProjection,
763
- staticProjection,
764
2656
  gameVersion,
765
2657
  actionSetVersion,
766
- projectionDigest: digestUIFixtureJson({
2658
+ projectionDigest: digestScenarioJson({
767
2659
  digestVersion: "reducer-scenario-frame@2",
768
2660
  scenarioId: options.scenarioId,
769
2661
  frameId: id,
770
2662
  dynamicProjection,
771
- staticProjection,
772
2663
  gameVersion,
773
2664
  actionSetVersion
774
2665
  })
@@ -785,6 +2676,7 @@ function createReducerScenarioRunner(options) {
785
2676
  if (operation.operation === "validate") {
786
2677
  const dispatched = await options.bundle.dispatch({
787
2678
  state: structuredClone(state),
2679
+ referenceBasis: { sessionId, version: gameVersion },
788
2680
  input: operation.input
789
2681
  });
790
2682
  const result2 = dispatched.kind === "accept" ? { valid: true } : {
@@ -803,6 +2695,7 @@ function createReducerScenarioRunner(options) {
803
2695
  }
804
2696
  const result = await options.bundle.dispatch({
805
2697
  state,
2698
+ referenceBasis: { sessionId, version: gameVersion },
806
2699
  input: operation.input
807
2700
  });
808
2701
  if (result.kind === "reject") {
@@ -847,857 +2740,618 @@ function createReducerScenarioRunner(options) {
847
2740
  }
848
2741
  };
849
2742
  }
850
- function readFlowState(state) {
2743
+ function readFlowState2(state) {
851
2744
  return {
852
2745
  currentPhase: state.domain?.flow?.currentPhase ?? null,
853
2746
  activePlayers: [...state.domain?.flow?.activePlayers ?? []]
854
2747
  };
855
2748
  }
856
2749
 
857
- // src/testing/ui-scenario/define-ui-scenario.ts
858
- function defineUIScenario(scenario) {
859
- return Object.freeze(scenario);
860
- }
861
-
862
- // src/testing/ui-fixture/create-fixture-host-harness.ts
863
- var DEFAULT_CHANNEL_ID = "fixture-channel";
864
- function createFixtureHostHarness(options) {
865
- const strict = options.strict ?? true;
866
- const latencyMs = options.latencyMs ?? 0;
867
- const channelId = options.channelId ?? DEFAULT_CHANNEL_ID;
868
- const events = [];
869
- const frameById = new Map(
870
- options.tape.frames.map((frame) => [frame.id, frame])
871
- );
872
- let eventSequence = 0;
873
- let envelopeSequence = 0;
874
- let stepCursor = 0;
875
- let currentFrameId = null;
876
- let onMessage = null;
877
- let firstFixtureError = null;
878
- const pendingHandlers = /* @__PURE__ */ new Set();
879
- const record = (event) => {
880
- const entry = {
881
- sequence: ++eventSequence,
882
- atMs: options.nowMs?.() ?? eventSequence,
883
- ...event
884
- };
885
- events.push(entry);
886
- options.onEvent?.(entry);
887
- };
888
- const createFixtureError = (code, message) => {
889
- record({ kind: "diagnostic" });
890
- const error = new Error(message);
891
- error.name = "FixtureHostHarnessError";
892
- Object.assign(error, { errorCode: code });
893
- return error;
894
- };
895
- const rememberFixtureError = (error) => {
896
- if (firstFixtureError) {
897
- return;
898
- }
899
- firstFixtureError = error instanceof Error ? error : createFixtureError(
900
- "fixture-unknown-error",
901
- "Fixture host failed with an unknown error."
902
- );
903
- };
904
- const throwFixtureError = () => {
905
- if (firstFixtureError) {
906
- throw firstFixtureError;
907
- }
908
- };
909
- const sendHostPayload = (payload) => {
910
- if (!onMessage) {
911
- return;
912
- }
913
- onMessage({
914
- protocol: DREAMBOARD_PLUGIN_PROTOCOL,
915
- version: DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
916
- channelId,
917
- sequence: ++envelopeSequence,
918
- payload
919
- });
920
- };
921
- const sendInit = () => {
922
- sendHostPayload({
923
- type: "runtime.init",
924
- session: options.tape.session
925
- });
926
- };
927
- const publishFrame = (frameId) => {
928
- const frame = frameById.get(frameId);
929
- if (!frame) {
930
- throw createFixtureError(
931
- "fixture-missing-frame",
932
- `Plugin protocol tape references missing frame '${frameId}'.`
2750
+ // src/testing/sources/test-source.ts
2751
+ function createTestSource(initial) {
2752
+ const sessionId = crypto.randomUUID();
2753
+ const submissions = [];
2754
+ const lifecycle = createSourceLifecycle({
2755
+ send(command) {
2756
+ submissions.push(
2757
+ Object.freeze({
2758
+ operation: command.type === "interaction.submit" ? "submit" : "cancel",
2759
+ interactionId: command.interactionId,
2760
+ ...command.type === "interaction.submit" ? { params: immutableCopy(command.params) } : {},
2761
+ resolve(result) {
2762
+ lifecycle.result({
2763
+ ...result,
2764
+ type: "interaction.result",
2765
+ clientActionId: command.clientActionId
2766
+ });
2767
+ }
2768
+ })
933
2769
  );
2770
+ },
2771
+ recover() {
2772
+ },
2773
+ close() {
934
2774
  }
935
- currentFrameId = frame.id;
936
- record({
937
- kind: "frame-sent",
938
- frameId: frame.id,
939
- projectionDigest: frame.projectionDigest
940
- });
941
- sendHostPayload({
942
- type: "gameplay.frame",
943
- frame: frame.frame
2775
+ });
2776
+ function emit(value) {
2777
+ const snapshot = SourceSnapshotSchema.parse(value);
2778
+ lifecycle.session({ sessionId, players: snapshot.players });
2779
+ lifecycle.frame({
2780
+ ...snapshot.frame,
2781
+ basis: {
2782
+ sessionId,
2783
+ version: snapshot.version,
2784
+ perspectivePlayerId: snapshot.me,
2785
+ actionSetVersion: computePluginActionSetVersion({
2786
+ version: snapshot.version,
2787
+ availableInteractions: snapshot.frame.availableInteractions
2788
+ })
2789
+ }
944
2790
  });
2791
+ }
2792
+ emit(initial);
2793
+ return {
2794
+ ...lifecycle.source,
2795
+ get submissions() {
2796
+ return [...submissions];
2797
+ },
2798
+ emit,
2799
+ recovering: lifecycle.recovering,
2800
+ fail: lifecycle.fail
945
2801
  };
946
- const drainHostFrames = () => {
947
- while (options.tape.steps[stepCursor]?.kind === "host.frame") {
948
- const step = options.tape.steps[stepCursor];
949
- stepCursor += 1;
950
- publishFrame(step.frameId);
951
- }
952
- };
953
- const drainOneHostFrame = () => {
954
- if (options.tape.steps[stepCursor]?.kind !== "host.frame") {
955
- return;
956
- }
957
- const step = options.tape.steps[stepCursor];
958
- stepCursor += 1;
959
- publishFrame(step.frameId);
960
- };
961
- const waitForConfiguredLatency = async () => {
962
- if (latencyMs <= 0) {
963
- await Promise.resolve();
964
- return;
965
- }
966
- await new Promise((resolve) => setTimeout(resolve, latencyMs));
2802
+ }
2803
+
2804
+ // src/testing/sources/local-source.ts
2805
+ import { createStore } from "@tanstack/store";
2806
+ import * as z4 from "zod";
2807
+ var checkpointSchema = z4.object({
2808
+ state: ReducerSessionStateSchema,
2809
+ terminal: GameOutcomeSchema.nullable()
2810
+ }).strict();
2811
+ async function localSource(game, options) {
2812
+ const setup = game.contract.manifest.normalSetup;
2813
+ const playerIds = resolvePlayerRoster(game, options.players);
2814
+ const runtime = createReducerTestingRuntime(game);
2815
+ const initial = await runtime.initialize({
2816
+ table: RuntimeJsonSchema.parse(setup.createInitialTable({ playerIds })),
2817
+ playerIds,
2818
+ rngSeed: options.seed,
2819
+ options: options.options
2820
+ });
2821
+ return createLocalProvider(
2822
+ game,
2823
+ runtime,
2824
+ playerIds,
2825
+ { state: initial.state, terminal: initial.terminal ?? null },
2826
+ options.as ?? playerIds[0]
2827
+ );
2828
+ }
2829
+ function createLocalProvider(game, runtime, playerIds, initial, selectedPlayerId) {
2830
+ let checkpoint = immutableCopy(checkpointSchema.parse(initial));
2831
+ let revision = 1;
2832
+ let playerId = selectedPlayerId;
2833
+ let epoch = 0;
2834
+ let disposed = false;
2835
+ let inFlight = false;
2836
+ let disconnect = () => {
967
2837
  };
968
- const consumeExpectedStep = (kind, requestDigest) => {
969
- const step = options.tape.steps[stepCursor];
970
- if (!step) {
971
- throw createFixtureError(
972
- "fixture-protocol-exhausted",
973
- `Plugin protocol tape has no remaining step for ${kind}.`
974
- );
975
- }
976
- if (step.kind !== kind) {
977
- throw createFixtureError(
978
- "fixture-protocol-step-mismatch",
979
- `Expected protocol step '${step.kind}' but received '${kind}'.`
980
- );
981
- }
982
- if (step.fromFrameId !== currentFrameId) {
983
- throw createFixtureError(
984
- "fixture-protocol-frame-mismatch",
985
- `Expected protocol from frame '${step.fromFrameId}' but current frame is '${currentFrameId ?? "<none>"}'.`
986
- );
987
- }
988
- if (step.requestDigest !== requestDigest) {
989
- const mode = strict ? "strict" : "compatible";
990
- throw createFixtureError(
991
- "fixture-protocol-digest-mismatch",
992
- `${mode} fixture protocol digest mismatch for ${kind}: expected ${step.requestDigest}, received ${requestDigest}.`
993
- );
994
- }
995
- stepCursor += 1;
996
- return step;
2838
+ const session = {
2839
+ sessionId: crypto.randomUUID(),
2840
+ players: playerIds.map((id) => ({ playerId: id, displayName: id }))
997
2841
  };
998
- const handleSubmit = async (command) => {
999
- const requestDigest = digestUIFixtureTransportRequest({
1000
- operation: "submit",
1001
- basis: command.basis,
1002
- interactionId: command.interactionId,
1003
- payload: command.params
1004
- });
1005
- const step = consumeExpectedStep("client.submit", requestDigest);
1006
- record({
1007
- kind: "submit-received",
1008
- requestDigest,
1009
- result: step.response.accepted ? "accepted" : "rejected"
2842
+ const store = createStore({
2843
+ snapshot: null,
2844
+ connection: "connecting",
2845
+ request: null,
2846
+ failure: null
2847
+ });
2848
+ let lifecycle;
2849
+ function assertOpen() {
2850
+ if (disposed) throw new Error("Local source disposed.");
2851
+ }
2852
+ function assertIdle() {
2853
+ assertOpen();
2854
+ if (inFlight || store.get().request)
2855
+ throw new Error("A gameplay interaction is already pending.");
2856
+ }
2857
+ function makeFrame(state = checkpoint.state, perspective = playerId) {
2858
+ const projection = runtime.project({
2859
+ state,
2860
+ playerIds: [perspective],
2861
+ referenceBasis: { sessionId: session.sessionId, version: revision }
1010
2862
  });
1011
- await waitForConfiguredLatency();
1012
- sendHostPayload({
1013
- ...step.response,
1014
- clientActionId: command.clientActionId
2863
+ const provisional = materializePluginGameplayFrame({
2864
+ currentPhase: state.domain.flow.currentPhase,
2865
+ activePlayers: projection.schedulerFlow?.activePlayerIds ?? [],
2866
+ dynamicProjection: projection,
2867
+ perspectivePlayerId: perspective,
2868
+ sessionId: session.sessionId,
2869
+ version: revision,
2870
+ actionSetVersion: "pending"
1015
2871
  });
1016
- if (step.response.accepted) {
1017
- await waitForConfiguredLatency();
1018
- drainHostFrames();
1019
- }
1020
- };
1021
- const handleClientPayload = (payload) => {
1022
- const trackHandler = (promise) => {
1023
- pendingHandlers.add(promise);
1024
- promise.catch((error) => {
1025
- rememberFixtureError(error);
1026
- }).finally(() => {
1027
- pendingHandlers.delete(promise);
1028
- });
1029
- };
1030
- switch (payload.type) {
1031
- case "runtime.ready":
1032
- record({ kind: "ready-received" });
1033
- break;
1034
- case "runtime.ack":
1035
- record({ kind: "ack-received" });
1036
- break;
1037
- case "interaction.submit":
1038
- trackHandler(handleSubmit(payload));
1039
- break;
1040
- case "runtime.error":
1041
- record({ kind: "diagnostic" });
1042
- break;
1043
- default: {
1044
- const _exhaustive = payload;
1045
- return _exhaustive;
2872
+ return {
2873
+ ...provisional,
2874
+ basis: {
2875
+ ...provisional.basis,
2876
+ actionSetVersion: computePluginActionSetVersion({
2877
+ version: revision,
2878
+ availableInteractions: provisional.availableInteractions
2879
+ })
1046
2880
  }
1047
- }
1048
- };
1049
- const transport = {
1050
- start(nextOnMessage) {
1051
- onMessage = nextOnMessage;
1052
- queueMicrotask(() => {
1053
- sendInit();
1054
- drainOneHostFrame();
2881
+ };
2882
+ }
2883
+ function publish() {
2884
+ lifecycle.frame(makeFrame());
2885
+ }
2886
+ async function execute(input, authored = false) {
2887
+ assertOpen();
2888
+ if (inFlight) throw new Error("A gameplay interaction is already pending.");
2889
+ if (checkpoint.terminal)
2890
+ return { accepted: false, errorCode: "game-ended" };
2891
+ inFlight = true;
2892
+ const generation = epoch;
2893
+ try {
2894
+ const result = await (authored ? runtime.dispatch : runtime.dispatchClient)({
2895
+ state: checkpoint.state,
2896
+ input,
2897
+ referenceBasis: { sessionId: session.sessionId, version: revision }
1055
2898
  });
1056
- return () => {
1057
- onMessage = null;
1058
- };
1059
- },
1060
- send(message) {
1061
- handleClientPayload(message);
2899
+ if (disposed || generation !== epoch)
2900
+ throw new Error("Local source lifetime changed.");
2901
+ if (result.kind === "reject")
2902
+ return {
2903
+ accepted: false,
2904
+ errorCode: result.errorCode,
2905
+ ...result.message === void 0 ? {} : { message: result.message }
2906
+ };
2907
+ checkpoint = immutableCopy({
2908
+ state: result.state,
2909
+ terminal: result.terminal ?? null
2910
+ });
2911
+ revision++;
2912
+ publish();
2913
+ return { accepted: true };
2914
+ } finally {
2915
+ if (generation === epoch) inFlight = false;
1062
2916
  }
1063
- };
1064
- return {
1065
- transport,
1066
- tape: options.tape,
1067
- reset() {
1068
- eventSequence = 0;
1069
- envelopeSequence = 0;
1070
- stepCursor = 0;
1071
- currentFrameId = null;
1072
- firstFixtureError = null;
1073
- pendingHandlers.clear();
1074
- events.length = 0;
1075
- if (onMessage) {
1076
- sendInit();
1077
- drainOneHostFrame();
2917
+ }
2918
+ function replaceLifetime() {
2919
+ if (!playerIds.includes(playerId))
2920
+ throw new Error("Unknown local source seat.");
2921
+ epoch++;
2922
+ inFlight = false;
2923
+ disconnect();
2924
+ lifecycle?.source.dispose();
2925
+ const generation = epoch;
2926
+ lifecycle = createSourceLifecycle({
2927
+ context: { sessionId: session.sessionId, playerId },
2928
+ send(command) {
2929
+ const current = lifecycle;
2930
+ void execute(
2931
+ command.type === "interaction.cancel" ? {
2932
+ kind: "interaction.cancel",
2933
+ basis: command.basis,
2934
+ playerId,
2935
+ interactionId: command.interactionId
2936
+ } : {
2937
+ kind: "interaction",
2938
+ basis: command.basis,
2939
+ playerId,
2940
+ interactionId: command.interactionId,
2941
+ params: command.params
2942
+ }
2943
+ ).then((result) => {
2944
+ if (generation === epoch)
2945
+ current.result({
2946
+ ...result,
2947
+ type: "interaction.result",
2948
+ clientActionId: command.clientActionId
2949
+ });
2950
+ }).catch((error) => {
2951
+ if (generation === epoch)
2952
+ current.fail(
2953
+ error instanceof Error ? error : new Error(String(error))
2954
+ );
2955
+ });
2956
+ },
2957
+ recover: publish,
2958
+ close() {
1078
2959
  }
2960
+ });
2961
+ lifecycle.session(session);
2962
+ publish();
2963
+ store.setState(() => lifecycle.source.store.get());
2964
+ const subscription = lifecycle.source.store.subscribe(
2965
+ () => store.setState(() => lifecycle.source.store.get())
2966
+ );
2967
+ disconnect = () => subscription.unsubscribe();
2968
+ }
2969
+ replaceLifetime();
2970
+ return {
2971
+ store: {
2972
+ get: () => store.get(),
2973
+ subscribe: (listener) => store.subscribe(listener)
1079
2974
  },
1080
- async flush() {
1081
- await waitForConfiguredLatency();
1082
- await Promise.resolve();
1083
- while (pendingHandlers.size > 0) {
1084
- await Promise.allSettled([...pendingHandlers]);
1085
- }
1086
- throwFixtureError();
2975
+ async submit(interactionId, params) {
2976
+ assertIdle();
2977
+ return lifecycle.source.submit(interactionId, params);
1087
2978
  },
1088
- async advanceHost() {
1089
- await waitForConfiguredLatency();
1090
- drainHostFrames();
1091
- throwFixtureError();
2979
+ async cancel(interactionId) {
2980
+ assertIdle();
2981
+ return lifecycle.source.cancel(interactionId);
1092
2982
  },
1093
- getCurrentFrameId() {
1094
- if (!currentFrameId) {
1095
- throw createFixtureError(
1096
- "fixture-frame-not-started",
1097
- "Fixture host has not emitted a gameplay frame."
1098
- );
1099
- }
1100
- return currentFrameId;
2983
+ async apply(command) {
2984
+ assertIdle();
2985
+ const actor = resolveScenarioSeatRef({
2986
+ ref: command.actor,
2987
+ playerIds,
2988
+ path: "command.actor"
2989
+ });
2990
+ const currentSchema = runtime.currentAuthorParamSchema({
2991
+ referenceBasis: { sessionId: session.sessionId, version: revision },
2992
+ state: checkpoint.state,
2993
+ playerId: actor,
2994
+ interactionId: command.interactionId
2995
+ });
2996
+ const params = resolveScenarioCommandParams({
2997
+ game,
2998
+ currentSchema,
2999
+ phase: checkpoint.state.domain.flow.currentPhase,
3000
+ interactionId: command.interactionId,
3001
+ params: command.params,
3002
+ playerIds,
3003
+ path: "command"
3004
+ });
3005
+ return execute(
3006
+ {
3007
+ kind: "interaction",
3008
+ basis: makeFrame(checkpoint.state, actor).basis,
3009
+ playerId: actor,
3010
+ interactionId: command.interactionId,
3011
+ params
3012
+ },
3013
+ true
3014
+ );
1101
3015
  },
1102
- getEvents() {
1103
- return events.map((event) => ({ ...event }));
3016
+ switchSeat(next) {
3017
+ assertOpen();
3018
+ if (!playerIds.includes(next))
3019
+ throw new Error("Unknown local source seat.");
3020
+ if (next === playerId) return;
3021
+ playerId = next;
3022
+ replaceLifetime();
1104
3023
  },
1105
- assertConsumed() {
1106
- throwFixtureError();
1107
- if (stepCursor !== options.tape.steps.length) {
1108
- throw createFixtureError(
1109
- "fixture-unconsumed-protocol-steps",
1110
- `Plugin protocol tape consumed ${stepCursor} of ${options.tape.steps.length} protocol step(s).`
1111
- );
3024
+ checkpoint() {
3025
+ assertOpen();
3026
+ return structuredClone(checkpoint);
3027
+ },
3028
+ restore(value) {
3029
+ assertOpen();
3030
+ const next = checkpointSchema.parse(value);
3031
+ runtime.project({
3032
+ state: next.state,
3033
+ playerIds: [...playerIds],
3034
+ referenceBasis: { sessionId: session.sessionId, version: revision + 1 }
3035
+ });
3036
+ const restoredRoster = next.state.domain.table.playerOrder;
3037
+ if (restoredRoster.length !== playerIds.length || restoredRoster.some((id, index) => id !== playerIds[index]))
3038
+ throw new Error("Checkpoint roster does not match the local session.");
3039
+ checkpoint = immutableCopy(next);
3040
+ revision++;
3041
+ replaceLifetime();
3042
+ },
3043
+ inspect() {
3044
+ assertOpen();
3045
+ return store.get().snapshot;
3046
+ },
3047
+ async explore({ maxEvaluations }) {
3048
+ assertOpen();
3049
+ if (!Number.isSafeInteger(maxEvaluations) || maxEvaluations < 1)
3050
+ throw new Error("Enumeration budget must be a positive integer.");
3051
+ if (checkpoint.terminal) return [];
3052
+ const commands = [];
3053
+ let remaining = maxEvaluations;
3054
+ const capturedState = checkpoint.state;
3055
+ const capturedEpoch = epoch;
3056
+ const capturedRevision = revision;
3057
+ for (const descriptor of store.get().snapshot.frame.availableInteractions) {
3058
+ if (remaining <= 0) break;
3059
+ const result = runtime.enumerateInteractionParams({
3060
+ referenceBasis: { sessionId: session.sessionId, version: revision },
3061
+ state: checkpoint.state,
3062
+ playerId,
3063
+ interactionId: descriptor.interactionId,
3064
+ maxEvaluations: remaining
3065
+ });
3066
+ if (!result.found || !result.visible || !result.enumeration) continue;
3067
+ remaining -= result.enumeration.evaluated;
3068
+ for (const params of result.enumeration.assignments) {
3069
+ const probe = await runtime.dispatch({
3070
+ state: structuredClone(capturedState),
3071
+ referenceBasis: {
3072
+ sessionId: session.sessionId,
3073
+ version: capturedRevision
3074
+ },
3075
+ input: {
3076
+ kind: "interaction",
3077
+ basis: makeFrame(capturedState).basis,
3078
+ playerId,
3079
+ interactionId: descriptor.interactionId,
3080
+ params
3081
+ }
3082
+ });
3083
+ if (disposed || capturedEpoch !== epoch || capturedRevision !== revision)
3084
+ throw new Error("Source changed during exploration.");
3085
+ if (probe.kind === "reject") continue;
3086
+ const projected = projectScenarioCommandParams({
3087
+ game,
3088
+ currentSchema: runtime.currentAuthorParamSchema({
3089
+ referenceBasis: {
3090
+ sessionId: session.sessionId,
3091
+ version: revision
3092
+ },
3093
+ state: checkpoint.state,
3094
+ playerId,
3095
+ interactionId: descriptor.interactionId
3096
+ }),
3097
+ phase: checkpoint.state.domain.flow.currentPhase,
3098
+ interactionId: descriptor.interactionId,
3099
+ params,
3100
+ playerIds,
3101
+ path: "explore"
3102
+ });
3103
+ commands.push({
3104
+ actor: { seat: playerIds.indexOf(playerId) },
3105
+ interactionId: descriptor.interactionId,
3106
+ params: projected
3107
+ });
3108
+ }
1112
3109
  }
3110
+ return commands;
3111
+ },
3112
+ dispose() {
3113
+ if (disposed) return;
3114
+ disposed = true;
3115
+ epoch++;
3116
+ lifecycle.source.dispose();
3117
+ disconnect();
1113
3118
  }
1114
3119
  };
1115
3120
  }
1116
3121
 
1117
- // src/testing/ui-fixture/FixturePluginRuntime.tsx
1118
- import { useMemo } from "react";
1119
- import { jsx } from "react/jsx-runtime";
1120
- function FixturePluginRuntime({
1121
- harness,
1122
- runtime,
1123
- children
1124
- }) {
1125
- const fixtureRuntime = useMemo(
1126
- () => runtime ?? createPluginRuntimeClient({ transport: harness.transport }),
1127
- [harness.transport, runtime]
3122
+ // src/testing/sources/scenario-source.ts
3123
+ async function scenarioSource(game, scenario, options = {}) {
3124
+ const materialized = await materializeScenarioRuntimeCheckpoint({
3125
+ game,
3126
+ scenario,
3127
+ ...options.at === void 0 ? {} : { at: resolveScenarioCheckpoint(scenario, options.at) }
3128
+ });
3129
+ return createLocalProvider(
3130
+ game,
3131
+ createReducerTestingRuntime(game),
3132
+ materialized.playerIds,
3133
+ { state: materialized.state, terminal: materialized.terminal },
3134
+ options.as ?? materialized.playerIds[0]
1128
3135
  );
1129
- return /* @__PURE__ */ jsx(PluginRuntimeBoundary, { runtime: fixtureRuntime, children });
1130
3136
  }
1131
3137
 
1132
- // src/testing/ui-fixture/compile-plugin-protocol-tape.ts
1133
- function compilePluginProtocolTape(options) {
1134
- const frames = options.trace.frames.map(
1135
- (frame) => materializeTraceFrame(frame, options.trace.viewer.playerId)
1136
- );
1137
- const frameById = new Map(frames.map((frame) => [frame.id, frame]));
1138
- const steps = [];
1139
- const firstFrame = frames[0];
1140
- if (!firstFrame) {
1141
- throw new Error(
1142
- `Reducer scenario '${options.trace.scenarioId}' did not record frames.`
1143
- );
1144
- }
1145
- steps.push({
1146
- id: `${firstFrame.id}.host-frame`,
1147
- kind: "host.frame",
1148
- frameId: firstFrame.id
1149
- });
1150
- for (const exchange of options.trace.exchanges) {
1151
- const fromFrame = frameById.get(exchange.fromFrameId);
1152
- if (!fromFrame) {
1153
- throw new Error(
1154
- `Reducer exchange '${exchange.id}' references missing frame '${exchange.fromFrameId}'.`
1155
- );
1156
- }
1157
- if (exchange.operation === "validate") {
1158
- continue;
1159
- }
1160
- steps.push({
1161
- id: exchange.id,
1162
- kind: "client.submit",
1163
- fromFrameId: exchange.fromFrameId,
1164
- requestDigest: digestRuntimeCommand(exchange, fromFrame),
1165
- response: exchange.result.kind === "accepted" ? {
1166
- type: "interaction.result",
1167
- clientActionId: exchange.id,
1168
- accepted: true
1169
- } : {
1170
- type: "interaction.result",
1171
- clientActionId: exchange.id,
1172
- accepted: false,
1173
- errorCode: exchange.result.diagnostics[0]?.code ?? "fixture-rejected",
1174
- message: exchange.result.diagnostics[0]?.message
1175
- }
1176
- });
1177
- if (exchange.result.kind === "accepted") {
1178
- if (!frameById.has(exchange.result.toFrameId)) {
1179
- throw new Error(
1180
- `Reducer exchange '${exchange.id}' references missing toFrameId '${exchange.result.toFrameId}'.`
1181
- );
3138
+ // src/testing/sources/fuzz.ts
3139
+ async function fuzz(game, options) {
3140
+ if (!Number.isSafeInteger(options.steps) || options.steps < 0)
3141
+ throw new Error("Fuzz steps must be a nonnegative integer.");
3142
+ const players = options.players ?? game.contract.manifest.normalSetup.minPlayers;
3143
+ const source = await localSource(game, { players, seed: options.seed });
3144
+ let rng = {
3145
+ seed: options.seed,
3146
+ cursor: 0,
3147
+ draws: [],
3148
+ trace: []
3149
+ };
3150
+ const commands = [];
3151
+ try {
3152
+ for (let step = 0; step < options.steps && !source.checkpoint().terminal; step++) {
3153
+ const actors = [];
3154
+ for (const player of source.inspect().players) {
3155
+ source.switchSeat(player.playerId);
3156
+ if (source.inspect().frame.availableInteractions.some(
3157
+ (interaction) => interaction.availability.status === "available"
3158
+ ))
3159
+ actors.push(player.playerId);
1182
3160
  }
1183
- steps.push({
1184
- id: `${exchange.id}.host-frame`,
1185
- kind: "host.frame",
1186
- frameId: exchange.result.toFrameId
3161
+ if (actors.length === 0) break;
3162
+ const [actorIndex, actorRng] = nextRandomInt(actors.length, rng);
3163
+ rng = actorRng;
3164
+ source.switchSeat(actors[actorIndex]);
3165
+ const choices = await source.explore({
3166
+ maxEvaluations: options.maxEvaluations ?? 5e3
1187
3167
  });
3168
+ if (choices.length === 0) break;
3169
+ const [index, next] = nextRandomInt(choices.length, rng);
3170
+ rng = next;
3171
+ const command = choices[index];
3172
+ commands.push(command);
3173
+ const result = await source.apply(command);
3174
+ if (!result.accepted)
3175
+ throw new Error(`Explored command rejected: ${result.errorCode}`);
1188
3176
  }
3177
+ return { commands, checkpoint: source.checkpoint() };
3178
+ } catch (error) {
3179
+ return {
3180
+ commands,
3181
+ checkpoint: source.checkpoint(),
3182
+ error: error instanceof Error ? error : new Error(String(error))
3183
+ };
3184
+ } finally {
3185
+ source.dispose();
1189
3186
  }
1190
- return {
1191
- session: options.session,
1192
- frames,
1193
- steps
1194
- };
1195
- }
1196
- function materializeTraceFrame(frame, perspectivePlayerId) {
1197
- const flow = readFlowState2(frame.reducerState);
1198
- return {
1199
- id: frame.id,
1200
- frame: materializePluginGameplayFrame({
1201
- currentPhase: flow.currentPhase,
1202
- activePlayers: flow.activePlayers,
1203
- dynamicProjection: frame.dynamicProjection,
1204
- staticProjection: frame.staticProjection,
1205
- perspectivePlayerId,
1206
- version: frame.gameVersion,
1207
- actionSetVersion: frame.actionSetVersion
1208
- }),
1209
- projectionDigest: frame.projectionDigest
1210
- };
1211
- }
1212
- function digestRuntimeCommand(exchange, frame) {
1213
- return digestUIFixtureTransportRequest({
1214
- operation: exchange.operation === "validate" ? "validate" : "submit",
1215
- basis: frame.frame.basis,
1216
- interactionId: exchange.input.interactionId,
1217
- payload: exchange.input.params
1218
- });
1219
- }
1220
- function readFlowState2(state) {
1221
- return {
1222
- currentPhase: state.domain?.flow?.currentPhase ?? null,
1223
- activePlayers: [...state.domain?.flow?.activePlayers ?? []]
1224
- };
1225
- }
1226
-
1227
- // src/testing/ui-fixture/deterministic-environment.ts
1228
- var defaultFixtureEnvironmentInit = {
1229
- clockIso: "2026-01-01T00:00:00.000Z",
1230
- randomSeed: "dreamboard-ui-fixture",
1231
- locale: "en-US",
1232
- timezone: "UTC",
1233
- reducedMotion: true,
1234
- network: "blocked"
1235
- };
1236
- function createDeterministicIdFactory(seed) {
1237
- let cursor = 0;
1238
- return () => {
1239
- cursor += 1;
1240
- return `fixture-${slug(seed)}-${cursor.toString(36).padStart(4, "0")}`;
1241
- };
1242
- }
1243
- function fixtureEnvironmentInitFor(options) {
1244
- return {
1245
- ...defaultFixtureEnvironmentInit,
1246
- clockIso: options.clockIso,
1247
- randomSeed: options.randomSeed
1248
- };
1249
- }
1250
- function slug(value) {
1251
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 48) || "seed";
1252
3187
  }
1253
3188
 
1254
- // src/testing/ui-fixture/schema.ts
1255
- import { z } from "zod";
1256
- var UI_SCENARIO_FIXTURE_SCHEMA_VERSION = 2;
1257
- var UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION = 2;
1258
- var UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR = 3;
1259
- var UI_SCENARIO_FIXTURE_PLUGIN_RUNTIME_PROTOCOL = DREAMBOARD_PLUGIN_PROTOCOL_VERSION;
1260
- var digestSchema = z.string().refine(isSha256Digest, {
1261
- message: "Expected a sha256:<64 lowercase hex> digest."
1262
- });
1263
- var nonEmptyStringSchema = z.string().min(1);
1264
- var jsonSchema = RuntimeJsonSchema;
1265
- var environmentSchema = z.object({
1266
- clockIso: nonEmptyStringSchema,
1267
- randomSeed: nonEmptyStringSchema,
1268
- locale: z.literal("en-US"),
1269
- timezone: z.literal("UTC"),
1270
- viewportTags: z.array(z.enum(["desktop", "tablet", "phone", "touch"]))
1271
- }).strict();
1272
- var uiFixtureFrameSchema = PluginProtocolFrameSchema;
1273
- var uiFixtureProtocolStepSchema = PluginProtocolStepSchema;
1274
- var pluginProtocolTapeSchema = PluginProtocolTapeSchema.refine(
1275
- (tape) => tape.frames.length > 0,
1276
- "UI fixture protocol tapes require at least one gameplay frame."
1277
- );
1278
- var browserInteractionIntentRequestSchema = z.object({
1279
- surface: nonEmptyStringSchema,
1280
- scopeId: nonEmptyStringSchema.optional(),
1281
- interactionKey: nonEmptyStringSchema.optional(),
1282
- interactionId: nonEmptyStringSchema.optional(),
1283
- intent: nonEmptyStringSchema,
1284
- inputKey: nonEmptyStringSchema.optional(),
1285
- candidateValue: jsonSchema.optional(),
1286
- candidateValueKey: nonEmptyStringSchema.optional(),
1287
- actuatorKind: z.enum(BROWSER_INTERACTION_ACTUATOR_KINDS).optional(),
1288
- allowDisabled: z.boolean().optional()
1289
- }).strict();
1290
- var browserInteractionEffectRequestSchema = z.object({
1291
- surface: nonEmptyStringSchema,
1292
- scopeId: nonEmptyStringSchema.optional(),
1293
- interactionKey: nonEmptyStringSchema.optional(),
1294
- interactionId: nonEmptyStringSchema.optional(),
1295
- effect: browserInteractionEffectSchema,
1296
- allowDisabled: z.boolean().optional()
1297
- }).strict();
1298
- var uiReplayRequestSchema = z.union([
1299
- browserInteractionIntentRequestSchema,
1300
- browserInteractionEffectRequestSchema
1301
- ]);
1302
- var uiReplayExecutionSchema = z.union([
1303
- z.object({
1304
- kind: z.literal("activate")
1305
- }).strict(),
1306
- z.object({
1307
- kind: z.literal("fill"),
1308
- value: z.string()
1309
- }).strict(),
1310
- z.object({
1311
- kind: z.literal("drag"),
1312
- target: browserInteractionEffectRequestSchema
1313
- }).strict()
1314
- ]);
1315
- var uiStepExpectationSchema = z.object({
1316
- frameId: nonEmptyStringSchema.optional(),
1317
- projectionDigest: digestSchema.optional(),
1318
- semanticDigest: digestSchema.optional(),
1319
- draftDigest: digestSchema.optional(),
1320
- submissionDigest: digestSchema.optional(),
1321
- focusedInteractionKey: nonEmptyStringSchema.optional(),
1322
- visibleInteractionKeys: z.array(nonEmptyStringSchema).optional()
1323
- }).strict();
1324
- var uiResolvedReplayIdentitySchema = z.object({
1325
- stepId: nonEmptyStringSchema,
1326
- surface: nonEmptyStringSchema,
1327
- scopeId: nonEmptyStringSchema,
1328
- interactionKey: nonEmptyStringSchema,
1329
- interactionId: nonEmptyStringSchema,
1330
- actuatorId: nonEmptyStringSchema,
1331
- descriptorDigest: digestSchema.optional(),
1332
- draftDigest: digestSchema.optional()
1333
- }).strict();
1334
- var portableSemanticReplayStepSchema = z.object({
1335
- stepId: nonEmptyStringSchema,
1336
- requestDigest: digestSchema,
1337
- resolve: uiReplayRequestSchema,
1338
- execute: uiReplayExecutionSchema,
1339
- expectedIdentity: uiResolvedReplayIdentitySchema.optional(),
1340
- expect: uiStepExpectationSchema
1341
- }).strict();
1342
- var uiScenarioReplayStepSchema = z.union([
1343
- portableSemanticReplayStepSchema,
1344
- z.object({
1345
- stepId: nonEmptyStringSchema,
1346
- kind: z.literal("assert"),
1347
- expect: uiStepExpectationSchema
1348
- }).strict()
3189
+ // src/testing/candidate-verification.ts
3190
+ var CANDIDATE_INPUT_FIELDS = /* @__PURE__ */ new Set([
3191
+ "reducer",
3192
+ "bundle",
3193
+ "scenarios",
3194
+ "maxScenarios",
3195
+ "maxStepsPerScenario"
1349
3196
  ]);
1350
- var uiScenarioFixtureSchema = z.object({
1351
- schemaVersion: z.literal(UI_SCENARIO_FIXTURE_SCHEMA_VERSION),
1352
- browserInteractionProtocol: z.literal(
1353
- DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION
1354
- ),
1355
- pluginRuntimeProtocol: z.literal(DREAMBOARD_PLUGIN_PROTOCOL_VERSION),
1356
- id: nonEmptyStringSchema,
1357
- title: nonEmptyStringSchema,
1358
- gameId: nonEmptyStringSchema,
1359
- tags: z.array(nonEmptyStringSchema),
1360
- source: z.object({
1361
- scenarioId: nonEmptyStringSchema,
1362
- reducerFingerprint: nonEmptyStringSchema,
1363
- uiContractFingerprint: nonEmptyStringSchema,
1364
- renderModule: nonEmptyStringSchema,
1365
- renderModuleDigest: digestSchema,
1366
- sourceDigest: digestSchema,
1367
- sourceFiles: z.array(nonEmptyStringSchema).optional()
1368
- }).strict(),
1369
- viewer: z.object({
1370
- seatId: nonEmptyStringSchema,
1371
- playerId: nonEmptyStringSchema.optional()
1372
- }).strict(),
1373
- environment: environmentSchema,
1374
- protocol: pluginProtocolTapeSchema,
1375
- replay: z.array(uiScenarioReplayStepSchema),
1376
- expected: z.object({
1377
- initialProjectionDigest: digestSchema,
1378
- finalProjectionDigest: digestSchema,
1379
- finalSemanticDigest: digestSchema,
1380
- submissionDigest: digestSchema
1381
- }).strict()
1382
- }).strict();
1383
- var uiScenarioFixtureBundleIndexSchema = z.object({
1384
- schemaVersion: z.literal(UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION),
1385
- bundleId: nonEmptyStringSchema,
1386
- sdkCommit: nonEmptyStringSchema,
1387
- pluginRuntimeProtocol: z.literal(DREAMBOARD_PLUGIN_PROTOCOL_VERSION),
1388
- browserInteractionProtocol: z.literal(
1389
- DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION
1390
- ),
1391
- fixtures: z.array(
1392
- z.object({
1393
- id: nonEmptyStringSchema,
1394
- file: nonEmptyStringSchema,
1395
- sha256: digestSchema,
1396
- renderModule: nonEmptyStringSchema,
1397
- renderModuleSha256: digestSchema,
1398
- components: z.array(nonEmptyStringSchema),
1399
- capabilities: z.array(nonEmptyStringSchema)
1400
- }).strict()
1401
- )
1402
- }).strict();
1403
- function parseUIScenarioFixture(value) {
1404
- const fixture = uiScenarioFixtureSchema.parse(
1405
- value
1406
- );
1407
- validateUIScenarioFixtureReferences(fixture);
1408
- return fixture;
1409
- }
1410
- function parseUIScenarioFixtureBundleIndex(value) {
1411
- const bundle = uiScenarioFixtureBundleIndexSchema.parse(value);
1412
- assertSupportedBrowserInteractionProtocol(bundle.browserInteractionProtocol);
1413
- const ids = /* @__PURE__ */ new Set();
1414
- for (const entry of bundle.fixtures) {
1415
- if (ids.has(entry.id)) {
1416
- throw new Error(`Duplicate fixture id '${entry.id}' in bundle index.`);
1417
- }
1418
- ids.add(entry.id);
3197
+ var MAX_DIAGNOSTIC_MESSAGE_LENGTH = 2e3;
3198
+ var MAX_DIAGNOSTIC_TRACE_ENTRIES = 100;
3199
+ async function runCandidateVerification(input) {
3200
+ assertCandidateInputFields(input);
3201
+ assertReducerBundleContract(input.bundle, "candidate verification");
3202
+ const scenarios = normalizeScenarios(input.scenarios);
3203
+ if (scenarios.length === 0) {
3204
+ throw new Error("Candidate verification requires at least one scenario.");
1419
3205
  }
1420
- return bundle;
1421
- }
1422
- function assertSupportedBrowserInteractionProtocol(version) {
1423
- const major = Number(version.split(".")[0]);
1424
- if (major !== UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR) {
3206
+ const maxScenarios = input.maxScenarios ?? scenarios.length;
3207
+ assertPositiveInteger(maxScenarios, "maxScenarios");
3208
+ if (scenarios.length > maxScenarios) {
1425
3209
  throw new Error(
1426
- `Unsupported browser-interaction protocol '${version}'. Expected major ${UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR}.`
3210
+ `Candidate verification contains ${scenarios.length} scenarios, exceeding limit ${maxScenarios}.`
1427
3211
  );
1428
3212
  }
1429
- }
1430
- function validateUIScenarioFixtureReferences(fixture) {
1431
- assertSupportedBrowserInteractionProtocol(fixture.browserInteractionProtocol);
1432
- const frameIds = /* @__PURE__ */ new Set();
1433
- const stepIds = /* @__PURE__ */ new Set();
1434
- for (const frame of fixture.protocol.frames) {
1435
- if (frameIds.has(frame.id)) {
1436
- throw new Error(`Duplicate fixture frame id '${frame.id}'.`);
1437
- }
1438
- frameIds.add(frame.id);
3213
+ const maxStepsPerScenario = input.maxStepsPerScenario ?? 1e3;
3214
+ assertPositiveInteger(maxStepsPerScenario, "maxStepsPerScenario");
3215
+ const results = [];
3216
+ for (const scenario of scenarios) {
3217
+ results.push(
3218
+ await runScenario({
3219
+ reducer: input.reducer,
3220
+ bundle: input.bundle,
3221
+ scenario,
3222
+ maxStepsPerScenario
3223
+ })
3224
+ );
1439
3225
  }
1440
- for (const step of fixture.protocol.steps) {
1441
- if (stepIds.has(step.id)) {
1442
- throw new Error(`Duplicate protocol step id '${step.id}'.`);
1443
- }
1444
- stepIds.add(step.id);
1445
- if (step.kind === "host.frame") {
1446
- if (!frameIds.has(step.frameId)) {
1447
- throw new Error(
1448
- `Protocol step '${step.id}' references missing frameId '${step.frameId}'.`
1449
- );
1450
- }
1451
- continue;
1452
- }
1453
- if (!frameIds.has(step.fromFrameId)) {
1454
- throw new Error(
1455
- `Protocol step '${step.id}' references missing fromFrameId '${step.fromFrameId}'.`
1456
- );
3226
+ const failed = results.filter((result) => result.status === "failed").length;
3227
+ return {
3228
+ status: failed === 0 ? "passed" : "failed",
3229
+ scenarioSummary: {
3230
+ total: results.length,
3231
+ passed: results.length - failed,
3232
+ failed,
3233
+ scenarios: results
1457
3234
  }
1458
- }
1459
- for (const step of fixture.replay) {
1460
- if ("resolve" in step) {
1461
- const actualDigest = digestUIFixtureRequest(
1462
- step.resolve
1463
- );
1464
- if (step.requestDigest !== actualDigest) {
1465
- throw new Error(
1466
- `Replay step '${step.stepId}' requestDigest ${step.requestDigest} does not match ${actualDigest}.`
1467
- );
3235
+ };
3236
+ }
3237
+ async function runScenario(input) {
3238
+ const actualSteps = input.scenario.given.length + input.scenario.when.length;
3239
+ if (actualSteps > input.maxStepsPerScenario) {
3240
+ return {
3241
+ id: input.scenario.id,
3242
+ status: "failed",
3243
+ diagnostic: {
3244
+ kind: "limit",
3245
+ message: `Scenario contains ${actualSteps} replay steps, exceeding maxStepsPerScenario limit ${input.maxStepsPerScenario}.`,
3246
+ actualSteps,
3247
+ maxStepsPerScenario: input.maxStepsPerScenario
1468
3248
  }
1469
- }
1470
- const expectedFrameId = step.expect.frameId;
1471
- if (expectedFrameId && !frameIds.has(expectedFrameId)) {
1472
- throw new Error(
1473
- `Replay step '${step.stepId}' expects missing frameId '${expectedFrameId}'.`
1474
- );
1475
- }
3249
+ };
1476
3250
  }
1477
- }
1478
-
1479
- // src/testing/ui-fixture/compiler.ts
1480
- function compileUIScenarioFixture(options) {
1481
- const firstFrame = options.protocol.frames[0];
1482
- const finalFrame = lastPublishedFrame(options.protocol);
1483
- if (!firstFrame || !finalFrame) {
1484
- throw new Error(
1485
- "compileUIScenarioFixture requires at least one protocol frame."
1486
- );
3251
+ let replay;
3252
+ try {
3253
+ replay = await replayScenario({
3254
+ game: input.reducer,
3255
+ bundle: input.bundle,
3256
+ scenario: input.scenario
3257
+ });
3258
+ } catch (error) {
3259
+ return {
3260
+ id: input.scenario.id,
3261
+ status: "failed",
3262
+ diagnostic: replayDiagnostic(error)
3263
+ };
1487
3264
  }
1488
- const replay = options.replay ?? [];
1489
- const submissionDigest = options.expected?.submissionDigest ?? digestUIFixtureJson({
1490
- digestVersion: "ui-submission@1",
1491
- fixtureId: options.id,
1492
- replay: replay.map(
1493
- (step) => "requestDigest" in step ? step.requestDigest : step.stepId
1494
- )
1495
- });
1496
- const finalSemanticDigest = options.expected?.finalSemanticDigest ?? digestUIFixtureJson({
1497
- digestVersion: "ui-semantic@1",
1498
- fixtureId: options.id,
1499
- frameId: finalFrame.id,
1500
- projectionDigest: finalFrame.projectionDigest
1501
- });
1502
- return parseUIScenarioFixture(
1503
- canonicalizeUIScenarioFixture({
1504
- schemaVersion: 2,
1505
- browserInteractionProtocol: DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
1506
- pluginRuntimeProtocol: DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
1507
- id: options.id,
1508
- title: options.title,
1509
- gameId: options.gameId,
1510
- tags: [...options.tags ?? []],
1511
- source: options.source,
1512
- viewer: options.viewer,
1513
- environment: options.environment,
1514
- protocol: options.protocol,
1515
- replay,
1516
- expected: {
1517
- initialProjectionDigest: options.expected?.initialProjectionDigest ?? firstFrame.projectionDigest,
1518
- finalProjectionDigest: options.expected?.finalProjectionDigest ?? finalFrame.projectionDigest,
1519
- finalSemanticDigest,
1520
- submissionDigest
3265
+ try {
3266
+ await assertScenario({ replay, assertion: input.scenario.then });
3267
+ return { id: input.scenario.id, status: "passed" };
3268
+ } catch (error) {
3269
+ return {
3270
+ id: input.scenario.id,
3271
+ status: "failed",
3272
+ diagnostic: {
3273
+ kind: "assertion",
3274
+ message: boundedMessage(error)
1521
3275
  }
1522
- })
1523
- );
3276
+ };
3277
+ }
1524
3278
  }
1525
- function lastPublishedFrame(protocol) {
1526
- const frameById = new Map(protocol.frames.map((frame) => [frame.id, frame]));
1527
- for (let index = protocol.steps.length - 1; index >= 0; index -= 1) {
1528
- const step = protocol.steps[index];
1529
- if (step?.kind === "host.frame") {
1530
- return frameById.get(step.frameId);
3279
+ function assertCandidateInputFields(input) {
3280
+ for (const field of Object.keys(input)) {
3281
+ if (!CANDIDATE_INPUT_FIELDS.has(field)) {
3282
+ throw new Error(
3283
+ `Candidate verification input contains unsupported field '${field}'.`
3284
+ );
1531
3285
  }
1532
3286
  }
1533
- return protocol.frames[protocol.frames.length - 1];
1534
3287
  }
1535
- function assertDeterministicUIScenarioFixture(first, second) {
1536
- const firstBytes = serializeUIScenarioFixture(first);
1537
- const secondBytes = serializeUIScenarioFixture(second);
1538
- if (firstBytes !== secondBytes) {
1539
- throw new Error(
1540
- `UI scenario fixture '${first.id}' is non-deterministic across compilation runs.`
1541
- );
3288
+ function assertPositiveInteger(value, field) {
3289
+ if (!Number.isSafeInteger(value) || value < 1) {
3290
+ throw new Error(`${field} must be a positive safe integer.`);
1542
3291
  }
1543
3292
  }
1544
- function assertUniqueReplayIdentity(step, candidates) {
1545
- if (candidates.length !== 1) {
3293
+ function normalizeScenarios(input) {
3294
+ if (typeof input !== "object" || input === null) {
1546
3295
  throw new Error(
1547
- `Replay step '${step.stepId}' resolved ${candidates.length} identities; expected exactly one.`
3296
+ "Candidate verification scenarios must be an array or record."
1548
3297
  );
1549
3298
  }
1550
- const candidate = candidates[0];
1551
- if (!candidate) {
1552
- throw new Error(`Replay step '${step.stepId}' did not resolve.`);
1553
- }
1554
- const expected = step.expectedIdentity;
1555
- if (!expected) {
1556
- return candidate;
1557
- }
1558
- for (const key of [
1559
- "surface",
1560
- "scopeId",
1561
- "interactionKey",
1562
- "interactionId",
1563
- "actuatorId",
1564
- "descriptorDigest",
1565
- "draftDigest"
1566
- ]) {
1567
- const expectedValue = expected[key];
1568
- if (expectedValue !== void 0 && candidate[key] !== expectedValue) {
1569
- throw new Error(
1570
- `Replay step '${step.stepId}' resolved ${key} '${String(
1571
- candidate[key]
1572
- )}', expected '${expectedValue}'.`
1573
- );
1574
- }
1575
- }
1576
- return candidate;
1577
- }
1578
- function assertUIStepExpectationSatisfied(stepId, expected, actual) {
1579
- for (const key of [
1580
- "frameId",
1581
- "projectionDigest",
1582
- "semanticDigest",
1583
- "draftDigest",
1584
- "submissionDigest",
1585
- "focusedInteractionKey"
1586
- ]) {
1587
- if (expected[key] !== void 0 && expected[key] !== actual[key]) {
1588
- throw new Error(
1589
- `Replay step '${stepId}' expected ${key} '${expected[key]}', received '${actual[key]}'.`
1590
- );
3299
+ const scenarios = Object.values(input);
3300
+ const seenIds = /* @__PURE__ */ new Set();
3301
+ for (const scenario of scenarios) {
3302
+ if (typeof scenario?.id !== "string" || scenario.id.length === 0) {
3303
+ throw new Error("Candidate verification scenario id is required.");
1591
3304
  }
1592
- }
1593
- if (expected.visibleInteractionKeys) {
1594
- const actualKeys = new Set(actual.visibleInteractionKeys ?? []);
1595
- const missing = expected.visibleInteractionKeys.filter(
1596
- (key) => !actualKeys.has(key)
1597
- );
1598
- if (missing.length > 0) {
3305
+ if (seenIds.has(scenario.id)) {
1599
3306
  throw new Error(
1600
- `Replay step '${stepId}' missing visible interactions: ${missing.join(
1601
- ", "
1602
- )}.`
3307
+ `Candidate verification scenario id '${scenario.id}' is duplicated.`
1603
3308
  );
1604
3309
  }
3310
+ seenIds.add(scenario.id);
1605
3311
  }
3312
+ return scenarios;
1606
3313
  }
1607
-
1608
- // src/testing/ui-scenario/replay-builders.ts
1609
- function semanticStep(options, execute) {
1610
- return {
1611
- stepId: options.stepId,
1612
- requestDigest: digestUIFixtureRequest(options.resolve),
1613
- resolve: options.resolve,
1614
- execute,
1615
- expectedIdentity: options.expectedIdentity,
1616
- expect: options.expect
1617
- };
1618
- }
1619
- function activate(options) {
1620
- return semanticStep(options, { kind: "activate" });
1621
- }
1622
- function fill(options) {
1623
- return semanticStep(options, { kind: "fill", value: options.value });
1624
- }
1625
- function press(options) {
1626
- return activate(options);
1627
- }
1628
- function drag(options) {
1629
- return semanticStep(options, { kind: "drag", target: options.target });
3314
+ function replayDiagnostic(error) {
3315
+ if (error instanceof ScenarioReplayError) {
3316
+ return {
3317
+ kind: "replay",
3318
+ message: boundedMessage(error),
3319
+ scenarioId: error.scenarioId,
3320
+ segment: error.segment,
3321
+ index: error.index,
3322
+ interactionId: error.interactionId,
3323
+ errorCode: error.errorCode,
3324
+ ...error.reducerMessage === void 0 ? {} : { reducerMessage: boundedText(error.reducerMessage) },
3325
+ trace: error.trace.slice(0, MAX_DIAGNOSTIC_TRACE_ENTRIES)
3326
+ };
3327
+ }
3328
+ return { kind: "replay", message: boundedMessage(error) };
1630
3329
  }
1631
- function submit(options) {
1632
- return activate(options);
3330
+ function boundedMessage(error) {
3331
+ return boundedText(error instanceof Error ? error.message : String(error));
1633
3332
  }
1634
- function assertStep(stepId, expect) {
1635
- return {
1636
- stepId,
1637
- kind: "assert",
1638
- expect
1639
- };
3333
+ function boundedText(value) {
3334
+ return value.slice(0, MAX_DIAGNOSTIC_MESSAGE_LENGTH);
1640
3335
  }
1641
3336
  export {
1642
- FixturePluginRuntime,
1643
3337
  ScenarioAssertionError,
1644
3338
  ScenarioDefinitionValidationError,
1645
3339
  ScenarioReplayError,
1646
- StaleContractArtifactError,
1647
- UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION,
1648
- UI_SCENARIO_FIXTURE_PLUGIN_RUNTIME_PROTOCOL,
1649
- UI_SCENARIO_FIXTURE_SCHEMA_VERSION,
1650
- UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR,
1651
- activate,
1652
- assertDeterministicUIScenarioFixture,
1653
3340
  assertScenario,
1654
- assertStep,
1655
- assertSupportedBrowserInteractionProtocol,
1656
- assertUIStepExpectationSatisfied,
1657
- assertUniqueReplayIdentity,
1658
- canonicalUIFixtureJson,
1659
- canonicalizeUIFixtureJson,
1660
- canonicalizeUIScenarioFixture,
1661
- compilePluginProtocolTape,
1662
- compileUIScenarioFixture,
1663
- createDeterministicIdFactory,
1664
3341
  createExpectApi,
1665
- createFixtureHostHarness,
1666
3342
  createReducerScenarioRunner,
1667
3343
  createScenarioAuthoring,
1668
- defaultFixtureEnvironmentInit,
1669
- defineUIScenario,
1670
- digestPluginProtocolTape,
1671
- digestUIFixtureJson,
1672
- digestUIFixtureRequest,
1673
- digestUIFixtureTransportRequest,
1674
- digestUIScenarioFixture,
1675
- drag,
3344
+ createTestSource,
1676
3345
  exploreScenario,
1677
- fill,
1678
- fixtureEnvironmentInitFor,
3346
+ fuzz,
1679
3347
  inspectScenario,
1680
- isSha256Digest,
1681
- isStaleContractArtifactError,
1682
- parseUIScenarioFixture,
1683
- parseUIScenarioFixtureBundleIndex,
1684
- pluginProtocolTapeSchema,
1685
- portableSemanticReplayStepSchema,
1686
- press,
3348
+ localSource,
3349
+ materializeScenarioRuntimeCheckpoint,
1687
3350
  probeScenarioCommand,
1688
3351
  replayScenario,
1689
3352
  resolveScenarioCheckpoint,
1690
- serializeUIScenarioFixture,
1691
- submit,
1692
- toScenarioReplayDefinition,
1693
- uiFixtureFrameSchema,
1694
- uiFixtureProtocolStepSchema,
1695
- uiReplayExecutionSchema,
1696
- uiReplayRequestSchema,
1697
- uiResolvedReplayIdentitySchema,
1698
- uiScenarioFixtureBundleIndexSchema,
1699
- uiScenarioFixtureSchema,
1700
- uiScenarioReplayStepSchema,
1701
- uiStepExpectationSchema
3353
+ runCandidateVerification,
3354
+ scenarioSource,
3355
+ toScenarioReplayDefinition
1702
3356
  };
1703
3357
  //# sourceMappingURL=testing.js.map