@dreamboard-games/sdk 0.4.0-alpha.8 → 0.4.0-alpha.9
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.
- package/REFERENCE.md +1067 -164
- package/dist/authoring/index.d.ts +4 -9
- package/dist/authoring/index.js +11 -547
- package/dist/authoring/index.js.map +1 -1
- package/dist/authoring-compiler.d.ts +27 -0
- package/dist/authoring-compiler.js +12 -0
- package/dist/authoring-compiler.js.map +1 -0
- package/dist/authoring-materialize-cli.d.ts +1 -0
- package/dist/authoring-materialize-cli.js +46 -0
- package/dist/authoring-materialize-cli.js.map +1 -0
- package/dist/browser-interaction.d.ts +1 -1
- package/dist/{chunk-3BLN5POZ.js → chunk-3PFDHTR7.js} +16 -6
- package/dist/chunk-3PFDHTR7.js.map +1 -0
- package/dist/chunk-45P5CEGH.js +176 -0
- package/dist/chunk-45P5CEGH.js.map +1 -0
- package/dist/chunk-5XTAQ4EQ.js +32 -0
- package/dist/chunk-5XTAQ4EQ.js.map +1 -0
- package/dist/chunk-7ADPYKFT.js +532 -0
- package/dist/chunk-7ADPYKFT.js.map +1 -0
- package/dist/{chunk-44N3T2A5.js → chunk-BUOODBVO.js} +2 -2
- package/dist/{chunk-ZYONCTZ2.js → chunk-GMOBG7Y7.js} +4 -4
- package/dist/{chunk-VTK4TIXC.js → chunk-JRGP6W4V.js} +3 -3
- package/dist/chunk-L3W2FPRQ.js +1793 -0
- package/dist/chunk-L3W2FPRQ.js.map +1 -0
- package/dist/{chunk-XV27IQHA.js → chunk-NGYFBNBP.js} +3 -3
- package/dist/chunk-P6IJ5YVY.js +154 -0
- package/dist/chunk-P6IJ5YVY.js.map +1 -0
- package/dist/{chunk-IYX5OO2K.js → chunk-QQNL4WTX.js} +15 -2
- package/dist/chunk-QQNL4WTX.js.map +1 -0
- package/dist/{chunk-2TAKLJJD.js → chunk-SACZWUA6.js} +139 -46
- package/dist/chunk-SACZWUA6.js.map +1 -0
- package/dist/{chunk-VTNEWMWJ.js → chunk-SFOAY5H4.js} +2 -2
- package/dist/{chunk-VTNEWMWJ.js.map → chunk-SFOAY5H4.js.map} +1 -1
- package/dist/chunk-UNVCT5Z7.js +73 -0
- package/dist/chunk-UNVCT5Z7.js.map +1 -0
- package/dist/{chunk-YGQTL63W.js → chunk-WDR7SKBM.js} +2 -2
- package/dist/chunk-XDGRC6OR.js +7039 -0
- package/dist/chunk-XDGRC6OR.js.map +1 -0
- package/dist/chunk-XLHLFTLI.js +399 -0
- package/dist/chunk-XLHLFTLI.js.map +1 -0
- package/dist/{chunk-B2YBS3SG.js → chunk-YNW3KBDE.js} +41 -13
- package/dist/chunk-YNW3KBDE.js.map +1 -0
- package/dist/codegen.js +1 -1
- package/dist/{components-BTwSIjXj.d.ts → components-BPzsSRs8.d.ts} +1 -1
- package/dist/definitions-0kjPV9wh.d.ts +294 -0
- package/dist/{diagnostics-1BWjRo6-.d.ts → diagnostics-DIi3ina7.d.ts} +2 -1
- package/dist/{index-CFTjJIXd.d.ts → index-CWzWpQzM.d.ts} +1 -1
- package/dist/{index.d-C0PKOVbh.d.ts → index.d-C7Ztirg6.d.ts} +211 -3
- package/dist/index.js +2 -2
- package/dist/package-set.d.ts +2 -2
- package/dist/package-set.js +2 -2
- package/dist/{protocol-nS2ojsGr.d.ts → protocol-B2LWcqB4.d.ts} +9 -0
- package/dist/reducer/advanced.d.ts +4 -4
- package/dist/reducer/advanced.js +7 -26
- package/dist/reducer/advanced.js.map +1 -1
- package/dist/reducer-contract.d.ts +1 -1
- package/dist/reducer-contract.js +2 -2
- package/dist/reducer.d.ts +17 -11
- package/dist/reducer.js +28 -6050
- package/dist/reducer.js.map +1 -1
- package/dist/reference-game-compiler.d.ts +45 -0
- package/dist/reference-game-compiler.js +25 -0
- package/dist/reference-game-compiler.js.map +1 -0
- package/dist/reference-game-source-manifest-cli.d.ts +1 -0
- package/dist/reference-game-source-manifest-cli.js +84 -0
- package/dist/reference-game-source-manifest-cli.js.map +1 -0
- package/dist/reference-games/index.d.ts +7 -217
- package/dist/reference-games/index.js +28 -208
- package/dist/reference-games/index.js.map +1 -1
- package/dist/runtime/primitives.d.ts +3 -3
- package/dist/runtime/primitives.js +3 -3
- package/dist/runtime/runtime-api.d.ts +1 -1
- package/dist/runtime/workspace-contract.d.ts +4 -4
- package/dist/runtime/workspace-contract.js +4 -4
- package/dist/{runtime-Dkfm1x2r.d.ts → runtime-B_w9AO2n.d.ts} +60 -5
- package/dist/{runtime-api-C6aWo9c0.d.ts → runtime-api-xCWLMQRT.d.ts} +2 -0
- package/dist/runtime.d.ts +7 -7
- package/dist/runtime.js +12 -12
- package/dist/schema-9jZzlEWZ.d.ts +220 -0
- package/dist/{stale-contract-artifact-error-C5AaZPJ8.d.ts → stale-contract-artifact-error-XLaweZtF.d.ts} +1 -1
- package/dist/testing-compiler.d.ts +36 -0
- package/dist/testing-compiler.js +236 -0
- package/dist/testing-compiler.js.map +1 -0
- package/dist/testing-runtime.d.ts +97 -21
- package/dist/testing-runtime.js +154 -95
- package/dist/testing-runtime.js.map +1 -1
- package/dist/testing.d.ts +178 -60
- package/dist/testing.js +641 -26
- package/dist/testing.js.map +1 -1
- package/dist/types-Bvptkukh.d.ts +122 -0
- package/dist/{types-CTeN_QAk.d.ts → types-_zfC_7CD.d.ts} +1 -1
- package/dist/ui/components.d.ts +2 -2
- package/dist/ui/plugin-styles.css +1 -1
- package/dist/{ui-contract-Ue6SywvO.d.ts → ui-contract-CUC7Lyoc.d.ts} +1 -1
- package/dist/ui.d.ts +3 -3
- package/dist/{views-Qx-EHBP8.d.ts → views-DE2eZxPz.d.ts} +15 -3
- package/package.json +22 -2
- package/dist/chunk-2TAKLJJD.js.map +0 -1
- package/dist/chunk-3BLN5POZ.js.map +0 -1
- package/dist/chunk-3OZMHZK3.js +0 -40
- package/dist/chunk-3OZMHZK3.js.map +0 -1
- package/dist/chunk-5CQIJHGF.js +0 -663
- package/dist/chunk-5CQIJHGF.js.map +0 -1
- package/dist/chunk-B2YBS3SG.js.map +0 -1
- package/dist/chunk-IYX5OO2K.js.map +0 -1
- package/dist/types-B_3pObUV.d.ts +0 -192
- /package/dist/{chunk-44N3T2A5.js.map → chunk-BUOODBVO.js.map} +0 -0
- /package/dist/{chunk-ZYONCTZ2.js.map → chunk-GMOBG7Y7.js.map} +0 -0
- /package/dist/{chunk-VTK4TIXC.js.map → chunk-JRGP6W4V.js.map} +0 -0
- /package/dist/{chunk-XV27IQHA.js.map → chunk-NGYFBNBP.js.map} +0 -0
- /package/dist/{chunk-YGQTL63W.js.map → chunk-WDR7SKBM.js.map} +0 -0
|
@@ -0,0 +1,1793 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createClientParamSchemasByPhase
|
|
3
|
+
} from "./chunk-5XTAQ4EQ.js";
|
|
4
|
+
import {
|
|
5
|
+
digestPluginRuntimeJson,
|
|
6
|
+
encodeCanonicalPluginRuntimeJson
|
|
7
|
+
} from "./chunk-QQNL4WTX.js";
|
|
8
|
+
import {
|
|
9
|
+
createReducerBundle
|
|
10
|
+
} from "./chunk-XDGRC6OR.js";
|
|
11
|
+
import {
|
|
12
|
+
manifestSchemaFamily
|
|
13
|
+
} from "./chunk-YNW3KBDE.js";
|
|
14
|
+
|
|
15
|
+
// src/testing/scenario-schema.ts
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
var ScenarioSchemaValueError = class extends Error {
|
|
18
|
+
path;
|
|
19
|
+
constructor(path, message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "ScenarioSchemaValueError";
|
|
22
|
+
this.path = path;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function appendScenarioPath(path, key) {
|
|
26
|
+
if (typeof key === "number") {
|
|
27
|
+
return `${path}[${key}]`;
|
|
28
|
+
}
|
|
29
|
+
if (typeof key === "symbol") {
|
|
30
|
+
return `${path}[${String(key)}]`;
|
|
31
|
+
}
|
|
32
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
|
|
33
|
+
}
|
|
34
|
+
function isPlainObject(value) {
|
|
35
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const prototype = Object.getPrototypeOf(value);
|
|
39
|
+
return prototype === Object.prototype || prototype === null;
|
|
40
|
+
}
|
|
41
|
+
function resolvePlayerSeatReference(options) {
|
|
42
|
+
if (!isPlainObject(options.value)) {
|
|
43
|
+
throw new ScenarioSchemaValueError(
|
|
44
|
+
options.path,
|
|
45
|
+
"expected a seat reference shaped as { seat: number }"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const keys = Object.keys(options.value);
|
|
49
|
+
if (keys.length !== 1 || keys[0] !== "seat") {
|
|
50
|
+
throw new ScenarioSchemaValueError(
|
|
51
|
+
options.path,
|
|
52
|
+
"expected a seat reference with only the 'seat' field"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const seat = options.value.seat;
|
|
56
|
+
const seatPath = appendScenarioPath(options.path, "seat");
|
|
57
|
+
if (typeof seat !== "number" || !Number.isSafeInteger(seat)) {
|
|
58
|
+
throw new ScenarioSchemaValueError(
|
|
59
|
+
seatPath,
|
|
60
|
+
"expected a finite safe integer"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (seat < 0 || seat >= options.playerIds.length) {
|
|
64
|
+
throw new ScenarioSchemaValueError(
|
|
65
|
+
seatPath,
|
|
66
|
+
`expected a seat from 0 through ${Math.max(0, options.playerIds.length - 1)}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const playerId = options.playerIds[seat];
|
|
70
|
+
if (playerId === void 0) {
|
|
71
|
+
throw new ScenarioSchemaValueError(
|
|
72
|
+
seatPath,
|
|
73
|
+
`could not resolve seat ${seat}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return playerId;
|
|
77
|
+
}
|
|
78
|
+
function projectPlayerSeatReference(options) {
|
|
79
|
+
if (typeof options.value !== "string") {
|
|
80
|
+
throw new ScenarioSchemaValueError(
|
|
81
|
+
options.path,
|
|
82
|
+
"expected a runtime player id string"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const seat = options.playerIds.indexOf(options.value);
|
|
86
|
+
if (seat < 0) {
|
|
87
|
+
throw new ScenarioSchemaValueError(
|
|
88
|
+
options.path,
|
|
89
|
+
`runtime player id '${options.value}' is not assigned to a scenario seat`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return { seat };
|
|
93
|
+
}
|
|
94
|
+
function tupleDefinition(schema) {
|
|
95
|
+
return schema._zod.def;
|
|
96
|
+
}
|
|
97
|
+
function intersectionDefinition(schema) {
|
|
98
|
+
return schema._zod.def;
|
|
99
|
+
}
|
|
100
|
+
function asClassicSchema(schema) {
|
|
101
|
+
return schema;
|
|
102
|
+
}
|
|
103
|
+
function resolveScenarioSeatReferences(options) {
|
|
104
|
+
return transformScenarioSchemaValue({ ...options, direction: "resolve" });
|
|
105
|
+
}
|
|
106
|
+
function projectScenarioSeatReferences(options) {
|
|
107
|
+
return transformScenarioSchemaValue({ ...options, direction: "project" });
|
|
108
|
+
}
|
|
109
|
+
function transformScenarioSchemaValue(options) {
|
|
110
|
+
const { schema, value, playerIds, path } = options;
|
|
111
|
+
if (manifestSchemaFamily(schema) === "playerId") {
|
|
112
|
+
return options.direction === "resolve" ? resolvePlayerSeatReference({ value, playerIds, path }) : projectPlayerSeatReference({ value, playerIds, path });
|
|
113
|
+
}
|
|
114
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodExactOptional) {
|
|
115
|
+
return value === void 0 ? value : transformScenarioSchemaValue({
|
|
116
|
+
schema: asClassicSchema(schema.unwrap()),
|
|
117
|
+
value,
|
|
118
|
+
playerIds,
|
|
119
|
+
path,
|
|
120
|
+
direction: options.direction
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (schema instanceof z.ZodNullable) {
|
|
124
|
+
return value === null ? value : transformScenarioSchemaValue({
|
|
125
|
+
schema: asClassicSchema(schema.unwrap()),
|
|
126
|
+
value,
|
|
127
|
+
playerIds,
|
|
128
|
+
path,
|
|
129
|
+
direction: options.direction
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (schema instanceof z.ZodDefault || schema instanceof z.ZodPrefault || schema instanceof z.ZodCatch || schema instanceof z.ZodReadonly || schema instanceof z.ZodNonOptional) {
|
|
133
|
+
return transformScenarioSchemaValue({
|
|
134
|
+
schema: asClassicSchema(schema.unwrap()),
|
|
135
|
+
value,
|
|
136
|
+
playerIds,
|
|
137
|
+
path,
|
|
138
|
+
direction: options.direction
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (schema instanceof z.ZodArray) {
|
|
142
|
+
if (!Array.isArray(value)) return value;
|
|
143
|
+
return value.map(
|
|
144
|
+
(item, index) => transformScenarioSchemaValue({
|
|
145
|
+
schema: asClassicSchema(schema.element),
|
|
146
|
+
value: item,
|
|
147
|
+
playerIds,
|
|
148
|
+
path: appendScenarioPath(path, index),
|
|
149
|
+
direction: options.direction
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (schema instanceof z.ZodTuple) {
|
|
154
|
+
if (!Array.isArray(value)) return value;
|
|
155
|
+
const definition = tupleDefinition(schema);
|
|
156
|
+
const items = definition.items ?? [];
|
|
157
|
+
return value.map((item, index) => {
|
|
158
|
+
const itemSchema = items[index] ?? definition.rest;
|
|
159
|
+
return itemSchema ? transformScenarioSchemaValue({
|
|
160
|
+
schema: asClassicSchema(itemSchema),
|
|
161
|
+
value: item,
|
|
162
|
+
playerIds,
|
|
163
|
+
path: appendScenarioPath(path, index),
|
|
164
|
+
direction: options.direction
|
|
165
|
+
}) : item;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (schema instanceof z.ZodObject) {
|
|
169
|
+
if (!isPlainObject(value)) return value;
|
|
170
|
+
const output = { ...value };
|
|
171
|
+
for (const [key, childSchema] of Object.entries(schema.shape)) {
|
|
172
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
|
|
173
|
+
output[key] = transformScenarioSchemaValue({
|
|
174
|
+
schema: asClassicSchema(childSchema),
|
|
175
|
+
value: value[key],
|
|
176
|
+
playerIds,
|
|
177
|
+
path: appendScenarioPath(path, key),
|
|
178
|
+
direction: options.direction
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return output;
|
|
182
|
+
}
|
|
183
|
+
if (schema instanceof z.ZodRecord) {
|
|
184
|
+
if (!isPlainObject(value)) return value;
|
|
185
|
+
if (manifestSchemaFamily(schema.keyType) === "playerId") {
|
|
186
|
+
throw new ScenarioSchemaValueError(
|
|
187
|
+
path,
|
|
188
|
+
"player-valued record keys cannot be represented by scenario seat references"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return Object.fromEntries(
|
|
192
|
+
Object.entries(value).map(([key, item]) => [
|
|
193
|
+
key,
|
|
194
|
+
transformScenarioSchemaValue({
|
|
195
|
+
schema: asClassicSchema(schema.valueType),
|
|
196
|
+
value: item,
|
|
197
|
+
playerIds,
|
|
198
|
+
path: appendScenarioPath(path, key),
|
|
199
|
+
direction: options.direction
|
|
200
|
+
})
|
|
201
|
+
])
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (schema instanceof z.ZodUnion) {
|
|
205
|
+
let firstSeatError;
|
|
206
|
+
for (const option of schema.options) {
|
|
207
|
+
try {
|
|
208
|
+
if (options.direction === "project" && !asClassicSchema(option).safeParse(value).success) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const resolved = transformScenarioSchemaValue({
|
|
212
|
+
schema: asClassicSchema(option),
|
|
213
|
+
value,
|
|
214
|
+
playerIds,
|
|
215
|
+
path,
|
|
216
|
+
direction: options.direction
|
|
217
|
+
});
|
|
218
|
+
if (options.direction === "project" || asClassicSchema(option).safeParse(resolved).success) {
|
|
219
|
+
return resolved;
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (firstSeatError === void 0 && error instanceof ScenarioSchemaValueError) {
|
|
223
|
+
firstSeatError = error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (firstSeatError) throw firstSeatError;
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
if (schema instanceof z.ZodIntersection) {
|
|
231
|
+
const definition = intersectionDefinition(schema);
|
|
232
|
+
const leftResolved = transformScenarioSchemaValue({
|
|
233
|
+
schema: asClassicSchema(definition.left),
|
|
234
|
+
value,
|
|
235
|
+
playerIds,
|
|
236
|
+
path,
|
|
237
|
+
direction: options.direction
|
|
238
|
+
});
|
|
239
|
+
return transformScenarioSchemaValue({
|
|
240
|
+
schema: asClassicSchema(definition.right),
|
|
241
|
+
value: leftResolved,
|
|
242
|
+
playerIds,
|
|
243
|
+
path,
|
|
244
|
+
direction: options.direction
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (schema instanceof z.ZodPipe) {
|
|
248
|
+
return transformScenarioSchemaValue({
|
|
249
|
+
schema: asClassicSchema(schema.in),
|
|
250
|
+
value,
|
|
251
|
+
playerIds,
|
|
252
|
+
path,
|
|
253
|
+
direction: options.direction
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (schema instanceof z.ZodLazy) {
|
|
257
|
+
return transformScenarioSchemaValue({
|
|
258
|
+
schema: asClassicSchema(schema.unwrap()),
|
|
259
|
+
value,
|
|
260
|
+
playerIds,
|
|
261
|
+
path,
|
|
262
|
+
direction: options.direction
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/testing/scenario-definition-validation.ts
|
|
269
|
+
var ScenarioDefinitionValidationError = class extends Error {
|
|
270
|
+
code;
|
|
271
|
+
path;
|
|
272
|
+
constructor(options) {
|
|
273
|
+
super(
|
|
274
|
+
`Invalid scenario definition at '${options.path}': ${options.reason}.`
|
|
275
|
+
);
|
|
276
|
+
this.name = "ScenarioDefinitionValidationError";
|
|
277
|
+
this.code = options.code;
|
|
278
|
+
this.path = options.path;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
function fail(options) {
|
|
282
|
+
throw new ScenarioDefinitionValidationError(options);
|
|
283
|
+
}
|
|
284
|
+
function isPlainObject2(value) {
|
|
285
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
const prototype = Object.getPrototypeOf(value);
|
|
289
|
+
return prototype === Object.prototype || prototype === null;
|
|
290
|
+
}
|
|
291
|
+
function requireObject(value, path) {
|
|
292
|
+
if (!isPlainObject2(value)) {
|
|
293
|
+
fail({ code: "INVALID_TYPE", path, reason: "expected a plain object" });
|
|
294
|
+
}
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
function assertExactKeys(value, allowed, path) {
|
|
298
|
+
const allowedSet = new Set(allowed);
|
|
299
|
+
for (const key of Object.keys(value)) {
|
|
300
|
+
if (!allowedSet.has(key)) {
|
|
301
|
+
fail({
|
|
302
|
+
code: "UNKNOWN_FIELD",
|
|
303
|
+
path: appendScenarioPath(path, key),
|
|
304
|
+
reason: "field is not part of the scenario contract"
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function assertOwn(value, key, path) {
|
|
310
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
311
|
+
fail({
|
|
312
|
+
code: "INVALID_TYPE",
|
|
313
|
+
path: appendScenarioPath(path, key),
|
|
314
|
+
reason: "required field is missing"
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function assertSafeInteger(value, path) {
|
|
319
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
|
|
320
|
+
fail({
|
|
321
|
+
code: "UNSAFE_INTEGER",
|
|
322
|
+
path,
|
|
323
|
+
reason: "expected a finite safe integer"
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
function assertJsonSerializable(value, path, ancestors = /* @__PURE__ */ new Set()) {
|
|
329
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (typeof value === "number") {
|
|
333
|
+
if (!Number.isFinite(value)) {
|
|
334
|
+
fail({
|
|
335
|
+
code: "NON_SERIALIZABLE",
|
|
336
|
+
path,
|
|
337
|
+
reason: "non-finite numbers are not JSON-serializable"
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (typeof value !== "object") {
|
|
343
|
+
fail({
|
|
344
|
+
code: "NON_SERIALIZABLE",
|
|
345
|
+
path,
|
|
346
|
+
reason: `${typeof value} values are not JSON-serializable`
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (ancestors.has(value)) {
|
|
350
|
+
fail({
|
|
351
|
+
code: "NON_SERIALIZABLE",
|
|
352
|
+
path,
|
|
353
|
+
reason: "cyclic values are not JSON-serializable"
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
if (!Array.isArray(value) && !isPlainObject2(value)) {
|
|
357
|
+
fail({
|
|
358
|
+
code: "NON_SERIALIZABLE",
|
|
359
|
+
path,
|
|
360
|
+
reason: "class instances are not JSON-serializable scenario data"
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
ancestors.add(value);
|
|
364
|
+
if (Array.isArray(value)) {
|
|
365
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
366
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) {
|
|
367
|
+
fail({
|
|
368
|
+
code: "NON_SERIALIZABLE",
|
|
369
|
+
path: appendScenarioPath(path, index),
|
|
370
|
+
reason: "sparse array entries are not allowed"
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
assertJsonSerializable(
|
|
374
|
+
value[index],
|
|
375
|
+
appendScenarioPath(path, index),
|
|
376
|
+
ancestors
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const extraKeys = Reflect.ownKeys(value).filter((key) => {
|
|
380
|
+
if (key === "length") return false;
|
|
381
|
+
return typeof key !== "string" || !/^(0|[1-9][0-9]*)$/.test(key);
|
|
382
|
+
});
|
|
383
|
+
if (extraKeys.length > 0) {
|
|
384
|
+
const key = extraKeys[0];
|
|
385
|
+
fail({
|
|
386
|
+
code: "NON_SERIALIZABLE",
|
|
387
|
+
path: appendScenarioPath(path, key),
|
|
388
|
+
reason: "array metadata is not JSON-serializable"
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
ancestors.delete(value);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
395
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
396
|
+
if (typeof key === "symbol") {
|
|
397
|
+
fail({
|
|
398
|
+
code: "NON_SERIALIZABLE",
|
|
399
|
+
path: appendScenarioPath(path, key),
|
|
400
|
+
reason: "symbol keys are not JSON-serializable"
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
const descriptor = descriptors[key];
|
|
404
|
+
if (descriptor === void 0 || !descriptor.enumerable || !("value" in descriptor)) {
|
|
405
|
+
fail({
|
|
406
|
+
code: "NON_SERIALIZABLE",
|
|
407
|
+
path: appendScenarioPath(path, key),
|
|
408
|
+
reason: "only enumerable data properties are allowed"
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
assertJsonSerializable(
|
|
412
|
+
descriptor.value,
|
|
413
|
+
appendScenarioPath(path, key),
|
|
414
|
+
ancestors
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
ancestors.delete(value);
|
|
418
|
+
}
|
|
419
|
+
function playerIdsForDefinition(game, players) {
|
|
420
|
+
const declared = game.contract.manifest.literals.playerIds;
|
|
421
|
+
return Array.from(
|
|
422
|
+
{ length: players },
|
|
423
|
+
(_, index) => declared[index] ?? `player-${index + 1}`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
function commandSchemas(game, interactionId) {
|
|
427
|
+
const schemasByPhase = createClientParamSchemasByPhase(
|
|
428
|
+
game
|
|
429
|
+
);
|
|
430
|
+
return Object.values(schemasByPhase).flatMap((schemas) => {
|
|
431
|
+
const schema = schemas[interactionId];
|
|
432
|
+
return schema ? [schema] : [];
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function issuePath(path, issue) {
|
|
436
|
+
return issue.path.reduce(appendScenarioPath, path);
|
|
437
|
+
}
|
|
438
|
+
function validateCommand(options) {
|
|
439
|
+
const command = requireObject(options.command, options.path);
|
|
440
|
+
assertJsonSerializable(command, options.path);
|
|
441
|
+
assertExactKeys(command, ["actor", "interactionId", "params"], options.path);
|
|
442
|
+
for (const field of ["actor", "interactionId", "params"]) {
|
|
443
|
+
assertOwn(command, field, options.path);
|
|
444
|
+
}
|
|
445
|
+
const actorPath = appendScenarioPath(options.path, "actor");
|
|
446
|
+
const actor = requireObject(command.actor, actorPath);
|
|
447
|
+
assertExactKeys(actor, ["seat"], actorPath);
|
|
448
|
+
assertOwn(actor, "seat", actorPath);
|
|
449
|
+
const actorSeat = assertSafeInteger(
|
|
450
|
+
actor.seat,
|
|
451
|
+
appendScenarioPath(actorPath, "seat")
|
|
452
|
+
);
|
|
453
|
+
if (actorSeat < 0 || actorSeat >= options.playerIds.length) {
|
|
454
|
+
fail({
|
|
455
|
+
code: "OUT_OF_RANGE",
|
|
456
|
+
path: appendScenarioPath(actorPath, "seat"),
|
|
457
|
+
reason: `expected a seat from 0 through ${Math.max(0, options.playerIds.length - 1)}`
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
const interactionPath = appendScenarioPath(options.path, "interactionId");
|
|
461
|
+
if (typeof command.interactionId !== "string" || command.interactionId === "") {
|
|
462
|
+
fail({
|
|
463
|
+
code: "INVALID_TYPE",
|
|
464
|
+
path: interactionPath,
|
|
465
|
+
reason: "expected a non-empty string"
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const paramsPath = appendScenarioPath(options.path, "params");
|
|
469
|
+
const schemas = commandSchemas(options.game, command.interactionId);
|
|
470
|
+
if (schemas.length === 0) {
|
|
471
|
+
fail({
|
|
472
|
+
code: "UNKNOWN_INTERACTION",
|
|
473
|
+
path: interactionPath,
|
|
474
|
+
reason: `interaction '${command.interactionId}' is not declared by the game`
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
let firstSeatError;
|
|
478
|
+
let firstIssue;
|
|
479
|
+
for (const schema of schemas) {
|
|
480
|
+
try {
|
|
481
|
+
const resolved = resolveScenarioSeatReferences({
|
|
482
|
+
schema,
|
|
483
|
+
value: command.params,
|
|
484
|
+
playerIds: options.playerIds,
|
|
485
|
+
path: paramsPath
|
|
486
|
+
});
|
|
487
|
+
const parsed = schema.safeParse(resolved);
|
|
488
|
+
if (parsed.success) return;
|
|
489
|
+
firstIssue ??= parsed.error.issues[0];
|
|
490
|
+
} catch (error) {
|
|
491
|
+
if (error instanceof ScenarioSchemaValueError) {
|
|
492
|
+
firstSeatError ??= error;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (firstSeatError) {
|
|
499
|
+
fail({
|
|
500
|
+
code: firstSeatError.message.includes("seat from") ? "OUT_OF_RANGE" : "INVALID_COMMAND_PARAMS",
|
|
501
|
+
path: firstSeatError.path,
|
|
502
|
+
reason: firstSeatError.message
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
fail({
|
|
506
|
+
code: "INVALID_COMMAND_PARAMS",
|
|
507
|
+
path: firstIssue ? issuePath(paramsPath, firstIssue) : paramsPath,
|
|
508
|
+
reason: firstIssue?.message ?? "parameters do not match the interaction contract"
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function validateScenarioDefinition(game, value) {
|
|
512
|
+
const definition = requireObject(value, "scenario");
|
|
513
|
+
assertExactKeys(
|
|
514
|
+
definition,
|
|
515
|
+
["id", "description", "setup", "given", "when", "then"],
|
|
516
|
+
"scenario"
|
|
517
|
+
);
|
|
518
|
+
for (const field of ["id", "setup", "given", "when", "then"]) {
|
|
519
|
+
assertOwn(definition, field, "scenario");
|
|
520
|
+
}
|
|
521
|
+
if (typeof definition.id !== "string" || definition.id === "") {
|
|
522
|
+
fail({
|
|
523
|
+
code: "INVALID_TYPE",
|
|
524
|
+
path: "scenario.id",
|
|
525
|
+
reason: "expected a non-empty string"
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
if (Object.prototype.hasOwnProperty.call(definition, "description") && typeof definition.description !== "string") {
|
|
529
|
+
fail({
|
|
530
|
+
code: "INVALID_TYPE",
|
|
531
|
+
path: "scenario.description",
|
|
532
|
+
reason: "expected a string when provided"
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const setup = requireObject(definition.setup, "scenario.setup");
|
|
536
|
+
assertExactKeys(
|
|
537
|
+
setup,
|
|
538
|
+
["players", "seed", "setupProfileId"],
|
|
539
|
+
"scenario.setup"
|
|
540
|
+
);
|
|
541
|
+
assertOwn(setup, "players", "scenario.setup");
|
|
542
|
+
assertOwn(setup, "seed", "scenario.setup");
|
|
543
|
+
const players = assertSafeInteger(setup.players, "scenario.setup.players");
|
|
544
|
+
const normalSetup = game.contract.manifest.normalSetup;
|
|
545
|
+
if (normalSetup === void 0 || !Number.isSafeInteger(normalSetup.minPlayers) || !Number.isSafeInteger(normalSetup.maxPlayers) || normalSetup.minPlayers <= 0 || normalSetup.maxPlayers < normalSetup.minPlayers) {
|
|
546
|
+
fail({
|
|
547
|
+
code: "NORMAL_SETUP_UNAVAILABLE",
|
|
548
|
+
path: "scenario.setup.players",
|
|
549
|
+
reason: "the game manifest does not provide valid normal setup bounds"
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
if (players < normalSetup.minPlayers || players > normalSetup.maxPlayers) {
|
|
553
|
+
fail({
|
|
554
|
+
code: "OUT_OF_RANGE",
|
|
555
|
+
path: "scenario.setup.players",
|
|
556
|
+
reason: `expected ${normalSetup.minPlayers} through ${normalSetup.maxPlayers} players`
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
assertSafeInteger(setup.seed, "scenario.setup.seed");
|
|
560
|
+
if (Object.prototype.hasOwnProperty.call(setup, "setupProfileId")) {
|
|
561
|
+
if (setup.setupProfileId !== null && typeof setup.setupProfileId !== "string") {
|
|
562
|
+
fail({
|
|
563
|
+
code: "INVALID_TYPE",
|
|
564
|
+
path: "scenario.setup.setupProfileId",
|
|
565
|
+
reason: "expected a declared setup profile id or null"
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
if (typeof setup.setupProfileId === "string" && !Object.prototype.hasOwnProperty.call(
|
|
569
|
+
game.contract.manifest.setupProfilesById,
|
|
570
|
+
setup.setupProfileId
|
|
571
|
+
)) {
|
|
572
|
+
fail({
|
|
573
|
+
code: "UNKNOWN_SETUP_PROFILE",
|
|
574
|
+
path: "scenario.setup.setupProfileId",
|
|
575
|
+
reason: `setup profile '${setup.setupProfileId}' is not declared by the manifest`
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
assertJsonSerializable(definition.id, "scenario.id");
|
|
580
|
+
if (Object.prototype.hasOwnProperty.call(definition, "description")) {
|
|
581
|
+
assertJsonSerializable(definition.description, "scenario.description");
|
|
582
|
+
}
|
|
583
|
+
assertJsonSerializable(setup, "scenario.setup");
|
|
584
|
+
if (!Array.isArray(definition.given)) {
|
|
585
|
+
fail({
|
|
586
|
+
code: "INVALID_TYPE",
|
|
587
|
+
path: "scenario.given",
|
|
588
|
+
reason: "expected an array"
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (!Array.isArray(definition.when)) {
|
|
592
|
+
fail({
|
|
593
|
+
code: "INVALID_TYPE",
|
|
594
|
+
path: "scenario.when",
|
|
595
|
+
reason: "expected an array"
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
if (typeof definition.then !== "function") {
|
|
599
|
+
fail({
|
|
600
|
+
code: "INVALID_TYPE",
|
|
601
|
+
path: "scenario.then",
|
|
602
|
+
reason: "expected an assertion function"
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
const playerIds = playerIdsForDefinition(game, players);
|
|
606
|
+
definition.given.forEach(
|
|
607
|
+
(command, index) => validateCommand({
|
|
608
|
+
game,
|
|
609
|
+
command,
|
|
610
|
+
path: `scenario.given[${index}]`,
|
|
611
|
+
playerIds
|
|
612
|
+
})
|
|
613
|
+
);
|
|
614
|
+
definition.when.forEach(
|
|
615
|
+
(command, index) => validateCommand({
|
|
616
|
+
game,
|
|
617
|
+
command,
|
|
618
|
+
path: `scenario.when[${index}]`,
|
|
619
|
+
playerIds
|
|
620
|
+
})
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/testing/definitions.ts
|
|
625
|
+
var ScenarioReplayError = class extends Error {
|
|
626
|
+
scenarioId;
|
|
627
|
+
segment;
|
|
628
|
+
index;
|
|
629
|
+
interactionId;
|
|
630
|
+
errorCode;
|
|
631
|
+
reducerMessage;
|
|
632
|
+
trace;
|
|
633
|
+
constructor(options) {
|
|
634
|
+
super(
|
|
635
|
+
`Scenario '${options.scenarioId}' rejected ${options.segment}[${options.index}] interaction '${options.interactionId}' with ${options.errorCode}` + (options.reducerMessage ? `: ${options.reducerMessage}` : ".")
|
|
636
|
+
);
|
|
637
|
+
this.name = "ScenarioReplayError";
|
|
638
|
+
this.scenarioId = options.scenarioId;
|
|
639
|
+
this.segment = options.segment;
|
|
640
|
+
this.index = options.index;
|
|
641
|
+
this.interactionId = options.interactionId;
|
|
642
|
+
this.errorCode = options.errorCode;
|
|
643
|
+
this.reducerMessage = options.reducerMessage;
|
|
644
|
+
this.trace = options.trace ?? [];
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
function createScenarioAuthoring(game) {
|
|
648
|
+
return {
|
|
649
|
+
defineScenario(definition) {
|
|
650
|
+
validateScenarioDefinition(game, definition);
|
|
651
|
+
return definition;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function toScenarioReplayDefinition(definition) {
|
|
656
|
+
const replay = {
|
|
657
|
+
id: definition.id,
|
|
658
|
+
...definition.description === void 0 ? {} : { description: definition.description },
|
|
659
|
+
setup: definition.setup,
|
|
660
|
+
given: definition.given,
|
|
661
|
+
when: definition.when
|
|
662
|
+
};
|
|
663
|
+
return structuredClone(replay);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/testing/scenario-assertion-error.ts
|
|
667
|
+
var SCENARIO_ASSERTION_ERROR_CODE = "SCENARIO_ASSERTION_FAILED";
|
|
668
|
+
var ScenarioAssertionError = class extends Error {
|
|
669
|
+
code = SCENARIO_ASSERTION_ERROR_CODE;
|
|
670
|
+
constructor(message) {
|
|
671
|
+
super(message);
|
|
672
|
+
this.name = "ScenarioAssertionError";
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
function scenarioAssertionFailure(message) {
|
|
676
|
+
throw new ScenarioAssertionError(message);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/testing/create-expect-api.ts
|
|
680
|
+
function isRecord(value) {
|
|
681
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
682
|
+
}
|
|
683
|
+
function isDeepStrictEqual(actual, expected) {
|
|
684
|
+
if (Object.is(actual, expected)) return true;
|
|
685
|
+
if (typeof actual !== typeof expected) return false;
|
|
686
|
+
if (actual === null || expected === null) return false;
|
|
687
|
+
if (typeof actual !== "object" || typeof expected !== "object") return false;
|
|
688
|
+
if (actual instanceof Date || expected instanceof Date) {
|
|
689
|
+
return actual instanceof Date && expected instanceof Date && Object.is(actual.getTime(), expected.getTime());
|
|
690
|
+
}
|
|
691
|
+
if (actual instanceof RegExp || expected instanceof RegExp) {
|
|
692
|
+
return actual instanceof RegExp && expected instanceof RegExp && actual.source === expected.source && actual.flags === expected.flags;
|
|
693
|
+
}
|
|
694
|
+
if (Array.isArray(actual) || Array.isArray(expected)) {
|
|
695
|
+
return Array.isArray(actual) && Array.isArray(expected) && actual.length === expected.length && actual.every((value, index) => isDeepStrictEqual(value, expected[index]));
|
|
696
|
+
}
|
|
697
|
+
const actualPrototype = Object.getPrototypeOf(actual);
|
|
698
|
+
const expectedPrototype = Object.getPrototypeOf(expected);
|
|
699
|
+
if (actualPrototype !== expectedPrototype) return false;
|
|
700
|
+
const actualRecord = actual;
|
|
701
|
+
const expectedRecord = expected;
|
|
702
|
+
const actualKeys = Object.keys(actualRecord);
|
|
703
|
+
const expectedKeys = Object.keys(expectedRecord);
|
|
704
|
+
return actualKeys.length === expectedKeys.length && actualKeys.every(
|
|
705
|
+
(key) => Object.prototype.hasOwnProperty.call(expectedRecord, key) && isDeepStrictEqual(actualRecord[key], expectedRecord[key])
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
function matchPartial(actual, expected, path = "value") {
|
|
709
|
+
if (!isRecord(expected)) {
|
|
710
|
+
return isDeepStrictEqual(actual, expected) ? null : `${path} does not match`;
|
|
711
|
+
}
|
|
712
|
+
if (!isRecord(actual)) {
|
|
713
|
+
return `${path} is not an object`;
|
|
714
|
+
}
|
|
715
|
+
for (const [key, expectedValue] of Object.entries(expected)) {
|
|
716
|
+
const actualValue = actual[key];
|
|
717
|
+
if (!(key in actual)) {
|
|
718
|
+
return `${path}.${key} is missing`;
|
|
719
|
+
}
|
|
720
|
+
const mismatch = matchPartial(actualValue, expectedValue, `${path}.${key}`);
|
|
721
|
+
if (mismatch) {
|
|
722
|
+
return mismatch;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
function asDescriptorList(actual) {
|
|
728
|
+
if (!Array.isArray(actual)) {
|
|
729
|
+
scenarioAssertionFailure("Expected interaction descriptor array.");
|
|
730
|
+
}
|
|
731
|
+
return actual;
|
|
732
|
+
}
|
|
733
|
+
function findInteraction(descriptors, interactionId) {
|
|
734
|
+
return descriptors.find(
|
|
735
|
+
(descriptor) => descriptor.interactionId === interactionId
|
|
736
|
+
) ?? null;
|
|
737
|
+
}
|
|
738
|
+
function assertDescriptorMatches(descriptor, opts) {
|
|
739
|
+
if (!opts) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const mismatch = matchPartial(descriptor, opts, "interaction");
|
|
743
|
+
if (mismatch) {
|
|
744
|
+
scenarioAssertionFailure(mismatch);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function createSubmissionErrorMatcher(actual, expected, options) {
|
|
748
|
+
if (isRecord(actual) && (actual.kind === "accepted" || actual.kind === "rejected")) {
|
|
749
|
+
if (actual.kind === "accepted") {
|
|
750
|
+
scenarioAssertionFailure(
|
|
751
|
+
"Expected probe command to reject, but it was accepted."
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
const errorCode = typeof actual.errorCode === "string" ? actual.errorCode : void 0;
|
|
755
|
+
const message = typeof actual.message === "string" ? actual.message : "";
|
|
756
|
+
if (expected.errorCode !== void 0 && errorCode !== expected.errorCode) {
|
|
757
|
+
scenarioAssertionFailure(
|
|
758
|
+
`Expected rejection errorCode '${expected.errorCode}', received '${errorCode ?? "undefined"}'.`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
if (typeof expected.message === "string" && message !== expected.message) {
|
|
762
|
+
scenarioAssertionFailure(
|
|
763
|
+
`Expected rejection message '${expected.message}', received '${message}'.`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (expected.message instanceof RegExp && !expected.message.test(message)) {
|
|
767
|
+
scenarioAssertionFailure(
|
|
768
|
+
`Expected rejection message '${message}' to match ${String(expected.message)}.`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
return Promise.resolve();
|
|
772
|
+
}
|
|
773
|
+
const resolvePromise = () => {
|
|
774
|
+
if (typeof actual === "function") {
|
|
775
|
+
return Promise.resolve().then(() => actual());
|
|
776
|
+
}
|
|
777
|
+
return Promise.resolve(actual);
|
|
778
|
+
};
|
|
779
|
+
return resolvePromise().then(
|
|
780
|
+
() => scenarioAssertionFailure("Expected promise to reject."),
|
|
781
|
+
(error) => {
|
|
782
|
+
if (!(error instanceof Error)) {
|
|
783
|
+
scenarioAssertionFailure("Expected rejection to be an Error.");
|
|
784
|
+
}
|
|
785
|
+
if (expected.errorCode !== void 0 && error.errorCode !== expected.errorCode) {
|
|
786
|
+
scenarioAssertionFailure(
|
|
787
|
+
`Expected rejection errorCode '${expected.errorCode}', received '${error.errorCode ?? "undefined"}'.${formatLastDiagnosticRejection(options)}`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (typeof expected.message === "string" && error.message !== expected.message) {
|
|
791
|
+
scenarioAssertionFailure(
|
|
792
|
+
`Expected rejection message '${expected.message}', received '${error.message}'.${formatLastDiagnosticRejection(
|
|
793
|
+
options
|
|
794
|
+
)}`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
if (expected.message instanceof RegExp && !expected.message.test(error.message)) {
|
|
798
|
+
scenarioAssertionFailure(
|
|
799
|
+
`Expected rejection message '${error.message}' to match ${String(
|
|
800
|
+
expected.message
|
|
801
|
+
)}.${formatLastDiagnosticRejection(options)}`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
function formatLastDiagnosticRejection(options) {
|
|
808
|
+
const rejection = options.lastDiagnosticRejection?.();
|
|
809
|
+
if (!rejection) return "";
|
|
810
|
+
const rule = rejection.ruleId ? ` ruleId=${rejection.ruleId}` : "";
|
|
811
|
+
const message = rejection.message ? ` message=${rejection.message}` : "";
|
|
812
|
+
return `
|
|
813
|
+
last rejection: errorCode=${rejection.errorCode}${rule}${message}`;
|
|
814
|
+
}
|
|
815
|
+
function assertLength(actual, expected) {
|
|
816
|
+
if (actual === null || actual === void 0 || typeof actual.length !== "number") {
|
|
817
|
+
scenarioAssertionFailure(
|
|
818
|
+
"toHaveLength expects a value with a numeric length."
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
const length = actual.length;
|
|
822
|
+
if (length !== expected) {
|
|
823
|
+
scenarioAssertionFailure(
|
|
824
|
+
`Expected length ${expected}, received ${length}.`
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function formatInteractionExplanation(explanation) {
|
|
829
|
+
if (!explanation) {
|
|
830
|
+
return "";
|
|
831
|
+
}
|
|
832
|
+
const lines = [
|
|
833
|
+
`availability: ${explanation.availability}`,
|
|
834
|
+
...explanation.rules.map((rule) => {
|
|
835
|
+
const code = rule.errorCode ? ` (${rule.errorCode})` : "";
|
|
836
|
+
const message = rule.message ? `: ${rule.message}` : "";
|
|
837
|
+
return `rule ${rule.ruleId} ${rule.outcome}${code}${message}`;
|
|
838
|
+
}),
|
|
839
|
+
...explanation.inputs.map(
|
|
840
|
+
(input) => `input ${input.key} eligibleCount=${input.eligibleCount}`
|
|
841
|
+
)
|
|
842
|
+
];
|
|
843
|
+
return `
|
|
844
|
+
${lines.join("\n")}`;
|
|
845
|
+
}
|
|
846
|
+
function assertThrown(actual, predicate) {
|
|
847
|
+
if (typeof actual !== "function") {
|
|
848
|
+
scenarioAssertionFailure("toThrow expects a function.");
|
|
849
|
+
}
|
|
850
|
+
try {
|
|
851
|
+
actual();
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (!(error instanceof Error)) {
|
|
854
|
+
scenarioAssertionFailure("Thrown value is not an Error.");
|
|
855
|
+
}
|
|
856
|
+
if (predicate === void 0) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (typeof predicate === "string" && error.message !== predicate) {
|
|
860
|
+
scenarioAssertionFailure(
|
|
861
|
+
`Expected thrown message '${predicate}', received '${error.message}'.`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
if (predicate instanceof RegExp && !predicate.test(error.message)) {
|
|
865
|
+
scenarioAssertionFailure(
|
|
866
|
+
`Expected thrown message '${error.message}' to match ${String(predicate)}.`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
if (typeof predicate === "function" && !predicate(error)) {
|
|
870
|
+
scenarioAssertionFailure("Thrown error did not satisfy predicate.");
|
|
871
|
+
}
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
scenarioAssertionFailure("Expected function to throw.");
|
|
875
|
+
}
|
|
876
|
+
function createExpectApi(options = {}) {
|
|
877
|
+
const buildMatchers = (actual) => ({
|
|
878
|
+
toBe: (expected) => {
|
|
879
|
+
if (actual !== expected) {
|
|
880
|
+
scenarioAssertionFailure(
|
|
881
|
+
`Expected '${String(actual)}' to be '${String(expected)}'.`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
},
|
|
885
|
+
toEqual: (expected) => {
|
|
886
|
+
if (!isDeepStrictEqual(actual, expected)) {
|
|
887
|
+
scenarioAssertionFailure("Expected values to be deeply equal.");
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
toMatchObject: (expected) => {
|
|
891
|
+
const mismatch = matchPartial(actual, expected);
|
|
892
|
+
if (mismatch) {
|
|
893
|
+
scenarioAssertionFailure(mismatch);
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
toBeDefined: () => {
|
|
897
|
+
if (actual === void 0) {
|
|
898
|
+
scenarioAssertionFailure("Expected value to be defined.");
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
toBeUndefined: () => {
|
|
902
|
+
if (actual !== void 0) {
|
|
903
|
+
scenarioAssertionFailure(
|
|
904
|
+
`Expected value to be undefined, but received '${String(actual)}'.`
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
toBeNull: () => {
|
|
909
|
+
if (actual !== null) {
|
|
910
|
+
scenarioAssertionFailure(
|
|
911
|
+
`Expected value to be null, but received '${String(actual)}'.`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
},
|
|
915
|
+
toContain: (expected) => {
|
|
916
|
+
if (Array.isArray(actual)) {
|
|
917
|
+
if (!actual.includes(expected)) {
|
|
918
|
+
scenarioAssertionFailure("Expected array to contain value.");
|
|
919
|
+
}
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (typeof actual === "string") {
|
|
923
|
+
if (!actual.includes(String(expected))) {
|
|
924
|
+
scenarioAssertionFailure("Expected string to contain value.");
|
|
925
|
+
}
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
scenarioAssertionFailure(
|
|
929
|
+
"toContain expects an array or string actual value."
|
|
930
|
+
);
|
|
931
|
+
},
|
|
932
|
+
toContainEqual: (expected) => {
|
|
933
|
+
if (!Array.isArray(actual)) {
|
|
934
|
+
scenarioAssertionFailure(
|
|
935
|
+
"toContainEqual expects an array actual value."
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
if (!actual.some((value) => isDeepStrictEqual(value, expected))) {
|
|
939
|
+
scenarioAssertionFailure("Expected array to contain an equal value.");
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
toHaveLength: (expected) => {
|
|
943
|
+
assertLength(actual, expected);
|
|
944
|
+
},
|
|
945
|
+
toBeGreaterThan: (expected) => {
|
|
946
|
+
if (typeof actual !== "number") {
|
|
947
|
+
scenarioAssertionFailure(
|
|
948
|
+
"toBeGreaterThan expects a number actual value."
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
if (actual <= expected) {
|
|
952
|
+
scenarioAssertionFailure(`Expected ${actual} to be > ${expected}.`);
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
toBeGreaterThanOrEqual: (expected) => {
|
|
956
|
+
if (typeof actual !== "number") {
|
|
957
|
+
scenarioAssertionFailure(
|
|
958
|
+
"toBeGreaterThanOrEqual expects a number actual value."
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
if (actual < expected) {
|
|
962
|
+
scenarioAssertionFailure(`Expected ${actual} to be >= ${expected}.`);
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
toThrow: (predicate) => {
|
|
966
|
+
assertThrown(actual, predicate);
|
|
967
|
+
},
|
|
968
|
+
toMatchSnapshot: (filename) => {
|
|
969
|
+
if (!options.matchSnapshot) {
|
|
970
|
+
scenarioAssertionFailure(
|
|
971
|
+
"Snapshot matching is not configured for this expect API."
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
options.matchSnapshot(filename, actual);
|
|
975
|
+
},
|
|
976
|
+
toRejectWith: (expected) => createSubmissionErrorMatcher(actual, expected, options),
|
|
977
|
+
toHaveInteraction: (interactionId, opts) => {
|
|
978
|
+
const descriptors = asDescriptorList(actual);
|
|
979
|
+
const descriptor = findInteraction(descriptors, interactionId);
|
|
980
|
+
if (!descriptor) {
|
|
981
|
+
scenarioAssertionFailure(
|
|
982
|
+
`Expected interaction '${interactionId}' to exist.`
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
assertDescriptorMatches(descriptor, opts);
|
|
986
|
+
},
|
|
987
|
+
toBeGatedBy: (reason, opts) => {
|
|
988
|
+
const descriptor = Array.isArray(actual) ? (() => {
|
|
989
|
+
if (!opts?.interactionId) {
|
|
990
|
+
scenarioAssertionFailure(
|
|
991
|
+
"toBeGatedBy on a descriptor array requires opts.interactionId."
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
return findInteraction(
|
|
995
|
+
asDescriptorList(actual),
|
|
996
|
+
opts.interactionId
|
|
997
|
+
);
|
|
998
|
+
})() : actual;
|
|
999
|
+
if (!descriptor) {
|
|
1000
|
+
scenarioAssertionFailure("Expected interaction descriptor to exist.");
|
|
1001
|
+
}
|
|
1002
|
+
if (descriptor.availability?.status === "available") {
|
|
1003
|
+
scenarioAssertionFailure("Expected interaction to be unavailable.");
|
|
1004
|
+
}
|
|
1005
|
+
const actualReason = descriptor.availability?.status === "notYourTurn" || descriptor.availability?.status === "insufficientResources" || descriptor.availability?.status === "blocked" ? descriptor.availability.reason : void 0;
|
|
1006
|
+
if (actualReason !== reason) {
|
|
1007
|
+
scenarioAssertionFailure(
|
|
1008
|
+
`Expected unavailable reason '${reason}', received '${actualReason ?? "undefined"}'.`
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
toBeAvailable: (explanation) => {
|
|
1013
|
+
const descriptor = Array.isArray(actual) ? (() => {
|
|
1014
|
+
if (!explanation?.interactionId) {
|
|
1015
|
+
scenarioAssertionFailure(
|
|
1016
|
+
"toBeAvailable on a descriptor array requires an explanation."
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
return findInteraction(
|
|
1020
|
+
asDescriptorList(actual),
|
|
1021
|
+
explanation.interactionId
|
|
1022
|
+
);
|
|
1023
|
+
})() : actual;
|
|
1024
|
+
if (!descriptor) {
|
|
1025
|
+
scenarioAssertionFailure(
|
|
1026
|
+
`Expected interaction descriptor to exist.${formatInteractionExplanation(
|
|
1027
|
+
explanation
|
|
1028
|
+
)}`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
if (descriptor.availability?.status !== "available") {
|
|
1032
|
+
scenarioAssertionFailure(
|
|
1033
|
+
`Expected interaction '${descriptor.interactionId ?? "unknown"}' to be available.${formatInteractionExplanation(
|
|
1034
|
+
explanation
|
|
1035
|
+
)}`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
toBeActiveFor: (playerId, opts) => {
|
|
1040
|
+
const descriptor = Array.isArray(actual) ? (() => {
|
|
1041
|
+
if (!opts?.interactionId) {
|
|
1042
|
+
scenarioAssertionFailure(
|
|
1043
|
+
"toBeActiveFor on a descriptor array requires opts.interactionId."
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
return findInteraction(
|
|
1047
|
+
asDescriptorList(actual),
|
|
1048
|
+
opts.interactionId
|
|
1049
|
+
);
|
|
1050
|
+
})() : actual;
|
|
1051
|
+
if (!descriptor) {
|
|
1052
|
+
scenarioAssertionFailure("Expected interaction descriptor to exist.");
|
|
1053
|
+
}
|
|
1054
|
+
if (descriptor.context?.to !== playerId) {
|
|
1055
|
+
scenarioAssertionFailure(
|
|
1056
|
+
`Expected interaction to target '${playerId}', received '${descriptor.context?.to ?? "undefined"}'.`
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
if (descriptor.availability?.status !== "available") {
|
|
1060
|
+
scenarioAssertionFailure("Expected interaction to be available.");
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
not: {
|
|
1064
|
+
toHaveInteraction: (interactionId) => {
|
|
1065
|
+
const descriptors = asDescriptorList(actual);
|
|
1066
|
+
if (findInteraction(descriptors, interactionId)) {
|
|
1067
|
+
scenarioAssertionFailure(
|
|
1068
|
+
`Expected interaction '${interactionId}' to be absent.`
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
return (actual) => buildMatchers(actual);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/testing/scenario-player-refs.ts
|
|
1078
|
+
var ScenarioCommandParamsError = class extends Error {
|
|
1079
|
+
path;
|
|
1080
|
+
constructor(path, message) {
|
|
1081
|
+
super(`${path}: ${message}`);
|
|
1082
|
+
this.name = "ScenarioCommandParamsError";
|
|
1083
|
+
this.path = path;
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
function isScenarioSeatRef(value) {
|
|
1087
|
+
if (!isPlainRecord(value)) return false;
|
|
1088
|
+
const keys = Object.keys(value);
|
|
1089
|
+
return keys.length === 1 && keys[0] === "seat" && Number.isSafeInteger(value.seat) && value.seat >= 0;
|
|
1090
|
+
}
|
|
1091
|
+
function resolveScenarioSeatRef(options) {
|
|
1092
|
+
if (!isScenarioSeatRef(options.ref)) {
|
|
1093
|
+
throw new ScenarioCommandParamsError(
|
|
1094
|
+
options.path,
|
|
1095
|
+
"expected a scenario seat reference"
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
const playerId = options.playerIds[options.ref.seat];
|
|
1099
|
+
if (playerId === void 0) {
|
|
1100
|
+
throw new ScenarioCommandParamsError(
|
|
1101
|
+
`${options.path}.seat`,
|
|
1102
|
+
`seat ${options.ref.seat} is out of range for ${options.playerIds.length} player(s)`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
return playerId;
|
|
1106
|
+
}
|
|
1107
|
+
function resolveScenarioCommandParams(options) {
|
|
1108
|
+
const candidateSchemas = clientParamSchemaCandidates(options);
|
|
1109
|
+
let firstSeatError;
|
|
1110
|
+
let firstIssue;
|
|
1111
|
+
for (const schema of candidateSchemas) {
|
|
1112
|
+
try {
|
|
1113
|
+
const resolved = resolveScenarioSeatReferences({
|
|
1114
|
+
schema,
|
|
1115
|
+
value: options.params,
|
|
1116
|
+
playerIds: options.playerIds,
|
|
1117
|
+
path: `${options.path}.params`
|
|
1118
|
+
});
|
|
1119
|
+
const parsed = schema.safeParse(resolved);
|
|
1120
|
+
if (parsed.success) {
|
|
1121
|
+
return parsed.data;
|
|
1122
|
+
}
|
|
1123
|
+
firstIssue ??= parsed.error.issues[0];
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
if (error instanceof ScenarioSchemaValueError) {
|
|
1126
|
+
firstSeatError ??= error;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
throw error;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if (firstSeatError) {
|
|
1133
|
+
throw new ScenarioCommandParamsError(
|
|
1134
|
+
firstSeatError.path,
|
|
1135
|
+
firstSeatError.message
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
const suffix = firstIssue?.path.map(
|
|
1139
|
+
(part) => typeof part === "number" ? `[${part}]` : `.${String(part)}`
|
|
1140
|
+
).join("") ?? "";
|
|
1141
|
+
throw new ScenarioCommandParamsError(
|
|
1142
|
+
`${options.path}.params${suffix}`,
|
|
1143
|
+
firstIssue?.message ?? "invalid interaction parameters"
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
function projectScenarioCommandParams(options) {
|
|
1147
|
+
const candidateSchemas = clientParamSchemaCandidates(options);
|
|
1148
|
+
let firstSeatError;
|
|
1149
|
+
for (const schema of candidateSchemas) {
|
|
1150
|
+
if (!schema.safeParse(options.params).success) continue;
|
|
1151
|
+
try {
|
|
1152
|
+
const projected = projectScenarioSeatReferences({
|
|
1153
|
+
schema,
|
|
1154
|
+
value: options.params,
|
|
1155
|
+
playerIds: options.playerIds,
|
|
1156
|
+
path: `${options.path}.params`
|
|
1157
|
+
});
|
|
1158
|
+
if (isPlainRecord(projected)) return projected;
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
if (error instanceof ScenarioSchemaValueError) {
|
|
1161
|
+
firstSeatError ??= error;
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
throw error;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (firstSeatError) {
|
|
1168
|
+
throw new ScenarioCommandParamsError(
|
|
1169
|
+
firstSeatError.path,
|
|
1170
|
+
firstSeatError.message
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
throw new ScenarioCommandParamsError(
|
|
1174
|
+
`${options.path}.params`,
|
|
1175
|
+
"runtime interaction parameters do not match a client parameter schema"
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
function clientParamSchemaCandidates(options) {
|
|
1179
|
+
const schemas = createClientParamSchemasByPhase(options.game);
|
|
1180
|
+
const currentPhaseSchema = schemas[options.phase]?.[options.interactionId];
|
|
1181
|
+
const candidateSchemas = [
|
|
1182
|
+
...currentPhaseSchema ? [currentPhaseSchema] : [],
|
|
1183
|
+
...Object.entries(schemas).flatMap(([phase, schemasForPhase]) => {
|
|
1184
|
+
if (phase === options.phase) return [];
|
|
1185
|
+
const schema = schemasForPhase[options.interactionId];
|
|
1186
|
+
return schema ? [schema] : [];
|
|
1187
|
+
})
|
|
1188
|
+
];
|
|
1189
|
+
if (candidateSchemas.length === 0) {
|
|
1190
|
+
throw new ScenarioCommandParamsError(
|
|
1191
|
+
`${options.path}.interactionId`,
|
|
1192
|
+
`interaction '${options.interactionId}' has no client parameter schema`
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
return candidateSchemas;
|
|
1196
|
+
}
|
|
1197
|
+
function isPlainRecord(value) {
|
|
1198
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1202
|
+
return prototype === Object.prototype || prototype === null;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/testing/scenario-replay.ts
|
|
1206
|
+
async function replayScenario(options) {
|
|
1207
|
+
validateReplayDefinition(options.game, options.scenario);
|
|
1208
|
+
const scenario = cloneReplayDefinition(options.scenario);
|
|
1209
|
+
const checkpoint = normalizeCheckpoint(scenario, options.at);
|
|
1210
|
+
const playerIds = resolvePlayerIds(options.game, scenario.setup.players);
|
|
1211
|
+
const events = [];
|
|
1212
|
+
const bundle = createScenarioBundle(options.game, events);
|
|
1213
|
+
const normalSetup = options.game.contract.manifest.normalSetup;
|
|
1214
|
+
if (!normalSetup) {
|
|
1215
|
+
throw new ScenarioDefinitionValidationError({
|
|
1216
|
+
code: "NORMAL_SETUP_UNAVAILABLE",
|
|
1217
|
+
path: "scenario.setup.players",
|
|
1218
|
+
reason: "the game manifest does not expose normal setup"
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
const table = normalSetup.createInitialTable({ playerIds });
|
|
1222
|
+
const reducerState = await bundle.initialize({
|
|
1223
|
+
table,
|
|
1224
|
+
playerIds: [...playerIds],
|
|
1225
|
+
rngSeed: scenario.setup.seed,
|
|
1226
|
+
setup: typeof scenario.setup.setupProfileId === "string" ? {
|
|
1227
|
+
profileId: scenario.setup.setupProfileId,
|
|
1228
|
+
optionValues: {}
|
|
1229
|
+
} : null
|
|
1230
|
+
});
|
|
1231
|
+
const runtime = new ScenarioReplayImplementation({
|
|
1232
|
+
game: options.game,
|
|
1233
|
+
scenario,
|
|
1234
|
+
playerIds,
|
|
1235
|
+
reducerState,
|
|
1236
|
+
checkpoint: { segment: "setup", completed: 0 },
|
|
1237
|
+
trace: [],
|
|
1238
|
+
events
|
|
1239
|
+
});
|
|
1240
|
+
await runtime.replayTo(checkpoint);
|
|
1241
|
+
return runtime;
|
|
1242
|
+
}
|
|
1243
|
+
async function materializeScenarioRuntimeCheckpoint(options) {
|
|
1244
|
+
const replay = requireScenarioReplayImplementation(
|
|
1245
|
+
await replayScenario(options)
|
|
1246
|
+
);
|
|
1247
|
+
return {
|
|
1248
|
+
checkpoint: structuredClone(replay.checkpoint),
|
|
1249
|
+
checkpointDigest: replay.checkpointDigest,
|
|
1250
|
+
playerIds: [...replay.playerIds],
|
|
1251
|
+
state: structuredClone(replay.reducerState)
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function cloneReplayDefinition(scenario) {
|
|
1255
|
+
return structuredClone({
|
|
1256
|
+
id: scenario.id,
|
|
1257
|
+
...scenario.description === void 0 ? {} : { description: scenario.description },
|
|
1258
|
+
setup: scenario.setup,
|
|
1259
|
+
given: scenario.given,
|
|
1260
|
+
when: scenario.when
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
async function assertScenario(options) {
|
|
1264
|
+
const replay = requireScenarioReplayImplementation(options.replay);
|
|
1265
|
+
if (!replay.complete) {
|
|
1266
|
+
throw new Error(
|
|
1267
|
+
`Scenario '${replay.scenarioId}' assertions require a full replay; checkpoint is ${replay.checkpoint.segment}:${replay.checkpoint.completed}.`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
const context = {
|
|
1271
|
+
expect: createExpectApi({
|
|
1272
|
+
lastDiagnosticRejection: () => lastRejectedEvent(replay.diagnostics)
|
|
1273
|
+
}),
|
|
1274
|
+
state: replay.state,
|
|
1275
|
+
view: replay.view,
|
|
1276
|
+
interactions: replay.interactions,
|
|
1277
|
+
explain: replay.explain,
|
|
1278
|
+
get diagnostics() {
|
|
1279
|
+
return replay.diagnostics;
|
|
1280
|
+
},
|
|
1281
|
+
probe: (command) => replay.probe(command)
|
|
1282
|
+
};
|
|
1283
|
+
await options.assertion(context);
|
|
1284
|
+
}
|
|
1285
|
+
async function probeScenarioCommand(options) {
|
|
1286
|
+
return requireScenarioReplayImplementation(options.replay).probe(
|
|
1287
|
+
options.command
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
var ScenarioReplayImplementation = class _ScenarioReplayImplementation {
|
|
1291
|
+
scenarioId;
|
|
1292
|
+
scenario;
|
|
1293
|
+
game;
|
|
1294
|
+
playerIds;
|
|
1295
|
+
bundle;
|
|
1296
|
+
events;
|
|
1297
|
+
checkpoint;
|
|
1298
|
+
reducerState;
|
|
1299
|
+
trace;
|
|
1300
|
+
constructor(state) {
|
|
1301
|
+
this.scenarioId = state.scenario.id;
|
|
1302
|
+
this.scenario = state.scenario;
|
|
1303
|
+
this.game = state.game;
|
|
1304
|
+
this.playerIds = [...state.playerIds];
|
|
1305
|
+
this.reducerState = structuredClone(state.reducerState);
|
|
1306
|
+
this.checkpoint = structuredClone(state.checkpoint);
|
|
1307
|
+
this.trace = structuredClone(
|
|
1308
|
+
state.trace
|
|
1309
|
+
);
|
|
1310
|
+
this.events = structuredClone(state.events);
|
|
1311
|
+
this.bundle = createScenarioBundle(state.game, this.events);
|
|
1312
|
+
this.state = this.state.bind(this);
|
|
1313
|
+
this.view = this.view.bind(this);
|
|
1314
|
+
this.interactions = this.interactions.bind(this);
|
|
1315
|
+
this.explain = this.explain.bind(this);
|
|
1316
|
+
}
|
|
1317
|
+
get checkpointDigest() {
|
|
1318
|
+
return digestPluginRuntimeJson({
|
|
1319
|
+
digestVersion: "scenario-checkpoint@1",
|
|
1320
|
+
scenarioId: this.scenarioId,
|
|
1321
|
+
checkpoint: this.checkpoint,
|
|
1322
|
+
reducerState: this.reducerState,
|
|
1323
|
+
trace: this.trace
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
get complete() {
|
|
1327
|
+
return this.checkpoint.segment === "when" && this.checkpoint.completed === this.scenario.when.length;
|
|
1328
|
+
}
|
|
1329
|
+
get diagnostics() {
|
|
1330
|
+
const flow = readFlowState(this.reducerState);
|
|
1331
|
+
const projection = this.project(this.playerIds[0]);
|
|
1332
|
+
return {
|
|
1333
|
+
events: structuredClone(this.events),
|
|
1334
|
+
lastDispatch: lastAcceptedDispatch(this.events),
|
|
1335
|
+
flow: {
|
|
1336
|
+
currentPhase: flow.currentPhase,
|
|
1337
|
+
currentStage: projection.currentStage ?? null,
|
|
1338
|
+
activeSeats: flow.activePlayers.flatMap((playerId) => {
|
|
1339
|
+
const seat = this.playerIds.indexOf(playerId);
|
|
1340
|
+
return seat < 0 ? [] : [{ seat }];
|
|
1341
|
+
})
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
state() {
|
|
1346
|
+
return structuredClone(this.reducerState.domain);
|
|
1347
|
+
}
|
|
1348
|
+
view(seat) {
|
|
1349
|
+
const playerId = this.playerId(seat, "seat");
|
|
1350
|
+
const projection = this.project(playerId);
|
|
1351
|
+
return structuredClone(projection.seats?.[playerId]?.view ?? null);
|
|
1352
|
+
}
|
|
1353
|
+
interactions(seat) {
|
|
1354
|
+
const playerId = this.playerId(seat, "seat");
|
|
1355
|
+
const projection = this.project(playerId);
|
|
1356
|
+
const refs = projection.seats?.[playerId]?.availableInteractionRefs ?? [];
|
|
1357
|
+
const interactionsByRef = projection.interactionsByRef ?? {};
|
|
1358
|
+
return structuredClone(
|
|
1359
|
+
refs.flatMap((ref) => {
|
|
1360
|
+
const descriptor = interactionsByRef[ref];
|
|
1361
|
+
return descriptor ? [descriptor] : [];
|
|
1362
|
+
})
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
explain(seat, interactionId) {
|
|
1366
|
+
return structuredClone(
|
|
1367
|
+
this.bundle.explainInteraction({
|
|
1368
|
+
state: this.reducerState,
|
|
1369
|
+
playerId: this.playerId(seat, "seat"),
|
|
1370
|
+
interactionId
|
|
1371
|
+
})
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
clone() {
|
|
1375
|
+
return new _ScenarioReplayImplementation({
|
|
1376
|
+
game: this.game,
|
|
1377
|
+
scenario: this.scenario,
|
|
1378
|
+
playerIds: this.playerIds,
|
|
1379
|
+
reducerState: this.reducerState,
|
|
1380
|
+
checkpoint: this.checkpoint,
|
|
1381
|
+
trace: this.trace,
|
|
1382
|
+
events: this.events
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
inspectionAuthority(perspective) {
|
|
1386
|
+
const selectedPlayerId = perspective.kind === "player" ? this.playerId({ seat: perspective.seat }, "perspective.seat") : null;
|
|
1387
|
+
const projection = this.bundle.projectSeatsDynamic({
|
|
1388
|
+
state: this.reducerState,
|
|
1389
|
+
playerIds: selectedPlayerId ? [selectedPlayerId] : []
|
|
1390
|
+
});
|
|
1391
|
+
const descriptors = selectedPlayerId ? descriptorsForPlayer(projection, selectedPlayerId) : [];
|
|
1392
|
+
const flow = readFlowState(this.reducerState);
|
|
1393
|
+
return {
|
|
1394
|
+
game: this.game,
|
|
1395
|
+
scenario: structuredClone(this.scenario),
|
|
1396
|
+
playerIds: [...this.playerIds],
|
|
1397
|
+
checkpoint: structuredClone(this.checkpoint),
|
|
1398
|
+
checkpointDigest: this.checkpointDigest,
|
|
1399
|
+
publicState: structuredClone(this.reducerState.domain.publicState),
|
|
1400
|
+
view: structuredClone(
|
|
1401
|
+
selectedPlayerId ? projection.seats[selectedPlayerId]?.view ?? null : projection.sharedView ?? null
|
|
1402
|
+
),
|
|
1403
|
+
scheduler: {
|
|
1404
|
+
phase: flow.currentPhase ?? "",
|
|
1405
|
+
step: readPhaseStep(this.reducerState.domain.phase),
|
|
1406
|
+
activePlayerIds: [
|
|
1407
|
+
...projection.schedulerFlow?.activePlayerIds ?? projection.stageSeats ?? []
|
|
1408
|
+
],
|
|
1409
|
+
pendingPlayerIds: [
|
|
1410
|
+
...projection.schedulerFlow?.pendingPlayerIds ?? projection.simultaneousPhase?.pendingPlayerIds ?? []
|
|
1411
|
+
],
|
|
1412
|
+
continuationDependencies: structuredClone(
|
|
1413
|
+
projection.schedulerFlow?.continuationDependencies ?? []
|
|
1414
|
+
)
|
|
1415
|
+
},
|
|
1416
|
+
interactions: descriptors.map((descriptor) => ({
|
|
1417
|
+
descriptor: structuredClone(descriptor),
|
|
1418
|
+
explanation: structuredClone(
|
|
1419
|
+
this.bundle.explainInteraction({
|
|
1420
|
+
state: this.reducerState,
|
|
1421
|
+
playerId: selectedPlayerId,
|
|
1422
|
+
interactionId: String(descriptor.interactionId ?? "")
|
|
1423
|
+
})
|
|
1424
|
+
),
|
|
1425
|
+
actionability: structuredClone(
|
|
1426
|
+
this.bundle.resolveInteractionActionability({
|
|
1427
|
+
state: this.reducerState,
|
|
1428
|
+
playerId: selectedPlayerId,
|
|
1429
|
+
interactionId: String(descriptor.interactionId ?? "")
|
|
1430
|
+
})
|
|
1431
|
+
)
|
|
1432
|
+
})),
|
|
1433
|
+
entropy: {
|
|
1434
|
+
seed: this.reducerState.runtime.rng.seed,
|
|
1435
|
+
draws: structuredClone(this.reducerState.runtime.rng.draws ?? [])
|
|
1436
|
+
},
|
|
1437
|
+
commandTrace: structuredClone(this.trace)
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
enumerateInteractionParams(options) {
|
|
1441
|
+
return structuredClone(
|
|
1442
|
+
this.bundle.enumerateInteractionParams({
|
|
1443
|
+
state: this.reducerState,
|
|
1444
|
+
playerId: this.playerId({ seat: options.seat }, "perspective.seat"),
|
|
1445
|
+
interactionId: options.interactionId,
|
|
1446
|
+
maxEvaluations: options.maxEvaluations
|
|
1447
|
+
})
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
async advance(command) {
|
|
1451
|
+
const clone = new _ScenarioReplayImplementation({
|
|
1452
|
+
game: this.game,
|
|
1453
|
+
scenario: this.scenario,
|
|
1454
|
+
playerIds: this.playerIds,
|
|
1455
|
+
reducerState: this.reducerState,
|
|
1456
|
+
checkpoint: this.checkpoint,
|
|
1457
|
+
trace: this.trace,
|
|
1458
|
+
events: this.events
|
|
1459
|
+
});
|
|
1460
|
+
const result = await clone.dispatch(command, "probe", 0);
|
|
1461
|
+
if (result.kind === "accept") {
|
|
1462
|
+
clone.trace.push({
|
|
1463
|
+
segment: this.checkpoint.segment === "when" ? "when" : "given",
|
|
1464
|
+
index: this.checkpoint.completed,
|
|
1465
|
+
command: structuredClone(command),
|
|
1466
|
+
trace: structuredClone(result.trace)
|
|
1467
|
+
});
|
|
1468
|
+
return {
|
|
1469
|
+
kind: "accepted",
|
|
1470
|
+
replay: clone,
|
|
1471
|
+
trace: structuredClone(result.trace)
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
return {
|
|
1475
|
+
kind: "rejected",
|
|
1476
|
+
errorCode: result.errorCode,
|
|
1477
|
+
...result.message === void 0 ? {} : { message: result.message },
|
|
1478
|
+
trace: structuredClone(result.trace)
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
async replayTo(target) {
|
|
1482
|
+
const givenCount = target.segment === "setup" ? 0 : target.segment === "given" ? target.completed : this.scenario.given.length;
|
|
1483
|
+
const whenCount = target.segment === "when" ? target.completed : 0;
|
|
1484
|
+
for (let index = 0; index < givenCount; index += 1) {
|
|
1485
|
+
await this.dispatchRequired("given", index, this.scenario.given[index]);
|
|
1486
|
+
}
|
|
1487
|
+
this.checkpoint = { segment: "given", completed: givenCount };
|
|
1488
|
+
for (let index = 0; index < whenCount; index += 1) {
|
|
1489
|
+
await this.dispatchRequired("when", index, this.scenario.when[index]);
|
|
1490
|
+
}
|
|
1491
|
+
this.checkpoint = structuredClone(target);
|
|
1492
|
+
}
|
|
1493
|
+
async probe(command) {
|
|
1494
|
+
const clone = requireScenarioReplayImplementation(this.clone());
|
|
1495
|
+
const result = await clone.dispatch(command, "probe", 0);
|
|
1496
|
+
if (result.kind === "accept") {
|
|
1497
|
+
const accepted = {
|
|
1498
|
+
kind: "accepted",
|
|
1499
|
+
command,
|
|
1500
|
+
checkpointDigest: digestPluginRuntimeJson({
|
|
1501
|
+
digestVersion: "scenario-probe@1",
|
|
1502
|
+
from: this.checkpointDigest,
|
|
1503
|
+
command,
|
|
1504
|
+
reducerState: clone.reducerState,
|
|
1505
|
+
trace: result.trace
|
|
1506
|
+
}),
|
|
1507
|
+
trace: result.trace,
|
|
1508
|
+
toBeAccepted() {
|
|
1509
|
+
},
|
|
1510
|
+
toRejectWith(expected) {
|
|
1511
|
+
scenarioAssertionFailure(
|
|
1512
|
+
`Expected probe to reject${expected.errorCode ? ` with ${expected.errorCode}` : ""}, but it was accepted.`
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
return accepted;
|
|
1517
|
+
}
|
|
1518
|
+
const rejected = {
|
|
1519
|
+
kind: "rejected",
|
|
1520
|
+
command,
|
|
1521
|
+
errorCode: result.errorCode,
|
|
1522
|
+
...result.message === void 0 ? {} : { message: result.message },
|
|
1523
|
+
trace: result.trace,
|
|
1524
|
+
toBeAccepted() {
|
|
1525
|
+
scenarioAssertionFailure(
|
|
1526
|
+
`Expected probe to be accepted, but it rejected with ${result.errorCode}.`
|
|
1527
|
+
);
|
|
1528
|
+
},
|
|
1529
|
+
toRejectWith(expected) {
|
|
1530
|
+
assertRejectionMatches(result, expected);
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
return rejected;
|
|
1534
|
+
}
|
|
1535
|
+
async dispatchRequired(segment, index, command) {
|
|
1536
|
+
const result = await this.dispatch(command, segment, index);
|
|
1537
|
+
if (result.kind === "reject") {
|
|
1538
|
+
throw new ScenarioReplayError({
|
|
1539
|
+
scenarioId: this.scenarioId,
|
|
1540
|
+
segment,
|
|
1541
|
+
index,
|
|
1542
|
+
interactionId: command.interactionId,
|
|
1543
|
+
errorCode: result.errorCode,
|
|
1544
|
+
reducerMessage: result.message,
|
|
1545
|
+
trace: result.trace
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
this.trace.push({
|
|
1549
|
+
segment,
|
|
1550
|
+
index,
|
|
1551
|
+
command: structuredClone(command),
|
|
1552
|
+
trace: result.trace
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
async dispatch(command, segment, index) {
|
|
1556
|
+
const phase = readFlowState(this.reducerState).currentPhase ?? "";
|
|
1557
|
+
const playerId = resolveScenarioSeatRef({
|
|
1558
|
+
ref: command.actor,
|
|
1559
|
+
playerIds: this.playerIds,
|
|
1560
|
+
path: `scenario.${segment}[${index}].actor`
|
|
1561
|
+
});
|
|
1562
|
+
const params = resolveScenarioCommandParams({
|
|
1563
|
+
game: this.game,
|
|
1564
|
+
phase,
|
|
1565
|
+
interactionId: command.interactionId,
|
|
1566
|
+
params: command.params,
|
|
1567
|
+
playerIds: this.playerIds,
|
|
1568
|
+
path: `scenario.${segment}[${index}]`
|
|
1569
|
+
});
|
|
1570
|
+
const result = await this.bundle.dispatch({
|
|
1571
|
+
state: this.reducerState,
|
|
1572
|
+
input: {
|
|
1573
|
+
kind: "interaction",
|
|
1574
|
+
playerId,
|
|
1575
|
+
interactionId: command.interactionId,
|
|
1576
|
+
params
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
if (result.kind === "reject") {
|
|
1580
|
+
return {
|
|
1581
|
+
kind: "reject",
|
|
1582
|
+
errorCode: result.errorCode,
|
|
1583
|
+
...result.message === void 0 ? {} : { message: result.message },
|
|
1584
|
+
trace: []
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
this.reducerState = structuredClone(result.state);
|
|
1588
|
+
return {
|
|
1589
|
+
kind: "accept",
|
|
1590
|
+
trace: summarizeWireTrace(result.trace)
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
playerId(seat, path) {
|
|
1594
|
+
return resolveScenarioSeatRef({
|
|
1595
|
+
ref: seat,
|
|
1596
|
+
playerIds: this.playerIds,
|
|
1597
|
+
path
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
project(playerId) {
|
|
1601
|
+
return this.bundle.projectSeatsDynamic({
|
|
1602
|
+
state: this.reducerState,
|
|
1603
|
+
playerIds: [playerId]
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
function inspectScenarioReplayAuthority(options) {
|
|
1608
|
+
return requireScenarioReplayImplementation(
|
|
1609
|
+
options.replay
|
|
1610
|
+
).inspectionAuthority(options.perspective);
|
|
1611
|
+
}
|
|
1612
|
+
function enumerateScenarioInteractionParams(options) {
|
|
1613
|
+
return requireScenarioReplayImplementation(
|
|
1614
|
+
options.replay
|
|
1615
|
+
).enumerateInteractionParams(options);
|
|
1616
|
+
}
|
|
1617
|
+
function advanceScenarioReplay(options) {
|
|
1618
|
+
return requireScenarioReplayImplementation(options.replay).advance(
|
|
1619
|
+
options.command
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
function descriptorsForPlayer(projection, playerId) {
|
|
1623
|
+
const refs = projection.seats[playerId]?.availableInteractionRefs ?? [];
|
|
1624
|
+
const interactionsByRef = projection.interactionsByRef ?? {};
|
|
1625
|
+
return refs.flatMap((ref) => {
|
|
1626
|
+
const descriptor = interactionsByRef[ref];
|
|
1627
|
+
return descriptor ? [descriptor] : [];
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
function createScenarioBundle(game, events) {
|
|
1631
|
+
return createReducerBundle(game, {
|
|
1632
|
+
diagnostics: {
|
|
1633
|
+
event(event) {
|
|
1634
|
+
events.push(structuredClone(event));
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
function validateReplayDefinition(game, replay) {
|
|
1640
|
+
validateScenarioDefinition(game, {
|
|
1641
|
+
...replay,
|
|
1642
|
+
then: () => void 0
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
function resolvePlayerIds(game, count) {
|
|
1646
|
+
const declared = game.contract.manifest.literals.playerIds;
|
|
1647
|
+
return Array.from(
|
|
1648
|
+
{ length: count },
|
|
1649
|
+
(_, index) => declared[index] ?? `player-${index + 1}`
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
function normalizeCheckpoint(scenario, checkpoint) {
|
|
1653
|
+
if (checkpoint === void 0) {
|
|
1654
|
+
return { segment: "when", completed: scenario.when.length };
|
|
1655
|
+
}
|
|
1656
|
+
const maximum = checkpoint.segment === "setup" ? 0 : checkpoint.segment === "given" ? scenario.given.length : scenario.when.length;
|
|
1657
|
+
if (!Number.isSafeInteger(checkpoint.completed) || checkpoint.completed < 0 || checkpoint.completed > maximum || checkpoint.segment === "setup" && checkpoint.completed !== 0) {
|
|
1658
|
+
throw new ScenarioDefinitionValidationError({
|
|
1659
|
+
code: "OUT_OF_RANGE",
|
|
1660
|
+
path: "at.completed",
|
|
1661
|
+
reason: `expected 0 through ${maximum} for ${checkpoint.segment}`
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
return structuredClone(checkpoint);
|
|
1665
|
+
}
|
|
1666
|
+
function readFlowState(state) {
|
|
1667
|
+
const flow = state.domain?.flow;
|
|
1668
|
+
if (typeof flow !== "object" || flow === null) {
|
|
1669
|
+
return { currentPhase: null, activePlayers: [] };
|
|
1670
|
+
}
|
|
1671
|
+
const record = flow;
|
|
1672
|
+
return {
|
|
1673
|
+
currentPhase: typeof record.currentPhase === "string" ? record.currentPhase : null,
|
|
1674
|
+
activePlayers: Array.isArray(record.activePlayers) ? record.activePlayers.filter(
|
|
1675
|
+
(playerId) => typeof playerId === "string"
|
|
1676
|
+
) : []
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function readPhaseStep(phase) {
|
|
1680
|
+
if (!phase || typeof phase !== "object") return null;
|
|
1681
|
+
const step = phase.step;
|
|
1682
|
+
return typeof step === "string" ? step : null;
|
|
1683
|
+
}
|
|
1684
|
+
function summarizeWireTrace(trace) {
|
|
1685
|
+
return trace.flatMap((entry) => {
|
|
1686
|
+
switch (entry.kind) {
|
|
1687
|
+
case "acceptedClientInput":
|
|
1688
|
+
return [
|
|
1689
|
+
{
|
|
1690
|
+
kind: "acceptedClientInput",
|
|
1691
|
+
interactionId: entry.input.interactionId,
|
|
1692
|
+
playerId: entry.input.playerId
|
|
1693
|
+
}
|
|
1694
|
+
];
|
|
1695
|
+
case "appliedEffect":
|
|
1696
|
+
return [
|
|
1697
|
+
{
|
|
1698
|
+
kind: "appliedInstruction",
|
|
1699
|
+
instruction: String(
|
|
1700
|
+
entry.effect.kind ?? entry.effect.type ?? "effect"
|
|
1701
|
+
)
|
|
1702
|
+
}
|
|
1703
|
+
];
|
|
1704
|
+
case "rngConsumption":
|
|
1705
|
+
return [
|
|
1706
|
+
{
|
|
1707
|
+
kind: "rngConsumption",
|
|
1708
|
+
version: entry.version,
|
|
1709
|
+
operation: entry.operation,
|
|
1710
|
+
drawIndex: entry.drawIndex
|
|
1711
|
+
}
|
|
1712
|
+
];
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
function lastAcceptedDispatch(events) {
|
|
1717
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
1718
|
+
const event = events[index];
|
|
1719
|
+
if (event?.type === "submitAccepted") {
|
|
1720
|
+
return {
|
|
1721
|
+
submissionId: event.submissionId,
|
|
1722
|
+
trace: structuredClone(event.trace)
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return null;
|
|
1727
|
+
}
|
|
1728
|
+
function lastRejectedEvent(diagnostics) {
|
|
1729
|
+
for (let index = diagnostics.events.length - 1; index >= 0; index -= 1) {
|
|
1730
|
+
const event = diagnostics.events[index];
|
|
1731
|
+
if (event?.type === "submitRejected") return event;
|
|
1732
|
+
}
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
function assertRejectionMatches(rejection, expected) {
|
|
1736
|
+
if (expected.errorCode !== void 0 && rejection.errorCode !== expected.errorCode) {
|
|
1737
|
+
scenarioAssertionFailure(
|
|
1738
|
+
`Expected rejection errorCode '${expected.errorCode}', received '${rejection.errorCode}'.`
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
if (typeof expected.message === "string" && rejection.message !== expected.message) {
|
|
1742
|
+
scenarioAssertionFailure(
|
|
1743
|
+
`Expected rejection message '${expected.message}', received '${rejection.message ?? "undefined"}'.`
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
if (expected.message instanceof RegExp && !expected.message.test(rejection.message ?? "")) {
|
|
1747
|
+
scenarioAssertionFailure(
|
|
1748
|
+
`Expected rejection message '${rejection.message ?? ""}' to match ${String(expected.message)}.`
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
function requireScenarioReplayImplementation(replay) {
|
|
1753
|
+
if (!(replay instanceof ScenarioReplayImplementation)) {
|
|
1754
|
+
throw new Error(
|
|
1755
|
+
"Scenario replay was not created by replayScenario(); alternate replay implementations are not supported."
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
return replay;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// src/testing/canonical.ts
|
|
1762
|
+
function canonicalScenarioJson(value) {
|
|
1763
|
+
return encodeCanonicalPluginRuntimeJson(value);
|
|
1764
|
+
}
|
|
1765
|
+
function digestScenarioJson(value) {
|
|
1766
|
+
return digestPluginRuntimeJson(value);
|
|
1767
|
+
}
|
|
1768
|
+
function compareCanonicalScenarioJson(left, right) {
|
|
1769
|
+
const leftJson = canonicalScenarioJson(left);
|
|
1770
|
+
const rightJson = canonicalScenarioJson(right);
|
|
1771
|
+
return leftJson === rightJson ? 0 : leftJson < rightJson ? -1 : 1;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
export {
|
|
1775
|
+
ScenarioDefinitionValidationError,
|
|
1776
|
+
ScenarioReplayError,
|
|
1777
|
+
createScenarioAuthoring,
|
|
1778
|
+
toScenarioReplayDefinition,
|
|
1779
|
+
ScenarioAssertionError,
|
|
1780
|
+
createExpectApi,
|
|
1781
|
+
resolveScenarioCommandParams,
|
|
1782
|
+
projectScenarioCommandParams,
|
|
1783
|
+
replayScenario,
|
|
1784
|
+
materializeScenarioRuntimeCheckpoint,
|
|
1785
|
+
assertScenario,
|
|
1786
|
+
probeScenarioCommand,
|
|
1787
|
+
inspectScenarioReplayAuthority,
|
|
1788
|
+
enumerateScenarioInteractionParams,
|
|
1789
|
+
advanceScenarioReplay,
|
|
1790
|
+
digestScenarioJson,
|
|
1791
|
+
compareCanonicalScenarioJson
|
|
1792
|
+
};
|
|
1793
|
+
//# sourceMappingURL=chunk-L3W2FPRQ.js.map
|