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

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 +136 -45
  2. package/dist/chunk-DMFVAZI2.js +16 -0
  3. package/dist/chunk-DMFVAZI2.js.map +1 -0
  4. package/dist/chunk-FMM5LVJW.js +833 -0
  5. package/dist/chunk-FMM5LVJW.js.map +1 -0
  6. package/dist/{chunk-S2JEDVWH.js → chunk-LRILEVKC.js} +6113 -6489
  7. package/dist/chunk-LRILEVKC.js.map +1 -0
  8. package/dist/chunk-SP234IY4.js +1363 -0
  9. package/dist/chunk-SP234IY4.js.map +1 -0
  10. package/dist/chunk-VUWNJXLJ.js +52 -0
  11. package/dist/chunk-VUWNJXLJ.js.map +1 -0
  12. package/dist/chunk-ZPRKWPEP.js +511 -0
  13. package/dist/chunk-ZPRKWPEP.js.map +1 -0
  14. package/dist/definition-BaHNPZol.d.ts +4639 -0
  15. package/dist/diagnostics-B0lgnuZ6.d.ts +65 -0
  16. package/dist/index.d.ts +3553 -1
  17. package/dist/index.js +1045 -6
  18. package/dist/index.js.map +1 -1
  19. package/dist/model-BHro6yXu.d.ts +288 -0
  20. package/dist/react.d.ts +32 -0
  21. package/dist/react.js +80 -0
  22. package/dist/react.js.map +1 -0
  23. package/dist/reducer.d.ts +1194 -79
  24. package/dist/reducer.js +4326 -121
  25. package/dist/reducer.js.map +1 -1
  26. package/dist/testing.d.ts +518 -564
  27. package/dist/testing.js +2448 -915
  28. package/dist/testing.js.map +1 -1
  29. package/dist/types-DimqnJBq.d.ts +86 -0
  30. package/package.json +26 -131
  31. package/dist/HandView-Bi2lxzQP.d.ts +0 -79
  32. package/dist/ResourceCounter-BFxJknqp.d.ts +0 -98
  33. package/dist/ThemeProvider-CElaL2ik.d.ts +0 -99
  34. package/dist/attributes-DbvyMbXw.d.ts +0 -68
  35. package/dist/browser-interaction.d.ts +0 -910
  36. package/dist/browser-interaction.js +0 -119
  37. package/dist/browser-interaction.js.map +0 -1
  38. package/dist/chunk-32PFKDV7.js +0 -11705
  39. package/dist/chunk-32PFKDV7.js.map +0 -1
  40. package/dist/chunk-3BAD4VC2.js +0 -263
  41. package/dist/chunk-3BAD4VC2.js.map +0 -1
  42. package/dist/chunk-3SKDNDPG.js +0 -104
  43. package/dist/chunk-3SKDNDPG.js.map +0 -1
  44. package/dist/chunk-5YV2WHJ4.js +0 -25
  45. package/dist/chunk-5YV2WHJ4.js.map +0 -1
  46. package/dist/chunk-ANRB4XKA.js +0 -90
  47. package/dist/chunk-ANRB4XKA.js.map +0 -1
  48. package/dist/chunk-BI4G3PM2.js +0 -1864
  49. package/dist/chunk-BI4G3PM2.js.map +0 -1
  50. package/dist/chunk-EVCLTY4Q.js +0 -935
  51. package/dist/chunk-EVCLTY4Q.js.map +0 -1
  52. package/dist/chunk-GDPKTBUR.js +0 -564
  53. package/dist/chunk-GDPKTBUR.js.map +0 -1
  54. package/dist/chunk-H6VDGFL5.js +0 -2903
  55. package/dist/chunk-H6VDGFL5.js.map +0 -1
  56. package/dist/chunk-LR3ZTWQF.js +0 -5960
  57. package/dist/chunk-LR3ZTWQF.js.map +0 -1
  58. package/dist/chunk-PF7L4BMG.js +0 -117
  59. package/dist/chunk-PF7L4BMG.js.map +0 -1
  60. package/dist/chunk-PN5O6GG2.js +0 -32
  61. package/dist/chunk-PN5O6GG2.js.map +0 -1
  62. package/dist/chunk-PZ5AY32C.js +0 -10
  63. package/dist/chunk-PZ5AY32C.js.map +0 -1
  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,1913 @@
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-ZPRKWPEP.js";
23
7
  import {
24
- PluginRuntimeBoundary,
25
- createPluginRuntimeClient
26
- } from "./chunk-3BAD4VC2.js";
27
- import "./chunk-PN5O6GG2.js";
28
- import "./chunk-GDPKTBUR.js";
8
+ immutableCopy
9
+ } from "./chunk-DMFVAZI2.js";
29
10
  import {
30
- BROWSER_INTERACTION_ACTUATOR_KINDS,
31
- DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
32
- browserInteractionEffectSchema
33
- } from "./chunk-WHR5UW3F.js";
11
+ assertReducerBundleContract,
12
+ collectReducerDefinitionIndex,
13
+ createClientParamSchemasByPhase,
14
+ createIngressRuntimeCodec,
15
+ createInteractionResolver,
16
+ createProjectionBuilder,
17
+ createReducerBundle,
18
+ createTrustedRuntimeScope,
19
+ manifestSchemaFamily,
20
+ nextRandomInt
21
+ } from "./chunk-LRILEVKC.js";
34
22
  import {
35
- DREAMBOARD_PLUGIN_PROTOCOL,
36
- DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
37
- PluginProtocolFrameSchema,
38
- PluginProtocolStepSchema,
39
- PluginProtocolTapeSchema,
40
- computePluginActionSetVersion,
23
+ GameOutcomeSchema,
24
+ ReducerSessionStateSchema,
41
25
  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";
26
+ encodeCanonicalPluginRuntimeJson
27
+ } from "./chunk-FMM5LVJW.js";
28
+ import "./chunk-VUWNJXLJ.js";
29
+
30
+ // src/testing/scenario-definition-validation.ts
31
+ import { z as z2 } from "zod";
32
+
33
+ // src/testing/scenario-schema.ts
34
+ import { z } from "zod";
35
+ var ScenarioSchemaValueError = class extends Error {
36
+ path;
37
+ constructor(path, message) {
38
+ super(message);
39
+ this.name = "ScenarioSchemaValueError";
40
+ this.path = path;
41
+ }
42
+ };
43
+ function appendScenarioPath(path, key) {
44
+ if (typeof key === "number") {
45
+ return `${path}[${key}]`;
46
+ }
47
+ if (typeof key === "symbol") {
48
+ return `${path}[${String(key)}]`;
49
+ }
50
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
51
+ }
52
+ function isPlainObject(value) {
53
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
54
+ return false;
55
+ }
56
+ const prototype = Object.getPrototypeOf(value);
57
+ return prototype === Object.prototype || prototype === null;
58
+ }
59
+ function resolvePlayerSeatReference(options) {
60
+ if (!isPlainObject(options.value)) {
61
+ throw new ScenarioSchemaValueError(
62
+ options.path,
63
+ "expected a seat reference shaped as { seat: number }"
64
+ );
65
+ }
66
+ const keys = Object.keys(options.value);
67
+ if (keys.length !== 1 || keys[0] !== "seat") {
68
+ throw new ScenarioSchemaValueError(
69
+ options.path,
70
+ "expected a seat reference with only the 'seat' field"
71
+ );
72
+ }
73
+ const seat = options.value.seat;
74
+ const seatPath = appendScenarioPath(options.path, "seat");
75
+ if (typeof seat !== "number" || !Number.isSafeInteger(seat)) {
76
+ throw new ScenarioSchemaValueError(
77
+ seatPath,
78
+ "expected a finite safe integer"
79
+ );
80
+ }
81
+ if (seat < 0 || seat >= options.playerIds.length) {
82
+ throw new ScenarioSchemaValueError(
83
+ seatPath,
84
+ `expected a seat from 0 through ${Math.max(0, options.playerIds.length - 1)}`
85
+ );
86
+ }
87
+ const playerId = options.playerIds[seat];
88
+ if (playerId === void 0) {
89
+ throw new ScenarioSchemaValueError(
90
+ seatPath,
91
+ `could not resolve seat ${seat}`
92
+ );
93
+ }
94
+ return playerId;
95
+ }
96
+ function projectPlayerSeatReference(options) {
97
+ if (typeof options.value !== "string") {
98
+ throw new ScenarioSchemaValueError(
99
+ options.path,
100
+ "expected a runtime player id string"
101
+ );
102
+ }
103
+ const seat = options.playerIds.indexOf(options.value);
104
+ if (seat < 0) {
105
+ throw new ScenarioSchemaValueError(
106
+ options.path,
107
+ `runtime player id '${options.value}' is not assigned to a scenario seat`
108
+ );
109
+ }
110
+ return { seat };
111
+ }
112
+ function tupleDefinition(schema) {
113
+ return schema._zod.def;
114
+ }
115
+ function intersectionDefinition(schema) {
116
+ return schema._zod.def;
117
+ }
118
+ function asClassicSchema(schema) {
119
+ return schema;
120
+ }
121
+ function resolveScenarioSeatReferences(options) {
122
+ return transformScenarioSchemaValue({ ...options, direction: "resolve" });
123
+ }
124
+ function projectScenarioSeatReferences(options) {
125
+ return transformScenarioSchemaValue({ ...options, direction: "project" });
126
+ }
127
+ function transformScenarioSchemaValue(options) {
128
+ const { schema, value, playerIds, path } = options;
129
+ if (manifestSchemaFamily(schema) === "playerId") {
130
+ return options.direction === "resolve" ? resolvePlayerSeatReference({ value, playerIds, path }) : projectPlayerSeatReference({ value, playerIds, path });
131
+ }
132
+ if (schema instanceof z.ZodOptional || schema instanceof z.ZodExactOptional) {
133
+ return value === void 0 ? value : transformScenarioSchemaValue({
134
+ schema: asClassicSchema(schema.unwrap()),
135
+ value,
136
+ playerIds,
137
+ path,
138
+ direction: options.direction
139
+ });
140
+ }
141
+ if (schema instanceof z.ZodNullable) {
142
+ return value === null ? value : transformScenarioSchemaValue({
143
+ schema: asClassicSchema(schema.unwrap()),
144
+ value,
145
+ playerIds,
146
+ path,
147
+ direction: options.direction
148
+ });
149
+ }
150
+ if (schema instanceof z.ZodDefault || schema instanceof z.ZodPrefault || schema instanceof z.ZodCatch || schema instanceof z.ZodReadonly || schema instanceof z.ZodNonOptional) {
151
+ return transformScenarioSchemaValue({
152
+ schema: asClassicSchema(schema.unwrap()),
153
+ value,
154
+ playerIds,
155
+ path,
156
+ direction: options.direction
157
+ });
158
+ }
159
+ if (schema instanceof z.ZodArray) {
160
+ if (!Array.isArray(value)) return value;
161
+ return value.map(
162
+ (item, index) => transformScenarioSchemaValue({
163
+ schema: asClassicSchema(schema.element),
164
+ value: item,
165
+ playerIds,
166
+ path: appendScenarioPath(path, index),
167
+ direction: options.direction
168
+ })
169
+ );
170
+ }
171
+ if (schema instanceof z.ZodTuple) {
172
+ if (!Array.isArray(value)) return value;
173
+ const definition = tupleDefinition(schema);
174
+ const items = definition.items ?? [];
175
+ return value.map((item, index) => {
176
+ const itemSchema = items[index] ?? definition.rest;
177
+ return itemSchema ? transformScenarioSchemaValue({
178
+ schema: asClassicSchema(itemSchema),
179
+ value: item,
180
+ playerIds,
181
+ path: appendScenarioPath(path, index),
182
+ direction: options.direction
183
+ }) : item;
184
+ });
185
+ }
186
+ if (schema instanceof z.ZodObject) {
187
+ if (!isPlainObject(value)) return value;
188
+ const output = { ...value };
189
+ for (const [key, childSchema] of Object.entries(schema.shape)) {
190
+ if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
191
+ output[key] = transformScenarioSchemaValue({
192
+ schema: asClassicSchema(childSchema),
193
+ value: value[key],
194
+ playerIds,
195
+ path: appendScenarioPath(path, key),
196
+ direction: options.direction
197
+ });
198
+ }
199
+ return output;
200
+ }
201
+ if (schema instanceof z.ZodRecord) {
202
+ if (!isPlainObject(value)) return value;
203
+ if (manifestSchemaFamily(schema.keyType) === "playerId") {
204
+ throw new ScenarioSchemaValueError(
205
+ path,
206
+ "player-valued record keys cannot be represented by scenario seat references"
207
+ );
208
+ }
209
+ return Object.fromEntries(
210
+ Object.entries(value).map(([key, item]) => [
211
+ key,
212
+ transformScenarioSchemaValue({
213
+ schema: asClassicSchema(schema.valueType),
214
+ value: item,
215
+ playerIds,
216
+ path: appendScenarioPath(path, key),
217
+ direction: options.direction
218
+ })
219
+ ])
220
+ );
221
+ }
222
+ if (schema instanceof z.ZodUnion) {
223
+ let firstSeatError;
224
+ for (const option of schema.options) {
225
+ try {
226
+ if (options.direction === "project" && !asClassicSchema(option).safeParse(value).success) {
227
+ continue;
228
+ }
229
+ const resolved = transformScenarioSchemaValue({
230
+ schema: asClassicSchema(option),
231
+ value,
232
+ playerIds,
233
+ path,
234
+ direction: options.direction
235
+ });
236
+ if (options.direction === "project" || asClassicSchema(option).safeParse(resolved).success) {
237
+ return resolved;
238
+ }
239
+ } catch (error) {
240
+ if (firstSeatError === void 0 && error instanceof ScenarioSchemaValueError) {
241
+ firstSeatError = error;
242
+ }
243
+ }
244
+ }
245
+ if (firstSeatError) throw firstSeatError;
246
+ return value;
247
+ }
248
+ if (schema instanceof z.ZodIntersection) {
249
+ const definition = intersectionDefinition(schema);
250
+ const leftResolved = transformScenarioSchemaValue({
251
+ schema: asClassicSchema(definition.left),
252
+ value,
253
+ playerIds,
254
+ path,
255
+ direction: options.direction
256
+ });
257
+ return transformScenarioSchemaValue({
258
+ schema: asClassicSchema(definition.right),
259
+ value: leftResolved,
260
+ playerIds,
261
+ path,
262
+ direction: options.direction
263
+ });
264
+ }
265
+ if (schema instanceof z.ZodPipe) {
266
+ return transformScenarioSchemaValue({
267
+ schema: asClassicSchema(schema.in),
268
+ value,
269
+ playerIds,
270
+ path,
271
+ direction: options.direction
272
+ });
273
+ }
274
+ if (schema instanceof z.ZodLazy) {
275
+ return transformScenarioSchemaValue({
276
+ schema: asClassicSchema(schema.unwrap()),
277
+ value,
278
+ playerIds,
279
+ path,
280
+ direction: options.direction
281
+ });
282
+ }
283
+ return value;
284
+ }
285
+
286
+ // src/testing/scenario-definition-validation.ts
287
+ var ScenarioDefinitionValidationError = class extends Error {
288
+ code;
289
+ path;
290
+ constructor(options) {
291
+ super(
292
+ `Invalid scenario definition at '${options.path}': ${options.reason}.`
293
+ );
294
+ this.name = "ScenarioDefinitionValidationError";
295
+ this.code = options.code;
296
+ this.path = options.path;
297
+ }
298
+ };
299
+ var checkpointIdPattern = /^[a-z0-9][a-z0-9-]*$/u;
300
+ function validateCheckpoints(definition) {
301
+ if (!Object.prototype.hasOwnProperty.call(definition, "checkpoints")) return;
302
+ const checkpoints = requireObject(
303
+ definition.checkpoints,
304
+ "scenario.checkpoints"
305
+ );
306
+ assertJsonSerializable(checkpoints, "scenario.checkpoints");
307
+ const occupied = /* @__PURE__ */ new Map();
308
+ for (const [id, candidate] of Object.entries(checkpoints)) {
309
+ const path = appendScenarioPath("scenario.checkpoints", id);
310
+ if (!checkpointIdPattern.test(id) || id === "setup") {
311
+ fail({
312
+ code: "INVALID_VALUE",
313
+ path,
314
+ reason: "checkpoint ids must be lowercase kebab-case and cannot be 'setup'"
315
+ });
316
+ }
317
+ const checkpoint = requireObject(candidate, path);
318
+ assertExactKeys(checkpoint, ["segment", "completed"], path);
319
+ assertOwn(checkpoint, "segment", path);
320
+ assertOwn(checkpoint, "completed", path);
321
+ if (!["setup", "given", "when"].includes(String(checkpoint.segment))) {
322
+ fail({
323
+ code: "INVALID_VALUE",
324
+ path: appendScenarioPath(path, "segment"),
325
+ reason: "expected setup, given, or when"
326
+ });
327
+ }
328
+ const completed = assertSafeInteger(
329
+ checkpoint.completed,
330
+ appendScenarioPath(path, "completed")
331
+ );
332
+ const segment = checkpoint.segment;
333
+ const maximum = segment === "setup" ? 0 : definition[segment].length;
334
+ if (completed < 0 || completed > maximum || segment === "setup" && completed !== 0) {
335
+ fail({
336
+ code: "OUT_OF_RANGE",
337
+ path: appendScenarioPath(path, "completed"),
338
+ reason: `expected 0 through ${maximum} for ${segment}`
339
+ });
340
+ }
341
+ const coordinate = `${segment}:${completed}`;
342
+ const previous = occupied.get(coordinate);
343
+ if (previous !== void 0) {
344
+ fail({
345
+ code: "INVALID_VALUE",
346
+ path,
347
+ reason: `duplicates checkpoint '${previous}' at ${coordinate}`
348
+ });
349
+ }
350
+ occupied.set(coordinate, id);
351
+ }
352
+ }
353
+ function fail(options) {
354
+ throw new ScenarioDefinitionValidationError(options);
355
+ }
356
+ function isPlainObject2(value) {
357
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
358
+ return false;
359
+ }
360
+ const prototype = Object.getPrototypeOf(value);
361
+ return prototype === Object.prototype || prototype === null;
362
+ }
363
+ function requireObject(value, path) {
364
+ if (!isPlainObject2(value)) {
365
+ fail({ code: "INVALID_TYPE", path, reason: "expected a plain object" });
366
+ }
367
+ return value;
368
+ }
369
+ function assertExactKeys(value, allowed, path) {
370
+ const allowedSet = new Set(allowed);
371
+ for (const key of Object.keys(value)) {
372
+ if (!allowedSet.has(key)) {
373
+ fail({
374
+ code: "UNKNOWN_FIELD",
375
+ path: appendScenarioPath(path, key),
376
+ reason: "field is not part of the scenario contract"
377
+ });
378
+ }
379
+ }
380
+ }
381
+ function assertOwn(value, key, path) {
382
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
383
+ fail({
384
+ code: "INVALID_TYPE",
385
+ path: appendScenarioPath(path, key),
386
+ reason: "required field is missing"
387
+ });
388
+ }
389
+ }
390
+ function assertSafeInteger(value, path) {
391
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) {
392
+ fail({
393
+ code: "UNSAFE_INTEGER",
394
+ path,
395
+ reason: "expected a finite safe integer"
396
+ });
397
+ }
398
+ return value;
399
+ }
400
+ function assertJsonSerializable(value, path, ancestors = /* @__PURE__ */ new Set()) {
401
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
402
+ return;
403
+ }
404
+ if (typeof value === "number") {
405
+ if (!Number.isFinite(value)) {
406
+ fail({
407
+ code: "NON_SERIALIZABLE",
408
+ path,
409
+ reason: "non-finite numbers are not JSON-serializable"
410
+ });
411
+ }
412
+ return;
413
+ }
414
+ if (typeof value !== "object") {
415
+ fail({
416
+ code: "NON_SERIALIZABLE",
417
+ path,
418
+ reason: `${typeof value} values are not JSON-serializable`
419
+ });
420
+ }
421
+ if (ancestors.has(value)) {
422
+ fail({
423
+ code: "NON_SERIALIZABLE",
424
+ path,
425
+ reason: "cyclic values are not JSON-serializable"
426
+ });
427
+ }
428
+ if (!Array.isArray(value) && !isPlainObject2(value)) {
429
+ fail({
430
+ code: "NON_SERIALIZABLE",
431
+ path,
432
+ reason: "class instances are not JSON-serializable scenario data"
433
+ });
434
+ }
435
+ ancestors.add(value);
436
+ if (Array.isArray(value)) {
437
+ for (let index = 0; index < value.length; index += 1) {
438
+ if (!Object.prototype.hasOwnProperty.call(value, index)) {
439
+ fail({
440
+ code: "NON_SERIALIZABLE",
441
+ path: appendScenarioPath(path, index),
442
+ reason: "sparse array entries are not allowed"
443
+ });
444
+ }
445
+ assertJsonSerializable(
446
+ value[index],
447
+ appendScenarioPath(path, index),
448
+ ancestors
449
+ );
450
+ }
451
+ const extraKeys = Reflect.ownKeys(value).filter((key) => {
452
+ if (key === "length") return false;
453
+ return typeof key !== "string" || !/^(0|[1-9][0-9]*)$/.test(key);
454
+ });
455
+ if (extraKeys.length > 0) {
456
+ const key = extraKeys[0];
457
+ fail({
458
+ code: "NON_SERIALIZABLE",
459
+ path: appendScenarioPath(path, key),
460
+ reason: "array metadata is not JSON-serializable"
461
+ });
462
+ }
463
+ ancestors.delete(value);
464
+ return;
465
+ }
466
+ const descriptors = Object.getOwnPropertyDescriptors(value);
467
+ for (const key of Reflect.ownKeys(value)) {
468
+ if (typeof key === "symbol") {
469
+ fail({
470
+ code: "NON_SERIALIZABLE",
471
+ path: appendScenarioPath(path, key),
472
+ reason: "symbol keys are not JSON-serializable"
473
+ });
474
+ }
475
+ const descriptor = descriptors[key];
476
+ if (descriptor === void 0 || !descriptor.enumerable || !("value" in descriptor)) {
477
+ fail({
478
+ code: "NON_SERIALIZABLE",
479
+ path: appendScenarioPath(path, key),
480
+ reason: "only enumerable data properties are allowed"
481
+ });
482
+ }
483
+ assertJsonSerializable(
484
+ descriptor.value,
485
+ appendScenarioPath(path, key),
486
+ ancestors
487
+ );
488
+ }
489
+ ancestors.delete(value);
490
+ }
491
+ function playerIdsForDefinition(game, players) {
492
+ const declared = game.contract.manifest.literals.playerIds;
493
+ return Array.from(
494
+ { length: players },
495
+ (_, index) => declared[index] ?? `player-${index + 1}`
496
+ );
497
+ }
498
+ function commandSchemas(game, interactionId) {
499
+ const schemasByPhase = createClientParamSchemasByPhase(
500
+ game
501
+ );
502
+ const schemas = Object.values(schemasByPhase).flatMap((schemas2) => {
503
+ const schema = schemas2[interactionId];
504
+ return schema ? [schema] : [];
505
+ });
506
+ const index = collectReducerDefinitionIndex(
507
+ game
508
+ );
509
+ for (const phase of index.phasesByName.values()) {
510
+ const steps = phase.interactions.find(([id]) => id === interactionId)?.[1].steps;
511
+ if (steps)
512
+ schemas.push(
513
+ z2.partialRecord(
514
+ z2.enum(steps.entries.map((step) => step.key)),
515
+ z2.unknown()
516
+ ).refine((params) => Object.keys(params).length === 1)
517
+ );
518
+ }
519
+ return schemas;
520
+ }
521
+ function issuePath(path, issue) {
522
+ return issue.path.reduce(appendScenarioPath, path);
523
+ }
524
+ function validateCommand(options) {
525
+ const command = requireObject(options.command, options.path);
526
+ assertJsonSerializable(command, options.path);
527
+ assertExactKeys(command, ["actor", "interactionId", "params"], options.path);
528
+ for (const field of ["actor", "interactionId", "params"]) {
529
+ assertOwn(command, field, options.path);
530
+ }
531
+ const actorPath = appendScenarioPath(options.path, "actor");
532
+ const actor = requireObject(command.actor, actorPath);
533
+ assertExactKeys(actor, ["seat"], actorPath);
534
+ assertOwn(actor, "seat", actorPath);
535
+ const actorSeat = assertSafeInteger(
536
+ actor.seat,
537
+ appendScenarioPath(actorPath, "seat")
538
+ );
539
+ if (actorSeat < 0 || actorSeat >= options.playerIds.length) {
540
+ fail({
541
+ code: "OUT_OF_RANGE",
542
+ path: appendScenarioPath(actorPath, "seat"),
543
+ reason: `expected a seat from 0 through ${Math.max(0, options.playerIds.length - 1)}`
544
+ });
545
+ }
546
+ const interactionPath = appendScenarioPath(options.path, "interactionId");
547
+ if (typeof command.interactionId !== "string" || command.interactionId === "") {
548
+ fail({
549
+ code: "INVALID_TYPE",
550
+ path: interactionPath,
551
+ reason: "expected a non-empty string"
552
+ });
553
+ }
554
+ const paramsPath = appendScenarioPath(options.path, "params");
555
+ const schemas = commandSchemas(options.game, command.interactionId);
556
+ if (schemas.length === 0) {
557
+ fail({
558
+ code: "UNKNOWN_INTERACTION",
559
+ path: interactionPath,
560
+ reason: `interaction '${command.interactionId}' is not declared by the game`
561
+ });
562
+ }
563
+ let firstSeatError;
564
+ let firstIssue;
565
+ for (const schema of schemas) {
566
+ try {
567
+ const resolved = resolveScenarioSeatReferences({
568
+ schema,
569
+ value: command.params,
570
+ playerIds: options.playerIds,
571
+ path: paramsPath
572
+ });
573
+ const parsed = schema.safeParse(resolved);
574
+ if (parsed.success) return;
575
+ firstIssue ??= parsed.error.issues[0];
576
+ } catch (error) {
577
+ if (error instanceof ScenarioSchemaValueError) {
578
+ firstSeatError ??= error;
579
+ continue;
580
+ }
581
+ throw error;
582
+ }
583
+ }
584
+ if (firstSeatError) {
585
+ fail({
586
+ code: firstSeatError.message.includes("seat from") ? "OUT_OF_RANGE" : "INVALID_COMMAND_PARAMS",
587
+ path: firstSeatError.path,
588
+ reason: firstSeatError.message
589
+ });
590
+ }
591
+ fail({
592
+ code: "INVALID_COMMAND_PARAMS",
593
+ path: firstIssue ? issuePath(paramsPath, firstIssue) : paramsPath,
594
+ reason: firstIssue?.message ?? "parameters do not match the interaction contract"
595
+ });
596
+ }
597
+ function validateScenarioDefinition(game, value) {
598
+ const definition = requireObject(value, "scenario");
599
+ assertExactKeys(
600
+ definition,
601
+ ["id", "description", "setup", "given", "when", "checkpoints", "then"],
602
+ "scenario"
603
+ );
604
+ for (const field of ["id", "setup", "given", "when", "then"]) {
605
+ assertOwn(definition, field, "scenario");
606
+ }
607
+ if (typeof definition.id !== "string" || definition.id === "") {
608
+ fail({
609
+ code: "INVALID_TYPE",
610
+ path: "scenario.id",
611
+ reason: "expected a non-empty string"
612
+ });
613
+ }
614
+ if (Object.prototype.hasOwnProperty.call(definition, "description") && typeof definition.description !== "string") {
615
+ fail({
616
+ code: "INVALID_TYPE",
617
+ path: "scenario.description",
618
+ reason: "expected a string when provided"
619
+ });
620
+ }
621
+ const setup = requireObject(definition.setup, "scenario.setup");
622
+ assertExactKeys(setup, ["players", "seed", "options"], "scenario.setup");
623
+ assertOwn(setup, "players", "scenario.setup");
624
+ assertOwn(setup, "seed", "scenario.setup");
625
+ const players = assertSafeInteger(setup.players, "scenario.setup.players");
626
+ const normalSetup = game.contract.manifest.normalSetup;
627
+ if (normalSetup === void 0 || !Number.isSafeInteger(normalSetup.minPlayers) || !Number.isSafeInteger(normalSetup.maxPlayers) || normalSetup.minPlayers <= 0 || normalSetup.maxPlayers < normalSetup.minPlayers) {
628
+ fail({
629
+ code: "NORMAL_SETUP_UNAVAILABLE",
630
+ path: "scenario.setup.players",
631
+ reason: "the game manifest does not provide valid normal setup bounds"
632
+ });
633
+ }
634
+ if (players < normalSetup.minPlayers || players > normalSetup.maxPlayers) {
635
+ fail({
636
+ code: "OUT_OF_RANGE",
637
+ path: "scenario.setup.players",
638
+ reason: `expected ${normalSetup.minPlayers} through ${normalSetup.maxPlayers} players`
639
+ });
640
+ }
641
+ assertSafeInteger(setup.seed, "scenario.setup.seed");
642
+ assertJsonSerializable(definition.id, "scenario.id");
643
+ if (Object.prototype.hasOwnProperty.call(definition, "description")) {
644
+ assertJsonSerializable(definition.description, "scenario.description");
645
+ }
646
+ assertJsonSerializable(setup, "scenario.setup");
647
+ if (!Array.isArray(definition.given)) {
648
+ fail({
649
+ code: "INVALID_TYPE",
650
+ path: "scenario.given",
651
+ reason: "expected an array"
652
+ });
653
+ }
654
+ if (!Array.isArray(definition.when)) {
655
+ fail({
656
+ code: "INVALID_TYPE",
657
+ path: "scenario.when",
658
+ reason: "expected an array"
659
+ });
660
+ }
661
+ if (typeof definition.then !== "function") {
662
+ fail({
663
+ code: "INVALID_TYPE",
664
+ path: "scenario.then",
665
+ reason: "expected an assertion function"
666
+ });
667
+ }
668
+ validateCheckpoints(definition);
669
+ const playerIds = playerIdsForDefinition(game, players);
670
+ definition.given.forEach(
671
+ (command, index) => validateCommand({
672
+ game,
673
+ command,
674
+ path: `scenario.given[${index}]`,
675
+ playerIds
676
+ })
677
+ );
678
+ definition.when.forEach(
679
+ (command, index) => validateCommand({
680
+ game,
681
+ command,
682
+ path: `scenario.when[${index}]`,
683
+ playerIds
684
+ })
685
+ );
686
+ }
687
+
688
+ // src/testing/definitions.ts
689
+ function resolveScenarioCheckpoint(definition, selector) {
690
+ if (typeof selector !== "string") {
691
+ return structuredClone(selector);
692
+ }
693
+ const checkpoint = definition.checkpoints?.[selector];
694
+ if (checkpoint === void 0) {
695
+ const available = Object.keys(definition.checkpoints ?? {}).sort();
696
+ throw new ScenarioDefinitionValidationError({
697
+ code: "INVALID_VALUE",
698
+ path: "scenario.checkpoints",
699
+ reason: `checkpoint '${selector}' is not declared by scenario '${definition.id}'` + (available.length === 0 ? "; no named checkpoints are available" : `; available checkpoints: ${available.join(", ")}`)
700
+ });
701
+ }
702
+ return structuredClone(checkpoint);
703
+ }
704
+ var ScenarioReplayError = class extends Error {
705
+ scenarioId;
706
+ segment;
707
+ index;
708
+ interactionId;
709
+ errorCode;
710
+ reducerMessage;
711
+ trace;
712
+ constructor(options) {
713
+ super(
714
+ `Scenario '${options.scenarioId}' rejected ${options.segment}[${options.index}] interaction '${options.interactionId}' with ${options.errorCode}` + (options.reducerMessage ? `: ${options.reducerMessage}` : ".")
715
+ );
716
+ this.name = "ScenarioReplayError";
717
+ this.scenarioId = options.scenarioId;
718
+ this.segment = options.segment;
719
+ this.index = options.index;
720
+ this.interactionId = options.interactionId;
721
+ this.errorCode = options.errorCode;
722
+ this.reducerMessage = options.reducerMessage;
723
+ this.trace = options.trace ?? [];
724
+ }
725
+ };
726
+ function createScenarioAuthoring(game) {
727
+ return {
728
+ defineScenario(definition) {
729
+ validateScenarioDefinition(game, definition);
730
+ return definition;
731
+ }
732
+ };
733
+ }
734
+ function toScenarioReplayDefinition(definition) {
735
+ const replay = {
736
+ id: definition.id,
737
+ ...definition.description === void 0 ? {} : { description: definition.description },
738
+ setup: definition.setup,
739
+ given: definition.given,
740
+ when: definition.when
741
+ };
742
+ return structuredClone(replay);
743
+ }
744
+
745
+ // src/testing/scenario-assertion-error.ts
746
+ var SCENARIO_ASSERTION_ERROR_CODE = "SCENARIO_ASSERTION_FAILED";
747
+ var ScenarioAssertionError = class extends Error {
748
+ code = SCENARIO_ASSERTION_ERROR_CODE;
749
+ constructor(message) {
750
+ super(message);
751
+ this.name = "ScenarioAssertionError";
752
+ }
753
+ };
754
+ function scenarioAssertionFailure(message) {
755
+ throw new ScenarioAssertionError(message);
756
+ }
757
+
758
+ // src/testing/create-expect-api.ts
759
+ function isRecord(value) {
760
+ return typeof value === "object" && value !== null && !Array.isArray(value);
761
+ }
762
+ function isDeepStrictEqual(actual, expected) {
763
+ if (Object.is(actual, expected)) return true;
764
+ if (typeof actual !== typeof expected) return false;
765
+ if (actual === null || expected === null) return false;
766
+ if (typeof actual !== "object" || typeof expected !== "object") return false;
767
+ if (actual instanceof Date || expected instanceof Date) {
768
+ return actual instanceof Date && expected instanceof Date && Object.is(actual.getTime(), expected.getTime());
769
+ }
770
+ if (actual instanceof RegExp || expected instanceof RegExp) {
771
+ return actual instanceof RegExp && expected instanceof RegExp && actual.source === expected.source && actual.flags === expected.flags;
772
+ }
773
+ if (Array.isArray(actual) || Array.isArray(expected)) {
774
+ return Array.isArray(actual) && Array.isArray(expected) && actual.length === expected.length && actual.every((value, index) => isDeepStrictEqual(value, expected[index]));
775
+ }
776
+ const actualPrototype = Object.getPrototypeOf(actual);
777
+ const expectedPrototype = Object.getPrototypeOf(expected);
778
+ if (actualPrototype !== expectedPrototype) return false;
779
+ const actualRecord = actual;
780
+ const expectedRecord = expected;
781
+ const actualKeys = Object.keys(actualRecord);
782
+ const expectedKeys = Object.keys(expectedRecord);
783
+ return actualKeys.length === expectedKeys.length && actualKeys.every(
784
+ (key) => Object.prototype.hasOwnProperty.call(expectedRecord, key) && isDeepStrictEqual(actualRecord[key], expectedRecord[key])
785
+ );
786
+ }
787
+ function matchPartial(actual, expected, path = "value") {
788
+ if (!isRecord(expected)) {
789
+ return isDeepStrictEqual(actual, expected) ? null : `${path} does not match`;
790
+ }
791
+ if (!isRecord(actual)) {
792
+ return `${path} is not an object`;
793
+ }
794
+ for (const [key, expectedValue] of Object.entries(expected)) {
795
+ const actualValue = actual[key];
796
+ if (!(key in actual)) {
797
+ return `${path}.${key} is missing`;
798
+ }
799
+ const mismatch = matchPartial(actualValue, expectedValue, `${path}.${key}`);
800
+ if (mismatch) {
801
+ return mismatch;
802
+ }
803
+ }
804
+ return null;
805
+ }
806
+ function asDescriptorList(actual) {
807
+ if (!Array.isArray(actual)) {
808
+ scenarioAssertionFailure("Expected interaction descriptor array.");
809
+ }
810
+ return actual;
811
+ }
812
+ function findInteraction(descriptors, interactionId) {
813
+ return descriptors.find(
814
+ (descriptor) => descriptor.interactionId === interactionId
815
+ ) ?? null;
816
+ }
817
+ function assertDescriptorMatches(descriptor, opts) {
818
+ if (!opts) {
819
+ return;
820
+ }
821
+ const mismatch = matchPartial(descriptor, opts, "interaction");
822
+ if (mismatch) {
823
+ scenarioAssertionFailure(mismatch);
824
+ }
825
+ }
826
+ function createSubmissionErrorMatcher(actual, expected, options) {
827
+ if (isRecord(actual) && (actual.kind === "accepted" || actual.kind === "rejected")) {
828
+ if (actual.kind === "accepted") {
829
+ scenarioAssertionFailure(
830
+ "Expected probe command to reject, but it was accepted."
831
+ );
832
+ }
833
+ const errorCode = typeof actual.errorCode === "string" ? actual.errorCode : void 0;
834
+ const message = typeof actual.message === "string" ? actual.message : "";
835
+ if (expected.errorCode !== void 0 && errorCode !== expected.errorCode) {
836
+ scenarioAssertionFailure(
837
+ `Expected rejection errorCode '${expected.errorCode}', received '${errorCode ?? "undefined"}'.`
838
+ );
839
+ }
840
+ if (typeof expected.message === "string" && message !== expected.message) {
841
+ scenarioAssertionFailure(
842
+ `Expected rejection message '${expected.message}', received '${message}'.`
843
+ );
844
+ }
845
+ if (expected.message instanceof RegExp && !expected.message.test(message)) {
846
+ scenarioAssertionFailure(
847
+ `Expected rejection message '${message}' to match ${String(expected.message)}.`
848
+ );
849
+ }
850
+ return Promise.resolve();
851
+ }
852
+ const resolvePromise = () => {
853
+ if (typeof actual === "function") {
854
+ return Promise.resolve().then(() => actual());
855
+ }
856
+ return Promise.resolve(actual);
857
+ };
858
+ return resolvePromise().then(
859
+ () => scenarioAssertionFailure("Expected promise to reject."),
860
+ (error) => {
861
+ if (!(error instanceof Error)) {
862
+ scenarioAssertionFailure("Expected rejection to be an Error.");
863
+ }
864
+ if (expected.errorCode !== void 0 && error.errorCode !== expected.errorCode) {
865
+ scenarioAssertionFailure(
866
+ `Expected rejection errorCode '${expected.errorCode}', received '${error.errorCode ?? "undefined"}'.${formatLastDiagnosticRejection(options)}`
867
+ );
868
+ }
869
+ if (typeof expected.message === "string" && error.message !== expected.message) {
870
+ scenarioAssertionFailure(
871
+ `Expected rejection message '${expected.message}', received '${error.message}'.${formatLastDiagnosticRejection(
872
+ options
873
+ )}`
874
+ );
875
+ }
876
+ if (expected.message instanceof RegExp && !expected.message.test(error.message)) {
877
+ scenarioAssertionFailure(
878
+ `Expected rejection message '${error.message}' to match ${String(
879
+ expected.message
880
+ )}.${formatLastDiagnosticRejection(options)}`
881
+ );
882
+ }
883
+ }
884
+ );
885
+ }
886
+ function formatLastDiagnosticRejection(options) {
887
+ const rejection = options.lastDiagnosticRejection?.();
888
+ if (!rejection) return "";
889
+ const rule = rejection.ruleId ? ` ruleId=${rejection.ruleId}` : "";
890
+ const message = rejection.message ? ` message=${rejection.message}` : "";
891
+ return `
892
+ last rejection: errorCode=${rejection.errorCode}${rule}${message}`;
893
+ }
894
+ function assertLength(actual, expected) {
895
+ if (actual === null || actual === void 0 || typeof actual.length !== "number") {
896
+ scenarioAssertionFailure(
897
+ "toHaveLength expects a value with a numeric length."
898
+ );
899
+ }
900
+ const length = actual.length;
901
+ if (length !== expected) {
902
+ scenarioAssertionFailure(
903
+ `Expected length ${expected}, received ${length}.`
904
+ );
905
+ }
906
+ }
907
+ function formatInteractionExplanation(explanation) {
908
+ if (!explanation) {
909
+ return "";
910
+ }
911
+ const lines = [
912
+ `availability: ${explanation.availability}`,
913
+ ...explanation.rules.map((rule) => {
914
+ const code = rule.errorCode ? ` (${rule.errorCode})` : "";
915
+ const message = rule.message ? `: ${rule.message}` : "";
916
+ return `rule ${rule.ruleId} ${rule.outcome}${code}${message}`;
917
+ }),
918
+ ...explanation.inputs.map(
919
+ (input) => `input ${input.key} eligibleCount=${input.eligibleCount}`
920
+ )
921
+ ];
922
+ return `
923
+ ${lines.join("\n")}`;
924
+ }
925
+ function assertThrown(actual, predicate) {
926
+ if (typeof actual !== "function") {
927
+ scenarioAssertionFailure("toThrow expects a function.");
928
+ }
929
+ try {
930
+ actual();
931
+ } catch (error) {
932
+ if (!(error instanceof Error)) {
933
+ scenarioAssertionFailure("Thrown value is not an Error.");
934
+ }
935
+ if (predicate === void 0) {
936
+ return;
937
+ }
938
+ if (typeof predicate === "string" && error.message !== predicate) {
939
+ scenarioAssertionFailure(
940
+ `Expected thrown message '${predicate}', received '${error.message}'.`
941
+ );
942
+ }
943
+ if (predicate instanceof RegExp && !predicate.test(error.message)) {
944
+ scenarioAssertionFailure(
945
+ `Expected thrown message '${error.message}' to match ${String(predicate)}.`
946
+ );
947
+ }
948
+ if (typeof predicate === "function" && !predicate(error)) {
949
+ scenarioAssertionFailure("Thrown error did not satisfy predicate.");
950
+ }
951
+ return;
952
+ }
953
+ scenarioAssertionFailure("Expected function to throw.");
954
+ }
955
+ function createExpectApi(options = {}) {
956
+ const buildMatchers = (actual) => ({
957
+ toBe: (expected) => {
958
+ if (actual !== expected) {
959
+ scenarioAssertionFailure(
960
+ `Expected '${String(actual)}' to be '${String(expected)}'.`
961
+ );
962
+ }
963
+ },
964
+ toEqual: (expected) => {
965
+ if (!isDeepStrictEqual(actual, expected)) {
966
+ scenarioAssertionFailure("Expected values to be deeply equal.");
967
+ }
968
+ },
969
+ toMatchObject: (expected) => {
970
+ const mismatch = matchPartial(actual, expected);
971
+ if (mismatch) {
972
+ scenarioAssertionFailure(mismatch);
973
+ }
974
+ },
975
+ toBeDefined: () => {
976
+ if (actual === void 0) {
977
+ scenarioAssertionFailure("Expected value to be defined.");
978
+ }
979
+ },
980
+ toBeUndefined: () => {
981
+ if (actual !== void 0) {
982
+ scenarioAssertionFailure(
983
+ `Expected value to be undefined, but received '${String(actual)}'.`
984
+ );
985
+ }
986
+ },
987
+ toBeNull: () => {
988
+ if (actual !== null) {
989
+ scenarioAssertionFailure(
990
+ `Expected value to be null, but received '${String(actual)}'.`
991
+ );
992
+ }
993
+ },
994
+ toContain: (expected) => {
995
+ if (Array.isArray(actual)) {
996
+ if (!actual.includes(expected)) {
997
+ scenarioAssertionFailure("Expected array to contain value.");
998
+ }
999
+ return;
1000
+ }
1001
+ if (typeof actual === "string") {
1002
+ if (!actual.includes(String(expected))) {
1003
+ scenarioAssertionFailure("Expected string to contain value.");
1004
+ }
1005
+ return;
1006
+ }
1007
+ scenarioAssertionFailure(
1008
+ "toContain expects an array or string actual value."
1009
+ );
1010
+ },
1011
+ toContainEqual: (expected) => {
1012
+ if (!Array.isArray(actual)) {
1013
+ scenarioAssertionFailure(
1014
+ "toContainEqual expects an array actual value."
1015
+ );
1016
+ }
1017
+ if (!actual.some((value) => isDeepStrictEqual(value, expected))) {
1018
+ scenarioAssertionFailure("Expected array to contain an equal value.");
1019
+ }
1020
+ },
1021
+ toHaveLength: (expected) => {
1022
+ assertLength(actual, expected);
1023
+ },
1024
+ toBeGreaterThan: (expected) => {
1025
+ if (typeof actual !== "number") {
1026
+ scenarioAssertionFailure(
1027
+ "toBeGreaterThan expects a number actual value."
1028
+ );
1029
+ }
1030
+ if (actual <= expected) {
1031
+ scenarioAssertionFailure(`Expected ${actual} to be > ${expected}.`);
1032
+ }
1033
+ },
1034
+ toBeGreaterThanOrEqual: (expected) => {
1035
+ if (typeof actual !== "number") {
1036
+ scenarioAssertionFailure(
1037
+ "toBeGreaterThanOrEqual expects a number actual value."
1038
+ );
1039
+ }
1040
+ if (actual < expected) {
1041
+ scenarioAssertionFailure(`Expected ${actual} to be >= ${expected}.`);
1042
+ }
1043
+ },
1044
+ toThrow: (predicate) => {
1045
+ assertThrown(actual, predicate);
1046
+ },
1047
+ toMatchSnapshot: (filename) => {
1048
+ if (!options.matchSnapshot) {
1049
+ scenarioAssertionFailure(
1050
+ "Snapshot matching is not configured for this expect API."
1051
+ );
1052
+ }
1053
+ options.matchSnapshot(filename, actual);
1054
+ },
1055
+ toRejectWith: (expected) => createSubmissionErrorMatcher(actual, expected, options),
1056
+ toHaveInteraction: (interactionId, opts) => {
1057
+ const descriptors = asDescriptorList(actual);
1058
+ const descriptor = findInteraction(descriptors, interactionId);
1059
+ if (!descriptor) {
1060
+ scenarioAssertionFailure(
1061
+ `Expected interaction '${interactionId}' to exist.`
1062
+ );
1063
+ }
1064
+ assertDescriptorMatches(descriptor, opts);
1065
+ },
1066
+ toBeGatedBy: (reason, opts) => {
1067
+ const descriptor = Array.isArray(actual) ? (() => {
1068
+ if (!opts?.interactionId) {
1069
+ scenarioAssertionFailure(
1070
+ "toBeGatedBy on a descriptor array requires opts.interactionId."
1071
+ );
1072
+ }
1073
+ return findInteraction(
1074
+ asDescriptorList(actual),
1075
+ opts.interactionId
1076
+ );
1077
+ })() : actual;
1078
+ if (!descriptor) {
1079
+ scenarioAssertionFailure("Expected interaction descriptor to exist.");
1080
+ }
1081
+ if (descriptor.availability?.status === "available") {
1082
+ scenarioAssertionFailure("Expected interaction to be unavailable.");
1083
+ }
1084
+ const actualReason = descriptor.availability?.status === "notYourTurn" || descriptor.availability?.status === "blocked" ? descriptor.availability.reason : void 0;
1085
+ if (actualReason !== reason) {
1086
+ scenarioAssertionFailure(
1087
+ `Expected unavailable reason '${reason}', received '${actualReason ?? "undefined"}'.`
1088
+ );
1089
+ }
1090
+ },
1091
+ toBeAvailable: (explanation) => {
1092
+ const descriptor = Array.isArray(actual) ? (() => {
1093
+ if (!explanation?.interactionId) {
1094
+ scenarioAssertionFailure(
1095
+ "toBeAvailable on a descriptor array requires an explanation."
1096
+ );
1097
+ }
1098
+ return findInteraction(
1099
+ asDescriptorList(actual),
1100
+ explanation.interactionId
1101
+ );
1102
+ })() : actual;
1103
+ if (!descriptor) {
1104
+ scenarioAssertionFailure(
1105
+ `Expected interaction descriptor to exist.${formatInteractionExplanation(
1106
+ explanation
1107
+ )}`
1108
+ );
1109
+ }
1110
+ if (descriptor.availability?.status !== "available") {
1111
+ scenarioAssertionFailure(
1112
+ `Expected interaction '${descriptor.interactionId ?? "unknown"}' to be available.${formatInteractionExplanation(
1113
+ explanation
1114
+ )}`
1115
+ );
1116
+ }
1117
+ },
1118
+ not: {
1119
+ toHaveInteraction: (interactionId) => {
1120
+ const descriptors = asDescriptorList(actual);
1121
+ if (findInteraction(descriptors, interactionId)) {
1122
+ scenarioAssertionFailure(
1123
+ `Expected interaction '${interactionId}' to be absent.`
1124
+ );
1125
+ }
1126
+ }
1127
+ }
1128
+ });
1129
+ return (actual) => buildMatchers(actual);
1130
+ }
1131
+
1132
+ // src/testing/player-roster.ts
1133
+ function resolvePlayerRoster(game, count) {
1134
+ const setup = game.contract.manifest.normalSetup;
1135
+ if (!setup) throw new Error("Game manifest does not expose normal setup.");
1136
+ if (!Number.isSafeInteger(count) || count < setup.minPlayers || count > setup.maxPlayers)
1137
+ throw new Error("Player count is outside manifest limits.");
1138
+ const declared = game.contract.manifest.literals.playerIds;
1139
+ if (declared.length < count)
1140
+ throw new Error("Manifest roster does not contain enough players.");
1141
+ return declared.slice(0, count);
1142
+ }
1143
+
1144
+ // src/testing/reducer-runtime.ts
1145
+ function createReducerTestingRuntime(definition, options = {}, bundle = createReducerBundle(definition, options)) {
1146
+ const codec = createIngressRuntimeCodec(definition);
1147
+ const parseState = (state) => codec.parseState(state);
1148
+ const parseInput = (input) => codec.parseInput(input);
1149
+ const scope = createTrustedRuntimeScope(definition);
1150
+ const interactions = createInteractionResolver(scope, {
1151
+ diagnostics: options.descriptorDiagnostics ?? (options.diagnostics === "verbose" ? "verbose" : void 0)
1152
+ });
1153
+ const projection = createProjectionBuilder(scope, interactions);
1154
+ function inspect({ state, playerId, interactionId }) {
1155
+ if (typeof playerId !== "string")
1156
+ throw new Error("Expected a string playerId.");
1157
+ return {
1158
+ state: scope.toCombinedState(parseState(state)),
1159
+ playerId: codec.parsePlayerId(playerId),
1160
+ interactionId
1161
+ };
1162
+ }
1163
+ return {
1164
+ ...bundle,
1165
+ async validateInput({ state, input }) {
1166
+ return interactions.validateClientInput(
1167
+ scope.toCombinedState(parseState(state)),
1168
+ parseInput(input)
1169
+ );
1170
+ },
1171
+ async reduce(input) {
1172
+ const result = await bundle.dispatch(input);
1173
+ if (result.kind === "reject") return result;
1174
+ return {
1175
+ kind: result.kind,
1176
+ state: result.state,
1177
+ events: result.events,
1178
+ ...result.terminal ? { terminal: result.terminal } : {}
1179
+ };
1180
+ },
1181
+ project({ state, playerIds, projectionMode }) {
1182
+ if (projectionMode !== "actionsOnly")
1183
+ return bundle.project({ state, playerIds });
1184
+ return projection.project({
1185
+ state: parseState(state),
1186
+ playerIds: playerIds.map((id) => codec.parsePlayerId(id)),
1187
+ projectionMode
1188
+ });
1189
+ },
1190
+ explainInteraction(input) {
1191
+ return interactions.explainInteraction(inspect(input));
1192
+ },
1193
+ currentClientParamSchema(input) {
1194
+ return interactions.currentClientParamSchema(inspect(input));
1195
+ },
1196
+ resolveInteractionActionability(input) {
1197
+ return interactions.resolveInteractionActionability(inspect(input));
1198
+ },
1199
+ enumerateInteractionParams(input) {
1200
+ return interactions.enumerateInteractionParams({
1201
+ ...inspect(input),
1202
+ maxEvaluations: input.maxEvaluations
1203
+ });
1204
+ }
1205
+ };
1206
+ }
1207
+
1208
+ // src/testing/scenario-player-refs.ts
1209
+ var ScenarioCommandParamsError = class extends Error {
1210
+ path;
1211
+ constructor(path, message) {
1212
+ super(`${path}: ${message}`);
1213
+ this.name = "ScenarioCommandParamsError";
1214
+ this.path = path;
1215
+ }
1216
+ };
1217
+ function isScenarioSeatRef(value) {
1218
+ if (!isPlainRecord(value)) return false;
1219
+ const keys = Object.keys(value);
1220
+ return keys.length === 1 && keys[0] === "seat" && Number.isSafeInteger(value.seat) && value.seat >= 0;
1221
+ }
1222
+ function resolveScenarioSeatRef(options) {
1223
+ if (!isScenarioSeatRef(options.ref)) {
1224
+ throw new ScenarioCommandParamsError(
1225
+ options.path,
1226
+ "expected a scenario seat reference"
1227
+ );
1228
+ }
1229
+ const playerId = options.playerIds[options.ref.seat];
1230
+ if (playerId === void 0) {
1231
+ throw new ScenarioCommandParamsError(
1232
+ `${options.path}.seat`,
1233
+ `seat ${options.ref.seat} is out of range for ${options.playerIds.length} player(s)`
1234
+ );
1235
+ }
1236
+ return playerId;
1237
+ }
1238
+ function resolveScenarioCommandParams(options) {
1239
+ const candidateSchemas = clientParamSchemaCandidates(options);
1240
+ let firstSeatError;
1241
+ let firstIssue;
1242
+ for (const schema of candidateSchemas) {
1243
+ try {
1244
+ const resolved = resolveScenarioSeatReferences({
1245
+ schema,
1246
+ value: options.params,
1247
+ playerIds: options.playerIds,
1248
+ path: `${options.path}.params`
1249
+ });
1250
+ const parsed = schema.safeParse(resolved);
1251
+ if (parsed.success) {
1252
+ return resolved;
1253
+ }
1254
+ firstIssue ??= parsed.error.issues[0];
1255
+ } catch (error) {
1256
+ if (error instanceof ScenarioSchemaValueError) {
1257
+ firstSeatError ??= error;
1258
+ continue;
1259
+ }
1260
+ throw error;
1261
+ }
1262
+ }
1263
+ if (firstSeatError) {
1264
+ throw new ScenarioCommandParamsError(
1265
+ firstSeatError.path,
1266
+ firstSeatError.message
1267
+ );
1268
+ }
1269
+ const suffix = firstIssue?.path.map(
1270
+ (part) => typeof part === "number" ? `[${part}]` : `.${String(part)}`
1271
+ ).join("") ?? "";
1272
+ throw new ScenarioCommandParamsError(
1273
+ `${options.path}.params${suffix}`,
1274
+ firstIssue?.message ?? "invalid interaction parameters"
1275
+ );
1276
+ }
1277
+ function projectScenarioCommandParams(options) {
1278
+ const candidateSchemas = clientParamSchemaCandidates(options);
1279
+ let firstSeatError;
1280
+ for (const schema of candidateSchemas) {
1281
+ if (!schema.safeParse(options.params).success) continue;
1282
+ try {
1283
+ const projected = projectScenarioSeatReferences({
1284
+ schema,
1285
+ value: options.params,
1286
+ playerIds: options.playerIds,
1287
+ path: `${options.path}.params`
1288
+ });
1289
+ if (isPlainRecord(projected)) return projected;
1290
+ } catch (error) {
1291
+ if (error instanceof ScenarioSchemaValueError) {
1292
+ firstSeatError ??= error;
1293
+ continue;
1294
+ }
1295
+ throw error;
1296
+ }
1297
+ }
1298
+ if (firstSeatError) {
1299
+ throw new ScenarioCommandParamsError(
1300
+ firstSeatError.path,
1301
+ firstSeatError.message
1302
+ );
1303
+ }
1304
+ throw new ScenarioCommandParamsError(
1305
+ `${options.path}.params`,
1306
+ "runtime interaction parameters do not match a client parameter schema"
1307
+ );
1308
+ }
1309
+ function clientParamSchemaCandidates(options) {
1310
+ if (options.currentSchema) return [options.currentSchema];
1311
+ const schemas = createClientParamSchemasByPhase(options.game);
1312
+ const currentPhaseSchema = schemas[options.phase]?.[options.interactionId];
1313
+ const candidateSchemas = [
1314
+ ...currentPhaseSchema ? [currentPhaseSchema] : [],
1315
+ ...Object.entries(schemas).flatMap(([phase, schemasForPhase]) => {
1316
+ if (phase === options.phase) return [];
1317
+ const schema = schemasForPhase[options.interactionId];
1318
+ return schema ? [schema] : [];
1319
+ })
1320
+ ];
1321
+ if (candidateSchemas.length === 0) {
1322
+ throw new ScenarioCommandParamsError(
1323
+ `${options.path}.interactionId`,
1324
+ `interaction '${options.interactionId}' has no client parameter schema`
1325
+ );
1326
+ }
1327
+ return candidateSchemas;
1328
+ }
1329
+ function isPlainRecord(value) {
1330
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1331
+ return false;
1332
+ }
1333
+ const prototype = Object.getPrototypeOf(value);
1334
+ return prototype === Object.prototype || prototype === null;
1335
+ }
1336
+
1337
+ // src/testing/scenario-replay.ts
1338
+ async function replayScenario(options) {
1339
+ validateReplayDefinition(options.game, options.scenario);
1340
+ const scenario = cloneReplayDefinition(options.scenario);
1341
+ const checkpoint = normalizeCheckpoint(scenario, options.at);
1342
+ const playerIds = resolvePlayerRoster(options.game, scenario.setup.players);
1343
+ const events = [];
1344
+ const bundle = createScenarioBundle(options.game, events, options.bundle);
1345
+ const normalSetup = options.game.contract.manifest.normalSetup;
1346
+ if (!normalSetup) {
1347
+ throw new ScenarioDefinitionValidationError({
1348
+ code: "NORMAL_SETUP_UNAVAILABLE",
1349
+ path: "scenario.setup.players",
1350
+ reason: "the game manifest does not expose normal setup"
1351
+ });
1352
+ }
1353
+ const table = normalSetup.createInitialTable({ playerIds });
1354
+ const initialized = await bundle.initialize({
1355
+ table,
1356
+ playerIds: [...playerIds],
1357
+ rngSeed: scenario.setup.seed,
1358
+ options: scenario.setup.options
1359
+ });
1360
+ const runtime = new ScenarioReplayImplementation({
1361
+ game: options.game,
1362
+ productionBundle: options.bundle,
1363
+ scenario,
1364
+ playerIds,
1365
+ reducerState: initialized.state,
1366
+ terminal: initialized.terminal ?? null,
1367
+ checkpoint: { segment: "setup", completed: 0 },
1368
+ trace: [],
1369
+ events
1370
+ });
1371
+ await runtime.replayTo(checkpoint);
1372
+ return runtime;
1373
+ }
1374
+ async function materializeScenarioRuntimeCheckpoint(options) {
1375
+ const replay = requireScenarioReplayImplementation(
1376
+ await replayScenario(options)
1377
+ );
1378
+ return {
1379
+ checkpoint: structuredClone(replay.checkpoint),
1380
+ checkpointDigest: replay.checkpointDigest,
1381
+ playerIds: [...replay.playerIds],
1382
+ state: structuredClone(replay.reducerState),
1383
+ terminal: structuredClone(replay.terminal)
1384
+ };
1385
+ }
1386
+ function cloneReplayDefinition(scenario) {
1387
+ return structuredClone({
1388
+ id: scenario.id,
1389
+ ...scenario.description === void 0 ? {} : { description: scenario.description },
1390
+ setup: scenario.setup,
1391
+ given: scenario.given,
1392
+ when: scenario.when
1393
+ });
1394
+ }
1395
+ async function assertScenario(options) {
1396
+ const replay = requireScenarioReplayImplementation(options.replay);
1397
+ if (!replay.complete) {
1398
+ throw new Error(
1399
+ `Scenario '${replay.scenarioId}' assertions require a full replay; checkpoint is ${replay.checkpoint.segment}:${replay.checkpoint.completed}.`
1400
+ );
1401
+ }
1402
+ const context = {
1403
+ expect: createExpectApi({
1404
+ lastDiagnosticRejection: () => lastRejectedEvent(replay.diagnostics)
1405
+ }),
1406
+ state: replay.state,
1407
+ view: replay.view,
1408
+ interactions: replay.interactions,
1409
+ explain: replay.explain,
1410
+ get diagnostics() {
1411
+ return replay.diagnostics;
1412
+ },
1413
+ probe: (command) => replay.probe(command)
1414
+ };
1415
+ await options.assertion(context);
1416
+ }
1417
+ async function probeScenarioCommand(options) {
1418
+ return requireScenarioReplayImplementation(options.replay).probe(
1419
+ options.command
1420
+ );
1421
+ }
1422
+ var ScenarioReplayImplementation = class _ScenarioReplayImplementation {
1423
+ scenarioId;
1424
+ scenario;
1425
+ game;
1426
+ playerIds;
1427
+ bundle;
1428
+ productionBundle;
1429
+ events;
1430
+ checkpoint;
1431
+ reducerState;
1432
+ terminal;
1433
+ trace;
1434
+ constructor(state) {
1435
+ this.scenarioId = state.scenario.id;
1436
+ this.scenario = state.scenario;
1437
+ this.game = state.game;
1438
+ this.playerIds = [...state.playerIds];
1439
+ this.reducerState = structuredClone(state.reducerState);
1440
+ this.terminal = structuredClone(state.terminal);
1441
+ this.checkpoint = structuredClone(state.checkpoint);
1442
+ this.trace = structuredClone(
1443
+ state.trace
1444
+ );
1445
+ this.events = structuredClone(state.events);
1446
+ this.productionBundle = state.productionBundle;
1447
+ this.bundle = createScenarioBundle(
1448
+ state.game,
1449
+ this.events,
1450
+ this.productionBundle
1451
+ );
1452
+ this.state = this.state.bind(this);
1453
+ this.view = this.view.bind(this);
1454
+ this.interactions = this.interactions.bind(this);
1455
+ this.explain = this.explain.bind(this);
1456
+ }
1457
+ get checkpointDigest() {
1458
+ return digestPluginRuntimeJson({
1459
+ digestVersion: "scenario-checkpoint@1",
1460
+ scenarioId: this.scenarioId,
1461
+ checkpoint: this.checkpoint,
1462
+ reducerState: this.reducerState,
1463
+ terminal: this.terminal,
1464
+ trace: this.trace
1465
+ });
1466
+ }
1467
+ get complete() {
1468
+ return this.checkpoint.segment === "when" && this.checkpoint.completed === this.scenario.when.length;
1469
+ }
1470
+ get diagnostics() {
1471
+ const flow = readFlowState(this.reducerState);
1472
+ return {
1473
+ events: structuredClone(this.events),
1474
+ lastDispatch: lastAcceptedDispatch(this.events),
1475
+ flow: {
1476
+ currentPhase: flow.currentPhase,
1477
+ activeSeats: flow.activePlayers.flatMap((playerId) => {
1478
+ const seat = this.playerIds.indexOf(playerId);
1479
+ return seat < 0 ? [] : [{ seat }];
1480
+ })
1481
+ }
1482
+ };
1483
+ }
1484
+ state() {
1485
+ return structuredClone(this.reducerState.domain);
1486
+ }
1487
+ view(seat) {
1488
+ const playerId = this.playerId(seat, "seat");
1489
+ const projection = this.project(playerId);
1490
+ return structuredClone(projection.seats?.[playerId]?.view ?? null);
1491
+ }
1492
+ interactions(seat) {
1493
+ const playerId = this.playerId(seat, "seat");
1494
+ const projection = this.project(playerId);
1495
+ const refs = projection.seats?.[playerId]?.availableInteractionRefs ?? [];
1496
+ const interactionsByRef = projection.interactionsByRef ?? {};
1497
+ return structuredClone(
1498
+ refs.flatMap((ref) => {
1499
+ const descriptor = interactionsByRef[ref];
1500
+ return descriptor ? [descriptor] : [];
1501
+ })
1502
+ );
1503
+ }
1504
+ explain(seat, interactionId) {
1505
+ return structuredClone(
1506
+ this.bundle.explainInteraction({
1507
+ state: this.reducerState,
1508
+ playerId: this.playerId(seat, "seat"),
1509
+ interactionId
1510
+ })
1511
+ );
1512
+ }
1513
+ clone() {
1514
+ return new _ScenarioReplayImplementation({
1515
+ game: this.game,
1516
+ productionBundle: this.productionBundle,
1517
+ scenario: this.scenario,
1518
+ playerIds: this.playerIds,
1519
+ reducerState: this.reducerState,
1520
+ terminal: this.terminal,
1521
+ checkpoint: this.checkpoint,
1522
+ trace: this.trace,
1523
+ events: this.events
1524
+ });
1525
+ }
1526
+ inspectionAuthority(perspective) {
1527
+ const selectedPlayerId = perspective.kind === "player" ? this.playerId({ seat: perspective.seat }, "perspective.seat") : null;
1528
+ const projection = this.bundle.project({
1529
+ state: this.reducerState,
1530
+ playerIds: selectedPlayerId ? [selectedPlayerId] : []
1531
+ });
1532
+ const descriptors = selectedPlayerId ? descriptorsForPlayer(projection, selectedPlayerId) : [];
1533
+ const flow = readFlowState(this.reducerState);
1534
+ return {
1535
+ game: this.game,
1536
+ scenario: structuredClone(this.scenario),
1537
+ playerIds: [...this.playerIds],
1538
+ checkpoint: structuredClone(this.checkpoint),
1539
+ checkpointDigest: this.checkpointDigest,
1540
+ publicState: structuredClone(this.reducerState.domain.publicState),
1541
+ view: structuredClone(
1542
+ selectedPlayerId ? projection.seats[selectedPlayerId]?.view ?? null : null
1543
+ ),
1544
+ scheduler: {
1545
+ phase: flow.currentPhase ?? "",
1546
+ step: readPhaseStep(this.reducerState.domain.phase),
1547
+ activePlayerIds: [...projection.schedulerFlow?.activePlayerIds ?? []],
1548
+ pendingPlayerIds: [
1549
+ ...projection.schedulerFlow?.pendingPlayerIds ?? projection.simultaneousPhase?.pendingPlayerIds ?? []
1550
+ ],
1551
+ continuationDependencies: structuredClone(
1552
+ projection.schedulerFlow?.continuationDependencies ?? []
1553
+ )
1554
+ },
1555
+ interactions: descriptors.map((descriptor) => ({
1556
+ descriptor: structuredClone(descriptor),
1557
+ explanation: structuredClone(
1558
+ this.bundle.explainInteraction({
1559
+ state: this.reducerState,
1560
+ playerId: selectedPlayerId,
1561
+ interactionId: String(descriptor.interactionId ?? "")
1562
+ })
1563
+ ),
1564
+ actionability: structuredClone(
1565
+ this.bundle.resolveInteractionActionability({
1566
+ state: this.reducerState,
1567
+ playerId: selectedPlayerId,
1568
+ interactionId: String(descriptor.interactionId ?? "")
1569
+ })
1570
+ )
1571
+ })),
1572
+ entropy: {
1573
+ seed: this.reducerState.runtime.rng.seed,
1574
+ draws: structuredClone(this.reducerState.runtime.rng.draws ?? [])
1575
+ },
1576
+ commandTrace: structuredClone(this.trace)
1577
+ };
1578
+ }
1579
+ currentClientParamSchema(seat, interactionId) {
1580
+ return this.bundle.currentClientParamSchema({
1581
+ state: this.reducerState,
1582
+ playerId: this.playerId({ seat }, "actor"),
1583
+ interactionId
1584
+ });
1585
+ }
1586
+ enumerateInteractionParams(options) {
1587
+ return structuredClone(
1588
+ this.bundle.enumerateInteractionParams({
1589
+ state: this.reducerState,
1590
+ playerId: this.playerId({ seat: options.seat }, "perspective.seat"),
1591
+ interactionId: options.interactionId,
1592
+ maxEvaluations: options.maxEvaluations
1593
+ })
1594
+ );
1595
+ }
1596
+ async advance(command) {
1597
+ const clone = new _ScenarioReplayImplementation({
1598
+ game: this.game,
1599
+ productionBundle: this.productionBundle,
1600
+ scenario: this.scenario,
1601
+ playerIds: this.playerIds,
1602
+ reducerState: this.reducerState,
1603
+ terminal: this.terminal,
1604
+ checkpoint: this.checkpoint,
1605
+ trace: this.trace,
1606
+ events: this.events
1607
+ });
1608
+ const result = await clone.dispatch(command, "probe", 0);
1609
+ if (result.kind === "accept") {
1610
+ clone.trace.push({
1611
+ segment: this.checkpoint.segment === "when" ? "when" : "given",
1612
+ index: this.checkpoint.completed,
1613
+ command: structuredClone(command),
1614
+ trace: structuredClone(result.trace)
1615
+ });
1616
+ return {
1617
+ kind: "accepted",
1618
+ replay: clone,
1619
+ trace: structuredClone(result.trace)
1620
+ };
1621
+ }
1622
+ return {
1623
+ kind: "rejected",
1624
+ errorCode: result.errorCode,
1625
+ ...result.message === void 0 ? {} : { message: result.message },
1626
+ trace: structuredClone(result.trace)
1627
+ };
1628
+ }
1629
+ async replayTo(target) {
1630
+ const givenCount = target.segment === "setup" ? 0 : target.segment === "given" ? target.completed : this.scenario.given.length;
1631
+ const whenCount = target.segment === "when" ? target.completed : 0;
1632
+ for (let index = 0; index < givenCount; index += 1) {
1633
+ await this.dispatchRequired("given", index, this.scenario.given[index]);
1634
+ }
1635
+ this.checkpoint = { segment: "given", completed: givenCount };
1636
+ for (let index = 0; index < whenCount; index += 1) {
1637
+ await this.dispatchRequired("when", index, this.scenario.when[index]);
1638
+ }
1639
+ this.checkpoint = structuredClone(target);
1640
+ }
1641
+ async probe(command) {
1642
+ const clone = requireScenarioReplayImplementation(this.clone());
1643
+ const result = await clone.dispatch(command, "probe", 0);
1644
+ if (result.kind === "accept") {
1645
+ const accepted = {
1646
+ kind: "accepted",
1647
+ command,
1648
+ checkpointDigest: digestPluginRuntimeJson({
1649
+ digestVersion: "scenario-probe@1",
1650
+ from: this.checkpointDigest,
1651
+ command,
1652
+ reducerState: clone.reducerState,
1653
+ trace: result.trace
1654
+ }),
1655
+ trace: result.trace,
1656
+ toBeAccepted() {
1657
+ },
1658
+ toRejectWith(expected) {
1659
+ scenarioAssertionFailure(
1660
+ `Expected probe to reject${expected.errorCode ? ` with ${expected.errorCode}` : ""}, but it was accepted.`
1661
+ );
1662
+ }
1663
+ };
1664
+ return accepted;
1665
+ }
1666
+ const rejected = {
1667
+ kind: "rejected",
1668
+ command,
1669
+ errorCode: result.errorCode,
1670
+ ...result.message === void 0 ? {} : { message: result.message },
1671
+ trace: result.trace,
1672
+ toBeAccepted() {
1673
+ scenarioAssertionFailure(
1674
+ `Expected probe to be accepted, but it rejected with ${result.errorCode}.`
1675
+ );
1676
+ },
1677
+ toRejectWith(expected) {
1678
+ assertRejectionMatches(result, expected);
1679
+ }
1680
+ };
1681
+ return rejected;
1682
+ }
1683
+ async dispatchRequired(segment, index, command) {
1684
+ const result = await this.dispatch(command, segment, index);
1685
+ if (result.kind === "reject") {
1686
+ throw new ScenarioReplayError({
1687
+ scenarioId: this.scenarioId,
1688
+ segment,
1689
+ index,
1690
+ interactionId: command.interactionId,
1691
+ errorCode: result.errorCode,
1692
+ reducerMessage: result.message,
1693
+ trace: result.trace
1694
+ });
1695
+ }
1696
+ this.trace.push({
1697
+ segment,
1698
+ index,
1699
+ command: structuredClone(command),
1700
+ trace: result.trace
1701
+ });
1702
+ }
1703
+ async dispatch(command, segment, index) {
1704
+ if (this.terminal)
1705
+ return { kind: "reject", errorCode: "game-ended", trace: [] };
1706
+ const phase = readFlowState(this.reducerState).currentPhase ?? "";
1707
+ const playerId = resolveScenarioSeatRef({
1708
+ ref: command.actor,
1709
+ playerIds: this.playerIds,
1710
+ path: `scenario.${segment}[${index}].actor`
1711
+ });
1712
+ const params = resolveScenarioCommandParams({
1713
+ game: this.game,
1714
+ currentSchema: this.currentClientParamSchema(
1715
+ command.actor.seat,
1716
+ command.interactionId
1717
+ ),
1718
+ phase,
1719
+ interactionId: command.interactionId,
1720
+ params: command.params,
1721
+ playerIds: this.playerIds,
1722
+ path: `scenario.${segment}[${index}]`
1723
+ });
1724
+ const result = await this.bundle.dispatch({
1725
+ state: this.reducerState,
1726
+ input: {
1727
+ kind: "interaction",
1728
+ playerId,
1729
+ interactionId: command.interactionId,
1730
+ params
1731
+ }
1732
+ });
1733
+ if (result.kind === "reject") {
1734
+ return {
1735
+ kind: "reject",
1736
+ errorCode: result.errorCode,
1737
+ ...result.message === void 0 ? {} : { message: result.message },
1738
+ trace: []
1739
+ };
1740
+ }
1741
+ this.reducerState = structuredClone(result.state);
1742
+ this.terminal = result.terminal ?? null;
1743
+ return {
1744
+ kind: "accept",
1745
+ trace: summarizeWireTrace(result.trace)
1746
+ };
1747
+ }
1748
+ playerId(seat, path) {
1749
+ return resolveScenarioSeatRef({
1750
+ ref: seat,
1751
+ playerIds: this.playerIds,
1752
+ path
1753
+ });
1754
+ }
1755
+ project(playerId) {
1756
+ return this.bundle.project({
1757
+ state: this.reducerState,
1758
+ playerIds: [playerId]
1759
+ });
1760
+ }
1761
+ };
1762
+ function inspectScenarioReplayAuthority(options) {
1763
+ return requireScenarioReplayImplementation(
1764
+ options.replay
1765
+ ).inspectionAuthority(options.perspective);
1766
+ }
1767
+ function currentScenarioClientParamSchema(replay, seat, interactionId) {
1768
+ return requireScenarioReplayImplementation(replay).currentClientParamSchema(
1769
+ seat,
1770
+ interactionId
1771
+ );
1772
+ }
1773
+ function enumerateScenarioInteractionParams(options) {
1774
+ return requireScenarioReplayImplementation(
1775
+ options.replay
1776
+ ).enumerateInteractionParams(options);
1777
+ }
1778
+ function advanceScenarioReplay(options) {
1779
+ return requireScenarioReplayImplementation(options.replay).advance(
1780
+ options.command
1781
+ );
1782
+ }
1783
+ function descriptorsForPlayer(projection, playerId) {
1784
+ const refs = projection.seats[playerId]?.availableInteractionRefs ?? [];
1785
+ const interactionsByRef = projection.interactionsByRef ?? {};
1786
+ return refs.flatMap((ref) => {
1787
+ const descriptor = interactionsByRef[ref];
1788
+ return descriptor ? [descriptor] : [];
1789
+ });
1790
+ }
1791
+ function createScenarioBundle(game, events, bundle) {
1792
+ return createReducerTestingRuntime(
1793
+ game,
1794
+ {
1795
+ diagnostics: {
1796
+ event(event) {
1797
+ events.push(structuredClone(event));
1798
+ }
1799
+ }
1800
+ },
1801
+ bundle
1802
+ );
1803
+ }
1804
+ function validateReplayDefinition(game, replay) {
1805
+ validateScenarioDefinition(game, {
1806
+ ...replay,
1807
+ then: () => void 0
1808
+ });
1809
+ }
1810
+ function normalizeCheckpoint(scenario, checkpoint) {
1811
+ if (checkpoint === void 0) {
1812
+ return { segment: "when", completed: scenario.when.length };
1813
+ }
1814
+ const maximum = checkpoint.segment === "setup" ? 0 : checkpoint.segment === "given" ? scenario.given.length : scenario.when.length;
1815
+ if (!Number.isSafeInteger(checkpoint.completed) || checkpoint.completed < 0 || checkpoint.completed > maximum || checkpoint.segment === "setup" && checkpoint.completed !== 0) {
1816
+ throw new ScenarioDefinitionValidationError({
1817
+ code: "OUT_OF_RANGE",
1818
+ path: "at.completed",
1819
+ reason: `expected 0 through ${maximum} for ${checkpoint.segment}`
1820
+ });
1821
+ }
1822
+ return structuredClone(checkpoint);
1823
+ }
1824
+ function readFlowState(state) {
1825
+ const flow = state.domain?.flow;
1826
+ if (typeof flow !== "object" || flow === null) {
1827
+ return { currentPhase: null, activePlayers: [] };
1828
+ }
1829
+ const record = flow;
1830
+ return {
1831
+ currentPhase: typeof record.currentPhase === "string" ? record.currentPhase : null,
1832
+ activePlayers: Array.isArray(record.activePlayers) ? record.activePlayers.filter(
1833
+ (playerId) => typeof playerId === "string"
1834
+ ) : []
1835
+ };
1836
+ }
1837
+ function readPhaseStep(phase) {
1838
+ if (!phase || typeof phase !== "object") return null;
1839
+ const step = phase.step;
1840
+ return typeof step === "string" ? step : null;
1841
+ }
1842
+ function summarizeWireTrace(trace) {
1843
+ return trace.flatMap((entry) => {
1844
+ switch (entry.kind) {
1845
+ case "acceptedClientInput":
1846
+ return [
1847
+ {
1848
+ kind: "acceptedClientInput",
1849
+ interactionId: entry.input.interactionId,
1850
+ playerId: entry.input.playerId
1851
+ }
1852
+ ];
1853
+ case "phaseEntered":
1854
+ return [{ kind: "phaseEntered", from: entry.from, to: entry.to }];
1855
+ case "rngConsumption":
1856
+ return [
1857
+ {
1858
+ kind: "rngConsumption",
1859
+ version: entry.version,
1860
+ operation: entry.operation,
1861
+ drawIndex: entry.drawIndex
1862
+ }
1863
+ ];
1864
+ }
1865
+ });
1866
+ }
1867
+ function lastAcceptedDispatch(events) {
1868
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1869
+ const event = events[index];
1870
+ if (event?.type === "submitAccepted") {
1871
+ return {
1872
+ submissionId: event.submissionId,
1873
+ trace: structuredClone(event.trace)
1874
+ };
1875
+ }
1876
+ }
1877
+ return null;
1878
+ }
1879
+ function lastRejectedEvent(diagnostics) {
1880
+ for (let index = diagnostics.events.length - 1; index >= 0; index -= 1) {
1881
+ const event = diagnostics.events[index];
1882
+ if (event?.type === "submitRejected") return event;
1883
+ }
1884
+ return null;
1885
+ }
1886
+ function assertRejectionMatches(rejection, expected) {
1887
+ if (expected.errorCode !== void 0 && rejection.errorCode !== expected.errorCode) {
1888
+ scenarioAssertionFailure(
1889
+ `Expected rejection errorCode '${expected.errorCode}', received '${rejection.errorCode}'.`
1890
+ );
1891
+ }
1892
+ if (typeof expected.message === "string" && rejection.message !== expected.message) {
1893
+ scenarioAssertionFailure(
1894
+ `Expected rejection message '${expected.message}', received '${rejection.message ?? "undefined"}'.`
1895
+ );
1896
+ }
1897
+ if (expected.message instanceof RegExp && !expected.message.test(rejection.message ?? "")) {
1898
+ scenarioAssertionFailure(
1899
+ `Expected rejection message '${rejection.message ?? ""}' to match ${String(expected.message)}.`
1900
+ );
1901
+ }
1902
+ }
1903
+ function requireScenarioReplayImplementation(replay) {
1904
+ if (!(replay instanceof ScenarioReplayImplementation)) {
1905
+ throw new Error(
1906
+ "Scenario replay was not created by replayScenario(); alternate replay implementations are not supported."
1907
+ );
1908
+ }
1909
+ return replay;
1910
+ }
50
1911
 
51
1912
  // src/testing/inspection/flow-diagnostic.ts
52
1913
  function createFlowDiagnostic(options) {
@@ -234,11 +2095,12 @@ function projectDispatchTrace(commandTrace, playerIds, perspective) {
234
2095
  }
235
2096
  ];
236
2097
  }
237
- case "appliedInstruction":
2098
+ case "phaseEntered":
238
2099
  return [
239
2100
  {
240
- kind: "appliedInstruction",
241
- instructionKind: trace.instruction
2101
+ kind: "phaseEntered",
2102
+ from: trace.from,
2103
+ to: trace.to
242
2104
  }
243
2105
  ];
244
2106
  case "rngConsumption":
@@ -266,8 +2128,20 @@ function assertSeed(seed) {
266
2128
  }
267
2129
  }
268
2130
 
2131
+ // src/testing/canonical.ts
2132
+ function canonicalScenarioJson(value) {
2133
+ return encodeCanonicalPluginRuntimeJson(value);
2134
+ }
2135
+ function digestScenarioJson(value) {
2136
+ return digestPluginRuntimeJson(value);
2137
+ }
2138
+ function compareCanonicalScenarioJson(left, right) {
2139
+ const leftJson = canonicalScenarioJson(left);
2140
+ const rightJson = canonicalScenarioJson(right);
2141
+ return leftJson === rightJson ? 0 : leftJson < rightJson ? -1 : 1;
2142
+ }
2143
+
269
2144
  // src/testing/exploration/cursor.ts
270
- import { Buffer } from "buffer";
271
2145
  var CURSOR_VERSION = 1;
272
2146
  var ExploreCursorError = class extends Error {
273
2147
  code = "TEST_EXPLORE_CURSOR_STALE";
@@ -290,9 +2164,12 @@ function createExploreCursor(options) {
290
2164
  seedOverride: options.seedOverride ?? null,
291
2165
  nextOrdinal: options.nextOrdinal
292
2166
  };
293
- const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString(
294
- "base64url"
295
- );
2167
+ const encoded = btoa(
2168
+ Array.from(
2169
+ new TextEncoder().encode(JSON.stringify(payload)),
2170
+ (byte) => String.fromCharCode(byte)
2171
+ ).join("")
2172
+ ).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
296
2173
  const digest = digestPluginRuntimeJson(payload).slice("sha256:".length);
297
2174
  return `dbx1.${encoded}.${digest}`;
298
2175
  }
@@ -308,7 +2185,12 @@ function readExploreCursor(options) {
308
2185
  let payload;
309
2186
  try {
310
2187
  payload = JSON.parse(
311
- Buffer.from(encoded, "base64url").toString("utf8")
2188
+ new TextDecoder("utf-8", { fatal: true }).decode(
2189
+ Uint8Array.from(
2190
+ atob(encoded.replaceAll("-", "+").replaceAll("_", "/")),
2191
+ (character) => character.charCodeAt(0)
2192
+ )
2193
+ )
312
2194
  );
313
2195
  } catch {
314
2196
  return stale();
@@ -436,6 +2318,11 @@ async function exploreTransitions(options) {
436
2318
  interactionId: action.interactionId,
437
2319
  params: projectScenarioCommandParams({
438
2320
  game: options.game,
2321
+ currentSchema: currentScenarioClientParamSchema(
2322
+ replay,
2323
+ action.actor.seat,
2324
+ action.interactionId
2325
+ ),
439
2326
  phase: node.flow.phase,
440
2327
  interactionId: action.interactionId,
441
2328
  params: assignment,
@@ -539,7 +2426,7 @@ async function exploreSeeds(options, range) {
539
2426
  actions: inspected.node.actions.map(({ actor, interactionId }) => ({
540
2427
  seat: actor.seat,
541
2428
  interactionId,
542
- concreteOptionCount: counts.get(`${actor.seat}:${interactionId}`) ?? "lazy"
2429
+ concreteOptionCount: counts.get(`${actor.seat}:${interactionId}`) ?? "unknown"
543
2430
  }))
544
2431
  }
545
2432
  });
@@ -582,7 +2469,7 @@ async function concreteOptionCounts(options) {
582
2469
  for (const action of options.node.actions) {
583
2470
  const remaining = options.maxEvaluations - evaluated;
584
2471
  if (remaining <= 0) {
585
- counts.set(`${action.actor.seat}:${action.interactionId}`, "lazy");
2472
+ counts.set(`${action.actor.seat}:${action.interactionId}`, "unknown");
586
2473
  continue;
587
2474
  }
588
2475
  const result = enumerateScenarioInteractionParams({
@@ -595,7 +2482,7 @@ async function concreteOptionCounts(options) {
595
2482
  evaluated += result.enumeration.evaluated;
596
2483
  counts.set(
597
2484
  `${action.actor.seat}:${action.interactionId}`,
598
- result.enumeration.status === "enumerated" ? result.enumeration.assignments.length : "lazy"
2485
+ result.enumeration.status === "enumerated" ? result.enumeration.assignments.length : "unknown"
599
2486
  );
600
2487
  }
601
2488
  return counts;
@@ -644,90 +2531,6 @@ function assertSeedRange(range) {
644
2531
  }
645
2532
  }
646
2533
 
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
2534
  // src/testing/reducer-scenario/create-reducer-scenario-runner.ts
732
2535
  function createReducerScenarioRunner(options) {
733
2536
  return {
@@ -742,7 +2545,7 @@ function createReducerScenarioRunner(options) {
742
2545
  playerIds: [options.viewer.playerId]
743
2546
  });
744
2547
  const staticProjection = options.bundle.boardStatic();
745
- const flow = readFlowState(state);
2548
+ const flow = readFlowState2(state);
746
2549
  const materialized = materializePluginGameplayFrame({
747
2550
  currentPhase: flow.currentPhase,
748
2551
  activePlayers: flow.activePlayers,
@@ -763,7 +2566,7 @@ function createReducerScenarioRunner(options) {
763
2566
  staticProjection,
764
2567
  gameVersion,
765
2568
  actionSetVersion,
766
- projectionDigest: digestUIFixtureJson({
2569
+ projectionDigest: digestScenarioJson({
767
2570
  digestVersion: "reducer-scenario-frame@2",
768
2571
  scenarioId: options.scenarioId,
769
2572
  frameId: id,
@@ -847,857 +2650,587 @@ function createReducerScenarioRunner(options) {
847
2650
  }
848
2651
  };
849
2652
  }
850
- function readFlowState(state) {
2653
+ function readFlowState2(state) {
851
2654
  return {
852
2655
  currentPhase: state.domain?.flow?.currentPhase ?? null,
853
2656
  activePlayers: [...state.domain?.flow?.activePlayers ?? []]
854
2657
  };
855
2658
  }
856
2659
 
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}'.`
2660
+ // src/testing/sources/test-source.ts
2661
+ function createTestSource(initial) {
2662
+ const sessionId = crypto.randomUUID();
2663
+ const submissions = [];
2664
+ const lifecycle = createSourceLifecycle({
2665
+ send(command) {
2666
+ submissions.push(
2667
+ Object.freeze({
2668
+ operation: command.type === "interaction.submit" ? "submit" : "cancel",
2669
+ interactionId: command.interactionId,
2670
+ ...command.type === "interaction.submit" ? { params: immutableCopy(command.params) } : {},
2671
+ resolve(result) {
2672
+ lifecycle.result({
2673
+ ...result,
2674
+ type: "interaction.result",
2675
+ clientActionId: command.clientActionId
2676
+ });
2677
+ }
2678
+ })
933
2679
  );
2680
+ },
2681
+ recover() {
2682
+ },
2683
+ close() {
934
2684
  }
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
2685
+ });
2686
+ function emit(value) {
2687
+ const snapshot = SourceSnapshotSchema.parse(value);
2688
+ lifecycle.session({ sessionId, players: snapshot.players });
2689
+ lifecycle.frame({
2690
+ ...snapshot.frame,
2691
+ basis: {
2692
+ version: snapshot.version,
2693
+ perspectivePlayerId: snapshot.me,
2694
+ actionSetVersion: computePluginActionSetVersion({
2695
+ version: snapshot.version,
2696
+ availableInteractions: snapshot.frame.availableInteractions
2697
+ })
2698
+ }
944
2699
  });
2700
+ }
2701
+ emit(initial);
2702
+ return {
2703
+ ...lifecycle.source,
2704
+ get submissions() {
2705
+ return [...submissions];
2706
+ },
2707
+ emit
945
2708
  };
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));
2709
+ }
2710
+
2711
+ // src/testing/sources/local-source.ts
2712
+ import { createStore } from "@tanstack/store";
2713
+ import { z as z3 } from "zod";
2714
+ var checkpointSchema = z3.object({
2715
+ state: ReducerSessionStateSchema,
2716
+ terminal: GameOutcomeSchema.nullable()
2717
+ }).strict();
2718
+ async function localSource(game, options) {
2719
+ const setup = game.contract.manifest.normalSetup;
2720
+ if (!setup) throw new Error("Game manifest does not expose normal setup.");
2721
+ const playerIds = resolvePlayerRoster(game, options.players);
2722
+ const runtime = createReducerTestingRuntime(game);
2723
+ const initial = await runtime.initialize({
2724
+ table: setup.createInitialTable({ playerIds }),
2725
+ playerIds,
2726
+ rngSeed: options.seed,
2727
+ options: options.options
2728
+ });
2729
+ return createLocalProvider(
2730
+ game,
2731
+ runtime,
2732
+ playerIds,
2733
+ { state: initial.state, terminal: initial.terminal ?? null },
2734
+ options.as ?? playerIds[0]
2735
+ );
2736
+ }
2737
+ function createLocalProvider(game, runtime, playerIds, initial, selectedPlayerId) {
2738
+ let checkpoint = immutableCopy(checkpointSchema.parse(initial));
2739
+ let revision = 1;
2740
+ let playerId = selectedPlayerId;
2741
+ let epoch = 0;
2742
+ let disposed = false;
2743
+ let inFlight = false;
2744
+ let disconnect = () => {
967
2745
  };
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;
2746
+ const session = {
2747
+ sessionId: crypto.randomUUID(),
2748
+ players: playerIds.map((id) => ({ playerId: id, displayName: id }))
997
2749
  };
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"
1010
- });
1011
- await waitForConfiguredLatency();
1012
- sendHostPayload({
1013
- ...step.response,
1014
- clientActionId: command.clientActionId
2750
+ const store = createStore({
2751
+ snapshot: null,
2752
+ connection: "connecting",
2753
+ request: null
2754
+ });
2755
+ let lifecycle;
2756
+ function assertOpen() {
2757
+ if (disposed) throw new Error("Local source disposed.");
2758
+ }
2759
+ function assertIdle() {
2760
+ assertOpen();
2761
+ if (inFlight || store.get().request)
2762
+ throw new Error("A gameplay interaction is already pending.");
2763
+ }
2764
+ function makeFrame(state = checkpoint.state) {
2765
+ const projection = runtime.project({ state, playerIds: [playerId] });
2766
+ const provisional = materializePluginGameplayFrame({
2767
+ currentPhase: state.domain.flow.currentPhase,
2768
+ activePlayers: projection.schedulerFlow?.activePlayerIds ?? [],
2769
+ dynamicProjection: projection,
2770
+ staticProjection: runtime.boardStatic(),
2771
+ perspectivePlayerId: playerId,
2772
+ version: revision,
2773
+ actionSetVersion: "pending"
1015
2774
  });
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;
2775
+ return {
2776
+ ...provisional,
2777
+ basis: {
2778
+ ...provisional.basis,
2779
+ actionSetVersion: computePluginActionSetVersion({
2780
+ version: revision,
2781
+ availableInteractions: provisional.availableInteractions
2782
+ })
1046
2783
  }
1047
- }
1048
- };
1049
- const transport = {
1050
- start(nextOnMessage) {
1051
- onMessage = nextOnMessage;
1052
- queueMicrotask(() => {
1053
- sendInit();
1054
- drainOneHostFrame();
2784
+ };
2785
+ }
2786
+ function publish() {
2787
+ lifecycle.frame(makeFrame());
2788
+ }
2789
+ async function execute(input) {
2790
+ assertOpen();
2791
+ if (inFlight) throw new Error("A gameplay interaction is already pending.");
2792
+ if (checkpoint.terminal)
2793
+ return { accepted: false, errorCode: "game-ended" };
2794
+ inFlight = true;
2795
+ const generation = epoch;
2796
+ try {
2797
+ const result = await runtime.dispatch({ state: checkpoint.state, input });
2798
+ if (disposed || generation !== epoch)
2799
+ throw new Error("Local source lifetime changed.");
2800
+ if (result.kind === "reject")
2801
+ return {
2802
+ accepted: false,
2803
+ errorCode: result.errorCode,
2804
+ ...result.message === void 0 ? {} : { message: result.message }
2805
+ };
2806
+ checkpoint = immutableCopy({
2807
+ state: result.state,
2808
+ terminal: result.terminal ?? null
1055
2809
  });
1056
- return () => {
1057
- onMessage = null;
1058
- };
1059
- },
1060
- send(message) {
1061
- handleClientPayload(message);
2810
+ revision++;
2811
+ publish();
2812
+ return { accepted: true };
2813
+ } finally {
2814
+ if (generation === epoch) inFlight = false;
1062
2815
  }
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();
2816
+ }
2817
+ function replaceLifetime() {
2818
+ if (!playerIds.includes(playerId))
2819
+ throw new Error("Unknown local source seat.");
2820
+ epoch++;
2821
+ inFlight = false;
2822
+ disconnect();
2823
+ lifecycle?.source.dispose();
2824
+ const generation = epoch;
2825
+ lifecycle = createSourceLifecycle({
2826
+ context: { sessionId: session.sessionId, playerId },
2827
+ send(command) {
2828
+ const current = lifecycle;
2829
+ void execute(
2830
+ command.type === "interaction.cancel" ? {
2831
+ kind: "interaction.cancel",
2832
+ playerId,
2833
+ interactionId: command.interactionId
2834
+ } : {
2835
+ kind: "interaction",
2836
+ playerId,
2837
+ interactionId: command.interactionId,
2838
+ params: command.params
2839
+ }
2840
+ ).then((result) => {
2841
+ if (generation === epoch)
2842
+ current.result({
2843
+ ...result,
2844
+ type: "interaction.result",
2845
+ clientActionId: command.clientActionId
2846
+ });
2847
+ }).catch((error) => {
2848
+ if (generation === epoch)
2849
+ current.fail(
2850
+ error instanceof Error ? error : new Error(String(error))
2851
+ );
2852
+ });
2853
+ },
2854
+ recover: publish,
2855
+ close() {
1078
2856
  }
2857
+ });
2858
+ lifecycle.session(session);
2859
+ publish();
2860
+ store.setState(() => lifecycle.source.store.get());
2861
+ const subscription = lifecycle.source.store.subscribe(
2862
+ () => store.setState(() => lifecycle.source.store.get())
2863
+ );
2864
+ disconnect = () => subscription.unsubscribe();
2865
+ }
2866
+ replaceLifetime();
2867
+ return {
2868
+ store: {
2869
+ get: () => store.get(),
2870
+ subscribe: (listener) => store.subscribe(listener)
1079
2871
  },
1080
- async flush() {
1081
- await waitForConfiguredLatency();
1082
- await Promise.resolve();
1083
- while (pendingHandlers.size > 0) {
1084
- await Promise.allSettled([...pendingHandlers]);
1085
- }
1086
- throwFixtureError();
2872
+ async submit(interactionId, params) {
2873
+ assertIdle();
2874
+ return lifecycle.source.submit(interactionId, params);
1087
2875
  },
1088
- async advanceHost() {
1089
- await waitForConfiguredLatency();
1090
- drainHostFrames();
1091
- throwFixtureError();
2876
+ async cancel(interactionId) {
2877
+ assertIdle();
2878
+ return lifecycle.source.cancel(interactionId);
1092
2879
  },
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;
2880
+ async apply(command) {
2881
+ assertIdle();
2882
+ const actor = resolveScenarioSeatRef({
2883
+ ref: command.actor,
2884
+ playerIds,
2885
+ path: "command.actor"
2886
+ });
2887
+ const currentSchema = runtime.currentClientParamSchema({
2888
+ state: checkpoint.state,
2889
+ playerId: actor,
2890
+ interactionId: command.interactionId
2891
+ });
2892
+ const params = resolveScenarioCommandParams({
2893
+ game,
2894
+ currentSchema,
2895
+ phase: checkpoint.state.domain.flow.currentPhase,
2896
+ interactionId: command.interactionId,
2897
+ params: command.params,
2898
+ playerIds,
2899
+ path: "command"
2900
+ });
2901
+ return execute({
2902
+ kind: "interaction",
2903
+ playerId: actor,
2904
+ interactionId: command.interactionId,
2905
+ params
2906
+ });
1101
2907
  },
1102
- getEvents() {
1103
- return events.map((event) => ({ ...event }));
2908
+ switchSeat(next) {
2909
+ assertOpen();
2910
+ if (!playerIds.includes(next))
2911
+ throw new Error("Unknown local source seat.");
2912
+ if (next === playerId) return;
2913
+ playerId = next;
2914
+ replaceLifetime();
1104
2915
  },
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
- );
2916
+ checkpoint() {
2917
+ assertOpen();
2918
+ return structuredClone(checkpoint);
2919
+ },
2920
+ restore(value) {
2921
+ assertOpen();
2922
+ const next = checkpointSchema.parse(value);
2923
+ runtime.project({ state: next.state, playerIds: [...playerIds] });
2924
+ const restoredRoster = next.state.domain.table.playerOrder;
2925
+ if (restoredRoster.length !== playerIds.length || restoredRoster.some((id, index) => id !== playerIds[index]))
2926
+ throw new Error("Checkpoint roster does not match the local session.");
2927
+ checkpoint = immutableCopy(next);
2928
+ revision++;
2929
+ replaceLifetime();
2930
+ },
2931
+ inspect() {
2932
+ assertOpen();
2933
+ return store.get().snapshot;
2934
+ },
2935
+ async explore({ maxEvaluations }) {
2936
+ assertOpen();
2937
+ if (!Number.isSafeInteger(maxEvaluations) || maxEvaluations < 1)
2938
+ throw new Error("Enumeration budget must be a positive integer.");
2939
+ if (checkpoint.terminal) return [];
2940
+ const commands = [];
2941
+ let remaining = maxEvaluations;
2942
+ const capturedState = checkpoint.state;
2943
+ const capturedEpoch = epoch;
2944
+ const capturedRevision = revision;
2945
+ for (const descriptor of store.get().snapshot.frame.availableInteractions) {
2946
+ if (remaining <= 0) break;
2947
+ const result = runtime.enumerateInteractionParams({
2948
+ state: checkpoint.state,
2949
+ playerId,
2950
+ interactionId: descriptor.interactionId,
2951
+ maxEvaluations: remaining
2952
+ });
2953
+ if (!result.found || !result.visible || !result.enumeration) continue;
2954
+ remaining -= result.enumeration.evaluated;
2955
+ for (const params of result.enumeration.assignments) {
2956
+ const probe = await runtime.dispatch({
2957
+ state: structuredClone(capturedState),
2958
+ input: {
2959
+ kind: "interaction",
2960
+ playerId,
2961
+ interactionId: descriptor.interactionId,
2962
+ params
2963
+ }
2964
+ });
2965
+ if (disposed || capturedEpoch !== epoch || capturedRevision !== revision)
2966
+ throw new Error("Source changed during exploration.");
2967
+ if (probe.kind === "reject") continue;
2968
+ const projected = projectScenarioCommandParams({
2969
+ game,
2970
+ currentSchema: runtime.currentClientParamSchema({
2971
+ state: checkpoint.state,
2972
+ playerId,
2973
+ interactionId: descriptor.interactionId
2974
+ }),
2975
+ phase: checkpoint.state.domain.flow.currentPhase,
2976
+ interactionId: descriptor.interactionId,
2977
+ params,
2978
+ playerIds,
2979
+ path: "explore"
2980
+ });
2981
+ commands.push({
2982
+ actor: { seat: playerIds.indexOf(playerId) },
2983
+ interactionId: descriptor.interactionId,
2984
+ params: projected
2985
+ });
2986
+ }
1112
2987
  }
2988
+ return commands;
2989
+ },
2990
+ dispose() {
2991
+ if (disposed) return;
2992
+ disposed = true;
2993
+ epoch++;
2994
+ lifecycle.source.dispose();
2995
+ disconnect();
1113
2996
  }
1114
2997
  };
1115
2998
  }
1116
2999
 
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]
3000
+ // src/testing/sources/scenario-source.ts
3001
+ async function scenarioSource(game, scenario, options = {}) {
3002
+ const materialized = await materializeScenarioRuntimeCheckpoint({
3003
+ game,
3004
+ scenario,
3005
+ ...options.at === void 0 ? {} : { at: resolveScenarioCheckpoint(scenario, options.at) }
3006
+ });
3007
+ return createLocalProvider(
3008
+ game,
3009
+ createReducerTestingRuntime(game),
3010
+ materialized.playerIds,
3011
+ { state: materialized.state, terminal: materialized.terminal },
3012
+ options.as ?? materialized.playerIds[0]
1128
3013
  );
1129
- return /* @__PURE__ */ jsx(PluginRuntimeBoundary, { runtime: fixtureRuntime, children });
1130
3014
  }
1131
3015
 
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
- );
3016
+ // src/testing/sources/fuzz.ts
3017
+ async function fuzz(game, options) {
3018
+ if (!Number.isSafeInteger(options.steps) || options.steps < 0)
3019
+ throw new Error("Fuzz steps must be a nonnegative integer.");
3020
+ const players = options.players ?? game.contract.manifest.normalSetup?.minPlayers;
3021
+ if (!players) throw new Error("Game manifest does not expose normal setup.");
3022
+ const source = await localSource(game, { players, seed: options.seed });
3023
+ let rng = {
3024
+ seed: options.seed,
3025
+ cursor: 0,
3026
+ draws: [],
3027
+ trace: []
3028
+ };
3029
+ const commands = [];
3030
+ try {
3031
+ for (let step = 0; step < options.steps && !source.checkpoint().terminal; step++) {
3032
+ const actors = [];
3033
+ for (const player of source.inspect().players) {
3034
+ source.switchSeat(player.playerId);
3035
+ if (source.inspect().frame.availableInteractions.some(
3036
+ (interaction) => interaction.availability.status === "available"
3037
+ ))
3038
+ actors.push(player.playerId);
1182
3039
  }
1183
- steps.push({
1184
- id: `${exchange.id}.host-frame`,
1185
- kind: "host.frame",
1186
- frameId: exchange.result.toFrameId
3040
+ if (actors.length === 0) break;
3041
+ const [actorIndex, actorRng] = nextRandomInt(actors.length, rng);
3042
+ rng = actorRng;
3043
+ source.switchSeat(actors[actorIndex]);
3044
+ const choices = await source.explore({
3045
+ maxEvaluations: options.maxEvaluations ?? 5e3
1187
3046
  });
3047
+ if (choices.length === 0) break;
3048
+ const [index, next] = nextRandomInt(choices.length, rng);
3049
+ rng = next;
3050
+ const command = choices[index];
3051
+ commands.push(command);
3052
+ const result = await source.apply(command);
3053
+ if (!result.accepted)
3054
+ throw new Error(`Explored command rejected: ${result.errorCode}`);
1188
3055
  }
3056
+ return { commands, checkpoint: source.checkpoint() };
3057
+ } catch (error) {
3058
+ return {
3059
+ commands,
3060
+ checkpoint: source.checkpoint(),
3061
+ error: error instanceof Error ? error : new Error(String(error))
3062
+ };
3063
+ } finally {
3064
+ source.dispose();
1189
3065
  }
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
3066
  }
1253
3067
 
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()
3068
+ // src/testing/candidate-verification.ts
3069
+ var CANDIDATE_INPUT_FIELDS = /* @__PURE__ */ new Set([
3070
+ "reducer",
3071
+ "bundle",
3072
+ "scenarios",
3073
+ "maxScenarios",
3074
+ "maxStepsPerScenario"
1349
3075
  ]);
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);
3076
+ var MAX_DIAGNOSTIC_MESSAGE_LENGTH = 2e3;
3077
+ var MAX_DIAGNOSTIC_TRACE_ENTRIES = 100;
3078
+ async function runCandidateVerification(input) {
3079
+ assertCandidateInputFields(input);
3080
+ assertReducerBundleContract(input.bundle, "candidate verification");
3081
+ const scenarios = normalizeScenarios(input.scenarios);
3082
+ if (scenarios.length === 0) {
3083
+ throw new Error("Candidate verification requires at least one scenario.");
1419
3084
  }
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) {
3085
+ const maxScenarios = input.maxScenarios ?? scenarios.length;
3086
+ assertPositiveInteger(maxScenarios, "maxScenarios");
3087
+ if (scenarios.length > maxScenarios) {
1425
3088
  throw new Error(
1426
- `Unsupported browser-interaction protocol '${version}'. Expected major ${UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR}.`
3089
+ `Candidate verification contains ${scenarios.length} scenarios, exceeding limit ${maxScenarios}.`
1427
3090
  );
1428
3091
  }
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);
3092
+ const maxStepsPerScenario = input.maxStepsPerScenario ?? 1e3;
3093
+ assertPositiveInteger(maxStepsPerScenario, "maxStepsPerScenario");
3094
+ const results = [];
3095
+ for (const scenario of scenarios) {
3096
+ results.push(
3097
+ await runScenario({
3098
+ reducer: input.reducer,
3099
+ bundle: input.bundle,
3100
+ scenario,
3101
+ maxStepsPerScenario
3102
+ })
3103
+ );
1439
3104
  }
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
- );
3105
+ const failed = results.filter((result) => result.status === "failed").length;
3106
+ return {
3107
+ status: failed === 0 ? "passed" : "failed",
3108
+ scenarioSummary: {
3109
+ total: results.length,
3110
+ passed: results.length - failed,
3111
+ failed,
3112
+ scenarios: results
1457
3113
  }
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
- );
3114
+ };
3115
+ }
3116
+ async function runScenario(input) {
3117
+ const actualSteps = input.scenario.given.length + input.scenario.when.length;
3118
+ if (actualSteps > input.maxStepsPerScenario) {
3119
+ return {
3120
+ id: input.scenario.id,
3121
+ status: "failed",
3122
+ diagnostic: {
3123
+ kind: "limit",
3124
+ message: `Scenario contains ${actualSteps} replay steps, exceeding maxStepsPerScenario limit ${input.maxStepsPerScenario}.`,
3125
+ actualSteps,
3126
+ maxStepsPerScenario: input.maxStepsPerScenario
1468
3127
  }
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
- }
3128
+ };
1476
3129
  }
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
- );
3130
+ let replay;
3131
+ try {
3132
+ replay = await replayScenario({
3133
+ game: input.reducer,
3134
+ bundle: input.bundle,
3135
+ scenario: input.scenario
3136
+ });
3137
+ } catch (error) {
3138
+ return {
3139
+ id: input.scenario.id,
3140
+ status: "failed",
3141
+ diagnostic: replayDiagnostic(error)
3142
+ };
1487
3143
  }
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
3144
+ try {
3145
+ await assertScenario({ replay, assertion: input.scenario.then });
3146
+ return { id: input.scenario.id, status: "passed" };
3147
+ } catch (error) {
3148
+ return {
3149
+ id: input.scenario.id,
3150
+ status: "failed",
3151
+ diagnostic: {
3152
+ kind: "assertion",
3153
+ message: boundedMessage(error)
1521
3154
  }
1522
- })
1523
- );
3155
+ };
3156
+ }
1524
3157
  }
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);
3158
+ function assertCandidateInputFields(input) {
3159
+ for (const field of Object.keys(input)) {
3160
+ if (!CANDIDATE_INPUT_FIELDS.has(field)) {
3161
+ throw new Error(
3162
+ `Candidate verification input contains unsupported field '${field}'.`
3163
+ );
1531
3164
  }
1532
3165
  }
1533
- return protocol.frames[protocol.frames.length - 1];
1534
3166
  }
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
- );
3167
+ function assertPositiveInteger(value, field) {
3168
+ if (!Number.isSafeInteger(value) || value < 1) {
3169
+ throw new Error(`${field} must be a positive safe integer.`);
1542
3170
  }
1543
3171
  }
1544
- function assertUniqueReplayIdentity(step, candidates) {
1545
- if (candidates.length !== 1) {
3172
+ function normalizeScenarios(input) {
3173
+ if (typeof input !== "object" || input === null) {
1546
3174
  throw new Error(
1547
- `Replay step '${step.stepId}' resolved ${candidates.length} identities; expected exactly one.`
3175
+ "Candidate verification scenarios must be an array or record."
1548
3176
  );
1549
3177
  }
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
- );
3178
+ const scenarios = Array.isArray(input) ? [...input] : Object.values(input);
3179
+ const seenIds = /* @__PURE__ */ new Set();
3180
+ for (const scenario of scenarios) {
3181
+ if (typeof scenario?.id !== "string" || scenario.id.length === 0) {
3182
+ throw new Error("Candidate verification scenario id is required.");
1591
3183
  }
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) {
3184
+ if (seenIds.has(scenario.id)) {
1599
3185
  throw new Error(
1600
- `Replay step '${stepId}' missing visible interactions: ${missing.join(
1601
- ", "
1602
- )}.`
3186
+ `Candidate verification scenario id '${scenario.id}' is duplicated.`
1603
3187
  );
1604
3188
  }
3189
+ seenIds.add(scenario.id);
1605
3190
  }
3191
+ return scenarios;
1606
3192
  }
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 });
3193
+ function replayDiagnostic(error) {
3194
+ if (error instanceof ScenarioReplayError) {
3195
+ return {
3196
+ kind: "replay",
3197
+ message: boundedMessage(error),
3198
+ scenarioId: error.scenarioId,
3199
+ segment: error.segment,
3200
+ index: error.index,
3201
+ interactionId: error.interactionId,
3202
+ errorCode: error.errorCode,
3203
+ ...error.reducerMessage === void 0 ? {} : { reducerMessage: boundedText(error.reducerMessage) },
3204
+ trace: error.trace.slice(0, MAX_DIAGNOSTIC_TRACE_ENTRIES)
3205
+ };
3206
+ }
3207
+ return { kind: "replay", message: boundedMessage(error) };
1630
3208
  }
1631
- function submit(options) {
1632
- return activate(options);
3209
+ function boundedMessage(error) {
3210
+ return boundedText(error instanceof Error ? error.message : String(error));
1633
3211
  }
1634
- function assertStep(stepId, expect) {
1635
- return {
1636
- stepId,
1637
- kind: "assert",
1638
- expect
1639
- };
3212
+ function boundedText(value) {
3213
+ return value.slice(0, MAX_DIAGNOSTIC_MESSAGE_LENGTH);
1640
3214
  }
1641
3215
  export {
1642
- FixturePluginRuntime,
1643
3216
  ScenarioAssertionError,
1644
3217
  ScenarioDefinitionValidationError,
1645
3218
  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
3219
  assertScenario,
1654
- assertStep,
1655
- assertSupportedBrowserInteractionProtocol,
1656
- assertUIStepExpectationSatisfied,
1657
- assertUniqueReplayIdentity,
1658
- canonicalUIFixtureJson,
1659
- canonicalizeUIFixtureJson,
1660
- canonicalizeUIScenarioFixture,
1661
- compilePluginProtocolTape,
1662
- compileUIScenarioFixture,
1663
- createDeterministicIdFactory,
1664
3220
  createExpectApi,
1665
- createFixtureHostHarness,
1666
3221
  createReducerScenarioRunner,
1667
3222
  createScenarioAuthoring,
1668
- defaultFixtureEnvironmentInit,
1669
- defineUIScenario,
1670
- digestPluginProtocolTape,
1671
- digestUIFixtureJson,
1672
- digestUIFixtureRequest,
1673
- digestUIFixtureTransportRequest,
1674
- digestUIScenarioFixture,
1675
- drag,
3223
+ createTestSource,
1676
3224
  exploreScenario,
1677
- fill,
1678
- fixtureEnvironmentInitFor,
3225
+ fuzz,
1679
3226
  inspectScenario,
1680
- isSha256Digest,
1681
- isStaleContractArtifactError,
1682
- parseUIScenarioFixture,
1683
- parseUIScenarioFixtureBundleIndex,
1684
- pluginProtocolTapeSchema,
1685
- portableSemanticReplayStepSchema,
1686
- press,
3227
+ localSource,
3228
+ materializeScenarioRuntimeCheckpoint,
1687
3229
  probeScenarioCommand,
1688
3230
  replayScenario,
1689
3231
  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
3232
+ runCandidateVerification,
3233
+ scenarioSource,
3234
+ toScenarioReplayDefinition
1702
3235
  };
1703
3236
  //# sourceMappingURL=testing.js.map