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