@series-inc/rundot-syncplay 5.23.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/README.md +610 -0
  2. package/dist/animation.d.ts +119 -0
  3. package/dist/animation.js +488 -0
  4. package/dist/asset-cooking.d.ts +82 -0
  5. package/dist/asset-cooking.js +364 -0
  6. package/dist/authority-room.d.ts +217 -0
  7. package/dist/authority-room.js +701 -0
  8. package/dist/bot-documents.d.ts +115 -0
  9. package/dist/bot-documents.js +549 -0
  10. package/dist/bot-primitives.d.ts +152 -0
  11. package/dist/bot-primitives.js +472 -0
  12. package/dist/browser.d.ts +57 -0
  13. package/dist/browser.js +53 -0
  14. package/dist/canonical.d.ts +12 -0
  15. package/dist/canonical.js +288 -0
  16. package/dist/certification.d.ts +455 -0
  17. package/dist/certification.js +1855 -0
  18. package/dist/chaos-certification.d.ts +56 -0
  19. package/dist/chaos-certification.js +1397 -0
  20. package/dist/cjs/animation.js +495 -0
  21. package/dist/cjs/asset-cooking.js +372 -0
  22. package/dist/cjs/authority-room.js +704 -0
  23. package/dist/cjs/bot-documents.js +556 -0
  24. package/dist/cjs/bot-primitives.js +482 -0
  25. package/dist/cjs/browser.js +96 -0
  26. package/dist/cjs/canonical.js +299 -0
  27. package/dist/cjs/certification.js +1860 -0
  28. package/dist/cjs/chaos-certification.js +1402 -0
  29. package/dist/cjs/collections.js +36 -0
  30. package/dist/cjs/collision.js +96 -0
  31. package/dist/cjs/commands.js +987 -0
  32. package/dist/cjs/config-bundle.js +491 -0
  33. package/dist/cjs/creator.js +30 -0
  34. package/dist/cjs/deterministic-primitives.js +134 -0
  35. package/dist/cjs/deterministic-session.js +1410 -0
  36. package/dist/cjs/ecs-lite.js +155 -0
  37. package/dist/cjs/effects-adapter.js +97 -0
  38. package/dist/cjs/engine-complete.js +2297 -0
  39. package/dist/cjs/engine-core-proof.js +70 -0
  40. package/dist/cjs/engine-parity-matrix.js +874 -0
  41. package/dist/cjs/engine-parity.js +3183 -0
  42. package/dist/cjs/engine-runtime.js +3141 -0
  43. package/dist/cjs/errors.js +22 -0
  44. package/dist/cjs/events.js +247 -0
  45. package/dist/cjs/examples/counter-duel.js +157 -0
  46. package/dist/cjs/examples/fixed-arena.js +368 -0
  47. package/dist/cjs/index.js +229 -0
  48. package/dist/cjs/input-authority.js +150 -0
  49. package/dist/cjs/input-button.js +256 -0
  50. package/dist/cjs/input-timeline.js +418 -0
  51. package/dist/cjs/instant-replay.js +66 -0
  52. package/dist/cjs/lag-compensation3d.js +837 -0
  53. package/dist/cjs/local-authority-server-runner.js +1121 -0
  54. package/dist/cjs/local-authority-wire.js +1497 -0
  55. package/dist/cjs/local-authority.js +817 -0
  56. package/dist/cjs/local-ws-adapter.js +150 -0
  57. package/dist/cjs/match-log.js +61 -0
  58. package/dist/cjs/math.js +157 -0
  59. package/dist/cjs/mock-network-adapter.js +26 -0
  60. package/dist/cjs/movement.js +703 -0
  61. package/dist/cjs/movement3d.js +1246 -0
  62. package/dist/cjs/multiclient-authority.js +308 -0
  63. package/dist/cjs/multiplayer-service.js +434 -0
  64. package/dist/cjs/navigation.js +347 -0
  65. package/dist/cjs/navmesh.js +1437 -0
  66. package/dist/cjs/networked-client.js +691 -0
  67. package/dist/cjs/networked-input-authority.js +443 -0
  68. package/dist/cjs/node.js +47 -0
  69. package/dist/cjs/number-lab.js +78 -0
  70. package/dist/cjs/offline-session.js +370 -0
  71. package/dist/cjs/package.json +1 -0
  72. package/dist/cjs/pause-resume.js +43 -0
  73. package/dist/cjs/physics-cert.js +426 -0
  74. package/dist/cjs/physics2d.js +751 -0
  75. package/dist/cjs/physics3d.js +1996 -0
  76. package/dist/cjs/prediction-culling.js +203 -0
  77. package/dist/cjs/protocol-lab.js +1299 -0
  78. package/dist/cjs/r3f-render-adapter.js +209 -0
  79. package/dist/cjs/random.js +59 -0
  80. package/dist/cjs/replay-bundle.js +1598 -0
  81. package/dist/cjs/replay-inspect.js +236 -0
  82. package/dist/cjs/replay-scoring.js +68 -0
  83. package/dist/cjs/replay.js +356 -0
  84. package/dist/cjs/reports.js +181 -0
  85. package/dist/cjs/rollback-history.js +251 -0
  86. package/dist/cjs/runtime-guards.js +111 -0
  87. package/dist/cjs/sample-lab.js +145 -0
  88. package/dist/cjs/sample-scenes.js +752 -0
  89. package/dist/cjs/schema-artifacts.js +359 -0
  90. package/dist/cjs/schema-codegen.js +544 -0
  91. package/dist/cjs/schema-dsl-binary.js +655 -0
  92. package/dist/cjs/schema.js +319 -0
  93. package/dist/cjs/sdk-offline.js +99 -0
  94. package/dist/cjs/session-promotion.js +74 -0
  95. package/dist/cjs/session-snapshot.js +265 -0
  96. package/dist/cjs/session-wire.js +216 -0
  97. package/dist/cjs/signals.js +103 -0
  98. package/dist/cjs/sparse-set.js +169 -0
  99. package/dist/cjs/synctest.js +136 -0
  100. package/dist/cjs/system-lifecycle.js +286 -0
  101. package/dist/cjs/timers.js +61 -0
  102. package/dist/cjs/tools/static-checker.js +1468 -0
  103. package/dist/cjs/types.js +2 -0
  104. package/dist/cjs/venus-mp-budget.js +125 -0
  105. package/dist/cjs/ws-frame.js +255 -0
  106. package/dist/cli.d.ts +2 -0
  107. package/dist/cli.js +11379 -0
  108. package/dist/collections.d.ts +9 -0
  109. package/dist/collections.js +30 -0
  110. package/dist/collision.d.ts +25 -0
  111. package/dist/collision.js +85 -0
  112. package/dist/commands.d.ts +137 -0
  113. package/dist/commands.js +977 -0
  114. package/dist/config-bundle.d.ts +89 -0
  115. package/dist/config-bundle.js +483 -0
  116. package/dist/creator.d.ts +20 -0
  117. package/dist/creator.js +11 -0
  118. package/dist/deterministic-primitives.d.ts +59 -0
  119. package/dist/deterministic-primitives.js +126 -0
  120. package/dist/deterministic-session.d.ts +197 -0
  121. package/dist/deterministic-session.js +1401 -0
  122. package/dist/ecs-lite.d.ts +26 -0
  123. package/dist/ecs-lite.js +152 -0
  124. package/dist/effects-adapter.d.ts +21 -0
  125. package/dist/effects-adapter.js +94 -0
  126. package/dist/engine-complete.d.ts +45 -0
  127. package/dist/engine-complete.js +2292 -0
  128. package/dist/engine-core-proof.d.ts +26 -0
  129. package/dist/engine-core-proof.js +66 -0
  130. package/dist/engine-parity-matrix.d.ts +300 -0
  131. package/dist/engine-parity-matrix.js +869 -0
  132. package/dist/engine-parity.d.ts +74 -0
  133. package/dist/engine-parity.js +3178 -0
  134. package/dist/engine-runtime.d.ts +351 -0
  135. package/dist/engine-runtime.js +3123 -0
  136. package/dist/errors.d.ts +11 -0
  137. package/dist/errors.js +17 -0
  138. package/dist/events.d.ts +45 -0
  139. package/dist/events.js +242 -0
  140. package/dist/examples/counter-duel.d.ts +43 -0
  141. package/dist/examples/counter-duel.js +150 -0
  142. package/dist/examples/fixed-arena.d.ts +110 -0
  143. package/dist/examples/fixed-arena.js +356 -0
  144. package/dist/index.d.ts +101 -0
  145. package/dist/index.js +66 -0
  146. package/dist/input-authority.d.ts +82 -0
  147. package/dist/input-authority.js +147 -0
  148. package/dist/input-button.d.ts +36 -0
  149. package/dist/input-button.js +249 -0
  150. package/dist/input-timeline.d.ts +65 -0
  151. package/dist/input-timeline.js +412 -0
  152. package/dist/instant-replay.d.ts +59 -0
  153. package/dist/instant-replay.js +62 -0
  154. package/dist/lag-compensation3d.d.ts +146 -0
  155. package/dist/lag-compensation3d.js +827 -0
  156. package/dist/local-authority-server-runner.d.ts +79 -0
  157. package/dist/local-authority-server-runner.js +1118 -0
  158. package/dist/local-authority-wire.d.ts +142 -0
  159. package/dist/local-authority-wire.js +1477 -0
  160. package/dist/local-authority.d.ts +160 -0
  161. package/dist/local-authority.js +810 -0
  162. package/dist/local-ws-adapter.d.ts +14 -0
  163. package/dist/local-ws-adapter.js +146 -0
  164. package/dist/match-log.d.ts +74 -0
  165. package/dist/match-log.js +57 -0
  166. package/dist/math.d.ts +41 -0
  167. package/dist/math.js +154 -0
  168. package/dist/mock-network-adapter.d.ts +13 -0
  169. package/dist/mock-network-adapter.js +23 -0
  170. package/dist/movement.d.ts +155 -0
  171. package/dist/movement.js +694 -0
  172. package/dist/movement3d.d.ts +195 -0
  173. package/dist/movement3d.js +1238 -0
  174. package/dist/multiclient-authority.d.ts +30 -0
  175. package/dist/multiclient-authority.js +305 -0
  176. package/dist/multiplayer-service.d.ts +153 -0
  177. package/dist/multiplayer-service.js +429 -0
  178. package/dist/navigation.d.ts +91 -0
  179. package/dist/navigation.js +338 -0
  180. package/dist/navmesh.d.ts +236 -0
  181. package/dist/navmesh.js +1423 -0
  182. package/dist/networked-client.d.ts +124 -0
  183. package/dist/networked-client.js +688 -0
  184. package/dist/networked-input-authority.d.ts +51 -0
  185. package/dist/networked-input-authority.js +439 -0
  186. package/dist/node.d.ts +22 -0
  187. package/dist/node.js +19 -0
  188. package/dist/number-lab.d.ts +15 -0
  189. package/dist/number-lab.js +75 -0
  190. package/dist/offline-session.d.ts +2 -0
  191. package/dist/offline-session.js +367 -0
  192. package/dist/pause-resume.d.ts +48 -0
  193. package/dist/pause-resume.js +40 -0
  194. package/dist/physics-cert.d.ts +40 -0
  195. package/dist/physics-cert.js +389 -0
  196. package/dist/physics2d.d.ts +142 -0
  197. package/dist/physics2d.js +741 -0
  198. package/dist/physics3d.d.ts +285 -0
  199. package/dist/physics3d.js +1979 -0
  200. package/dist/prediction-culling.d.ts +34 -0
  201. package/dist/prediction-culling.js +200 -0
  202. package/dist/protocol-lab.d.ts +115 -0
  203. package/dist/protocol-lab.js +1295 -0
  204. package/dist/r3f-render-adapter.d.ts +61 -0
  205. package/dist/r3f-render-adapter.js +203 -0
  206. package/dist/random.d.ts +13 -0
  207. package/dist/random.js +55 -0
  208. package/dist/replay-bundle.d.ts +150 -0
  209. package/dist/replay-bundle.js +1586 -0
  210. package/dist/replay-inspect.d.ts +60 -0
  211. package/dist/replay-inspect.js +228 -0
  212. package/dist/replay-scoring.d.ts +28 -0
  213. package/dist/replay-scoring.js +65 -0
  214. package/dist/replay.d.ts +17 -0
  215. package/dist/replay.js +351 -0
  216. package/dist/reports.d.ts +176 -0
  217. package/dist/reports.js +174 -0
  218. package/dist/rollback-history.d.ts +46 -0
  219. package/dist/rollback-history.js +247 -0
  220. package/dist/runtime-guards.d.ts +1 -0
  221. package/dist/runtime-guards.js +108 -0
  222. package/dist/sample-lab.d.ts +32 -0
  223. package/dist/sample-lab.js +134 -0
  224. package/dist/sample-scenes.d.ts +86 -0
  225. package/dist/sample-scenes.js +745 -0
  226. package/dist/schema-artifacts.d.ts +49 -0
  227. package/dist/schema-artifacts.js +356 -0
  228. package/dist/schema-codegen.d.ts +21 -0
  229. package/dist/schema-codegen.js +538 -0
  230. package/dist/schema-dsl-binary.d.ts +75 -0
  231. package/dist/schema-dsl-binary.js +648 -0
  232. package/dist/schema.d.ts +87 -0
  233. package/dist/schema.js +315 -0
  234. package/dist/sdk-offline.d.ts +34 -0
  235. package/dist/sdk-offline.js +96 -0
  236. package/dist/session-promotion.d.ts +26 -0
  237. package/dist/session-promotion.js +70 -0
  238. package/dist/session-snapshot.d.ts +95 -0
  239. package/dist/session-snapshot.js +255 -0
  240. package/dist/session-wire.d.ts +89 -0
  241. package/dist/session-wire.js +211 -0
  242. package/dist/signals.d.ts +31 -0
  243. package/dist/signals.js +99 -0
  244. package/dist/sparse-set.d.ts +34 -0
  245. package/dist/sparse-set.js +165 -0
  246. package/dist/synctest.d.ts +31 -0
  247. package/dist/synctest.js +133 -0
  248. package/dist/system-lifecycle.d.ts +72 -0
  249. package/dist/system-lifecycle.js +281 -0
  250. package/dist/timers.d.ts +17 -0
  251. package/dist/timers.js +53 -0
  252. package/dist/tools/index.d.ts +4 -0
  253. package/dist/tools/index.js +4 -0
  254. package/dist/tools/static-checker.d.ts +31 -0
  255. package/dist/tools/static-checker.js +1462 -0
  256. package/dist/tools/vite-plugin.d.ts +69 -0
  257. package/dist/tools/vite-plugin.js +203 -0
  258. package/dist/types.d.ts +148 -0
  259. package/dist/types.js +1 -0
  260. package/dist/venus-mp-budget.d.ts +29 -0
  261. package/dist/venus-mp-budget.js +122 -0
  262. package/dist/ws-frame.d.ts +49 -0
  263. package/dist/ws-frame.js +247 -0
  264. package/package.json +169 -0
@@ -0,0 +1,1402 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.explainChaosFixture = explainChaosFixture;
4
+ exports.runChaosCertificationSuite = runChaosCertificationSuite;
5
+ exports.createChaosReplayBundleArtifacts = createChaosReplayBundleArtifacts;
6
+ const canonical_js_1 = require("./canonical.js");
7
+ const asset_cooking_js_1 = require("./asset-cooking.js");
8
+ const certification_js_1 = require("./certification.js");
9
+ const deterministic_primitives_js_1 = require("./deterministic-primitives.js");
10
+ const events_js_1 = require("./events.js");
11
+ const physics_cert_js_1 = require("./physics-cert.js");
12
+ const protocol_lab_js_1 = require("./protocol-lab.js");
13
+ const random_js_1 = require("./random.js");
14
+ const replay_inspect_js_1 = require("./replay-inspect.js");
15
+ const replay_bundle_js_1 = require("./replay-bundle.js");
16
+ const replay_js_1 = require("./replay.js");
17
+ const sample_lab_js_1 = require("./sample-lab.js");
18
+ const artifactRoot = 'artifacts/certification/chaos';
19
+ const fixtureDefinitions = [
20
+ { fixtureId: 'resync.snapshot-tail', category: 'resync', run: runResyncSnapshotTail },
21
+ { fixtureId: 'resync.late-join-snapshot-tail', category: 'resync', run: runResyncLateJoinSnapshotTail },
22
+ { fixtureId: 'resync.spectator-reconstruction', category: 'resync', run: runResyncSpectatorReconstruction },
23
+ { fixtureId: 'resync.long-disconnect', category: 'resync', run: runResyncLongDisconnect },
24
+ { fixtureId: 'resync.short-disconnect', category: 'resync', run: runResyncShortDisconnect },
25
+ { fixtureId: 'rollback-boundary.snapshot-buffer', category: 'rollback-boundary', run: runRollbackBoundary },
26
+ { fixtureId: 'input-policy.player-counts', category: 'input-policy', run: runInputPolicy },
27
+ { fixtureId: 'input-policy.stale-future-pruned', category: 'input-policy', run: runInputFrameRejectionPolicy },
28
+ { fixtureId: 'network.virtual-chaos', category: 'network', run: runNetworkChaos },
29
+ { fixtureId: 'hostile.malformed-envelope', category: 'hostile', run: runHostileProtocol },
30
+ { fixtureId: 'commands.idempotency', category: 'commands', run: runCommandIdempotency },
31
+ { fixtureId: 'events.exactly-once', category: 'events', run: runEventSemantics },
32
+ { fixtureId: 'replay-identity.version-schema', category: 'replay-identity', run: runReplayIdentity },
33
+ { fixtureId: 'replay-identity.first-divergence', category: 'replay-identity', run: runReplayDebugging },
34
+ { fixtureId: 'random.named-stream-rollback', category: 'random', run: runRandomRollback },
35
+ { fixtureId: 'tick.catch-up-boundary', category: 'tick', run: runTickBoundary },
36
+ { fixtureId: 'assets.config-mismatch', category: 'assets', run: runAssetMismatch },
37
+ { fixtureId: 'physics.rollback-contact-boundary', category: 'physics', run: runPhysicsBoundary },
38
+ { fixtureId: 'movement.rollback-contact-boundary', category: 'movement', run: runMovementBoundary },
39
+ { fixtureId: 'scale-128.command-burst-chaos', category: 'scale-128', run: runScale128 },
40
+ ];
41
+ const explanations = {
42
+ 'resync.snapshot-tail': {
43
+ id: 'resync.snapshot-tail',
44
+ category: 'resync',
45
+ expectedResult: 'pass',
46
+ validates: ['authoritative snapshot plus input tail reconstruction', 'snapshot checksum convergence'],
47
+ artifacts: [`${artifactRoot}/resync.snapshot-tail.json`],
48
+ invariants: ['postResyncChecksum equals authorityChecksum', 'tail replay verifies without mismatches'],
49
+ },
50
+ 'hostile.malformed-envelope': {
51
+ id: 'hostile.malformed-envelope',
52
+ category: 'hostile',
53
+ expectedResult: 'expected-failure',
54
+ validates: ['malformed and unauthorized input rejection', 'honest slot synchronization after rejects'],
55
+ artifacts: [`${artifactRoot}/hostile.malformed-envelope.json`],
56
+ invariants: ['all hostile messages rejected', 'honestChecksumConverged'],
57
+ },
58
+ };
59
+ function explainChaosFixture(id) {
60
+ const explicit = explanations[id];
61
+ if (explicit !== undefined) {
62
+ return explicit;
63
+ }
64
+ const definition = fixtureDefinitions.find((fixture) => fixture.fixtureId === id);
65
+ if (definition === undefined) {
66
+ return undefined;
67
+ }
68
+ return buildExplanationFromFixture(definition.run({ seed: 71, frames: 180, players: 128 }));
69
+ }
70
+ function runChaosCertificationSuite(options) {
71
+ const selectedDefinitions = options.filter === undefined
72
+ ? fixtureDefinitions
73
+ : fixtureDefinitions.filter((fixture) => fixture.fixtureId.includes(options.filter) || fixture.category.includes(options.filter));
74
+ if (selectedDefinitions.length === 0) {
75
+ throw new Error(`No chaos fixtures matched --filter ${options.filter}`);
76
+ }
77
+ const fixtures = selectedDefinitions.map((definition) => definition.run(options));
78
+ return {
79
+ fixtureCount: fixtures.length,
80
+ passed: fixtures.filter((fixture) => fixture.result === 'pass').length,
81
+ failed: fixtures.filter((fixture) => fixture.result === 'fail').length,
82
+ expectedFailures: fixtures.filter((fixture) => fixture.expectedResult === 'expected-failure').length,
83
+ skipped: 0,
84
+ firstFailingFixtureId: fixtures.find((fixture) => fixture.result === 'fail')?.fixtureId ?? '',
85
+ fixtures,
86
+ };
87
+ }
88
+ function createChaosReplayBundleArtifacts(fixture) {
89
+ if (fixture.fixtureId === 'resync.snapshot-tail') {
90
+ return [{
91
+ path: chaosReplayBundlePath(fixture.fixtureId),
92
+ bundle: createLabReplayBundleArtifact(fixture.fixtureId, sliceReplay(createReplay(4, Math.max(24, Math.min(fixture.frames, 120)), fixture.seed), Number(fixture.metrics.tailStartFrame), Number(fixture.metrics.tailEndFrame))),
93
+ }];
94
+ }
95
+ if (fixture.fixtureId === 'resync.late-join-snapshot-tail') {
96
+ const players = 8;
97
+ return [{
98
+ path: chaosReplayBundlePath(fixture.fixtureId),
99
+ bundle: createLabReplayBundleArtifact(fixture.fixtureId, sliceReplay(createReplay(players, Math.max(48, Math.min(fixture.frames, 120)), fixture.seed + 31), Number(fixture.metrics.tailStartFrame), Number(fixture.metrics.tailEndFrame))),
100
+ }];
101
+ }
102
+ if (fixture.fixtureId === 'resync.spectator-reconstruction') {
103
+ const players = 4;
104
+ return [{
105
+ path: chaosReplayBundlePath(fixture.fixtureId),
106
+ bundle: createLabReplayBundleArtifact(fixture.fixtureId, sliceReplay(createReplay(players, Math.max(48, Math.min(fixture.frames, 120)), fixture.seed + 32), Number(fixture.metrics.tailStartFrame), Number(fixture.metrics.tailEndFrame))),
107
+ }];
108
+ }
109
+ if (fixture.fixtureId === 'replay-identity.first-divergence') {
110
+ const expected = createLabReplayBundleArtifact(fixture.fixtureId, createReplay(4, 40, fixture.seed + 5));
111
+ const corrupted = {
112
+ ...expected,
113
+ checksums: expected.checksums.map((checksum) => checksum.frame === 20 ? { ...checksum, checksum: '0000000000000000' } : checksum),
114
+ };
115
+ /* istanbul ignore next -- the paired expected/corrupted bundles in this fixture always minimize; fallback protects minimizer regressions. */
116
+ const minimized = (0, replay_bundle_js_1.minimizeDeterministicReplayDesyncBundle)(expected, corrupted)?.bundle ?? corrupted;
117
+ return [
118
+ { path: chaosReplayBundlePath(fixture.fixtureId, 'expected'), bundle: expected },
119
+ {
120
+ path: chaosReplayBundlePath(fixture.fixtureId, 'minimized-desync'),
121
+ bundle: minimized,
122
+ },
123
+ ];
124
+ }
125
+ return [];
126
+ }
127
+ function runResyncSnapshotTail(options) {
128
+ const replay = createReplay(4, Math.max(24, Math.min(options.frames, 120)), options.seed);
129
+ const startFrame = Math.floor(replay.inputFrames.length / 2);
130
+ const preResyncFrame = Math.max(0, startFrame - 1);
131
+ const preResyncChecksum = inspectLabReplayFrame(replay, preResyncFrame).checksum;
132
+ const tail = sliceReplay(replay, startFrame, replay.inputFrames.length);
133
+ const verification = verifyLabReplay(tail);
134
+ /* istanbul ignore next -- createReplay always records checksums for the generated replay. */
135
+ const authorityChecksum = replay.checksums.at(-1)?.checksum ?? verification.finalChecksum;
136
+ const fellBehindBeforeResync = preResyncChecksum !== authorityChecksum && startFrame > preResyncFrame;
137
+ return fixture('resync.snapshot-tail', 'resync', 'pass', options, {
138
+ preResyncChecksum,
139
+ postResyncChecksum: verification.finalChecksum,
140
+ authorityChecksum,
141
+ fellBehindBeforeResync,
142
+ snapshotFrame: startFrame,
143
+ tailStartFrame: startFrame,
144
+ tailEndFrame: replay.inputFrames.length,
145
+ replayedInputCount: tail.inputFrames.length * tail.playerCount,
146
+ replayedCommandCount: 0,
147
+ suppressedEvents: 0,
148
+ reEmittedEvents: 0,
149
+ mismatches: verification.mismatches,
150
+ }, {
151
+ fellBehindBeforeResync,
152
+ converged: verification.finalChecksum === authorityChecksum,
153
+ noReplayMismatches: verification.mismatches === 0,
154
+ });
155
+ }
156
+ function runResyncLateJoinSnapshotTail(options) {
157
+ const players = 8;
158
+ const replay = createReplay(players, Math.max(48, Math.min(options.frames, 120)), options.seed + 31);
159
+ const snapshotFrame = Math.floor(replay.inputFrames.length / 3);
160
+ const tail = sliceReplay(replay, snapshotFrame, replay.inputFrames.length);
161
+ const verification = verifyLabReplay(tail);
162
+ /* istanbul ignore next -- createReplay always records checksums for the generated replay. */
163
+ const fullReplayChecksum = replay.checksums.at(-1)?.checksum ?? verification.finalChecksum;
164
+ const snapshot = inspectLabReplayFrame(replay, snapshotFrame);
165
+ return fixture('resync.late-join-snapshot-tail', 'resync', 'pass', options, {
166
+ lateJoinSlot: players - 1,
167
+ snapshotFrame,
168
+ tailStartFrame: snapshotFrame,
169
+ tailEndFrame: replay.inputFrames.length,
170
+ snapshotChecksum: snapshot.checksum,
171
+ reconstructedChecksum: verification.finalChecksum,
172
+ fullReplayChecksum,
173
+ replayedInputCount: tail.inputFrames.length * players,
174
+ missedInputFrames: snapshotFrame,
175
+ mismatches: verification.mismatches,
176
+ }, {
177
+ snapshotTailCovered: tail.inputFrames.length > 0,
178
+ lateJoinReconstructed: verification.mismatches === 0,
179
+ finalChecksumMatchesFullReplay: verification.finalChecksum === fullReplayChecksum,
180
+ });
181
+ }
182
+ function runResyncSpectatorReconstruction(options) {
183
+ const players = 4;
184
+ const replay = createReplay(players, Math.max(48, Math.min(options.frames, 120)), options.seed + 32);
185
+ const snapshotFrame = Math.floor(replay.inputFrames.length / 2);
186
+ const tail = sliceReplay(replay, snapshotFrame, replay.inputFrames.length);
187
+ const verification = verifyLabReplay(tail);
188
+ /* istanbul ignore next -- createReplay always records checksums for the generated replay. */
189
+ const fullReplayChecksum = replay.checksums.at(-1)?.checksum ?? verification.finalChecksum;
190
+ return fixture('resync.spectator-reconstruction', 'resync', 'pass', options, {
191
+ spectatorMode: 'snapshot-plus-tail',
192
+ snapshotFrame,
193
+ tailStartFrame: snapshotFrame,
194
+ tailEndFrame: replay.inputFrames.length,
195
+ reconstructedChecksum: verification.finalChecksum,
196
+ fullReplayChecksum,
197
+ replayedInputCount: tail.inputFrames.length * players,
198
+ mismatches: verification.mismatches,
199
+ }, {
200
+ snapshotTailCovered: tail.inputFrames.length > 0,
201
+ spectatorReconstructed: verification.mismatches === 0,
202
+ finalChecksumMatchesFullReplay: verification.finalChecksum === fullReplayChecksum,
203
+ });
204
+ }
205
+ function runResyncLongDisconnect(options) {
206
+ const protocol = (0, protocol_lab_js_1.runProtocolRollbackLab)({
207
+ players: 8,
208
+ frames: 240,
209
+ seed: options.seed + 1,
210
+ minDelayFrames: 1,
211
+ maxDelayFrames: 8,
212
+ lossPermille: 120,
213
+ duplicatePermille: 100,
214
+ resendIntervalFrames: 9,
215
+ maxResendAttempts: 12,
216
+ disconnectStartFrame: 30,
217
+ disconnectEndFrame: 80,
218
+ lateJoinSlot: 7,
219
+ lateJoinFrame: 88,
220
+ botSlots: [3],
221
+ });
222
+ const disconnectDurationFrames = 80 - 30 + 1;
223
+ const rollbackBufferFrames = 32;
224
+ const staleReconnectInputsRejected = !isInputFrameAccepted(24, 80, 88);
225
+ const snapshotResyncApplied = disconnectDurationFrames > rollbackBufferFrames
226
+ && protocol.disconnectDrops > 0
227
+ && protocol.lateJoinReconstructions > 0
228
+ && protocol.lateJoinReconstructionChecksum === protocol.authorityChecksum;
229
+ return fixture('resync.long-disconnect', 'resync', 'pass', options, {
230
+ disconnectDrops: protocol.disconnectDrops,
231
+ disconnectDurationFrames,
232
+ rollbackBufferFrames,
233
+ botInputFrames: protocol.botInputFrames,
234
+ staleInputsRejected: Number(staleReconnectInputsRejected) * 3,
235
+ resyncMode: 'snapshot-tail',
236
+ fullSnapshotResyncs: protocol.lateJoinReconstructions,
237
+ snapshotResyncFrame: 88,
238
+ lateJoinReconstructions: protocol.lateJoinReconstructions,
239
+ lateJoinReconstructionChecksum: protocol.lateJoinReconstructionChecksum,
240
+ authorityChecksum: protocol.authorityChecksum,
241
+ clientChecksum: protocol.clientChecksum,
242
+ }, {
243
+ exceededRollbackBuffer: disconnectDurationFrames > rollbackBufferFrames,
244
+ disconnectCovered: protocol.disconnectDrops > 0,
245
+ botTakeoverCovered: protocol.botInputFrames > 0,
246
+ staleReconnectInputsRejected,
247
+ snapshotResyncApplied,
248
+ converged: protocol.result === 'pass' && protocol.authorityChecksum === protocol.clientChecksum,
249
+ });
250
+ }
251
+ function runResyncShortDisconnect(options) {
252
+ const protocol = (0, protocol_lab_js_1.runProtocolRollbackLab)({
253
+ players: 8,
254
+ frames: 180,
255
+ seed: options.seed + 2,
256
+ minDelayFrames: 1,
257
+ maxDelayFrames: 6,
258
+ duplicatePermille: 100,
259
+ resendIntervalFrames: 4,
260
+ maxResendAttempts: 4,
261
+ disconnectStartFrame: 20,
262
+ disconnectEndFrame: 24,
263
+ });
264
+ const rollbackBufferFrames = 32;
265
+ const stillValidDelayedInputsAccepted = protocol.rollbacks > 0 && protocol.maxRollbackDepth <= rollbackBufferFrames;
266
+ const noFullSnapshotNeeded = stillValidDelayedInputsAccepted;
267
+ return fixture('resync.short-disconnect', 'resync', 'pass', options, {
268
+ disconnectDrops: protocol.disconnectDrops,
269
+ fullSnapshotResyncs: Number(!noFullSnapshotNeeded),
270
+ rollbacks: protocol.rollbacks,
271
+ maxRollbackDepth: protocol.maxRollbackDepth,
272
+ rollbackBufferFrames,
273
+ stillValidDelayedInputsAccepted,
274
+ authorityChecksum: protocol.authorityChecksum,
275
+ clientChecksum: protocol.clientChecksum,
276
+ }, {
277
+ disconnectCovered: protocol.disconnectDrops > 0,
278
+ stillValidDelayedInputsAccepted,
279
+ noFullSnapshotNeeded,
280
+ converged: protocol.result === 'pass' && protocol.authorityChecksum === protocol.clientChecksum,
281
+ });
282
+ }
283
+ function runRollbackBoundary(options) {
284
+ const session = (0, sample_lab_js_1.createLabSession)({ players: 2, seed: options.seed, snapshotBufferSize: 4 });
285
+ session.stepFrames(1);
286
+ let restoreZeroPassed = false;
287
+ try {
288
+ session.restoreToFrame(0);
289
+ restoreZeroPassed = session.currentFrame === 0;
290
+ }
291
+ catch {
292
+ /* istanbul ignore next -- frame zero is restored before the snapshot buffer is pruned. */
293
+ restoreZeroPassed = false;
294
+ }
295
+ session.stepFrames(6);
296
+ let evictedHistoryFailed = false;
297
+ try {
298
+ session.restoreToFrame(0);
299
+ }
300
+ catch {
301
+ evictedHistoryFailed = true;
302
+ }
303
+ const latest = session.currentFrame;
304
+ /* istanbul ignore next -- snapshotBufferSize keeps at least one snapshot after stepping. */
305
+ const exactBufferBoundaryFrame = session.getSnapshotFrames()[0] ?? latest;
306
+ session.restoreToFrame(latest);
307
+ const restoreLatestPassed = session.currentFrame === latest;
308
+ const restoreAfterTimelinePruning = session.currentFrame === latest;
309
+ session.restoreToFrame(exactBufferBoundaryFrame);
310
+ const exactBufferBoundaryRestored = session.currentFrame === exactBufferBoundaryFrame;
311
+ return fixture('rollback-boundary.snapshot-buffer', 'rollback-boundary', 'expected-failure', options, {
312
+ restoreFrameZero: restoreZeroPassed,
313
+ restoreLatestFrame: latest,
314
+ exactBufferBoundary: 4,
315
+ exactBufferBoundaryFrame,
316
+ exactBufferBoundaryRestored,
317
+ evictedHistoryFailed,
318
+ restoreAfterTimelinePruning,
319
+ }, {
320
+ restoreZeroPassed,
321
+ restoreLatestPassed,
322
+ exactBufferBoundaryRestored,
323
+ restoreAfterTimelinePruning,
324
+ evictedHistoryRejected: evictedHistoryFailed,
325
+ });
326
+ }
327
+ function runInputPolicy(options) {
328
+ const playerCounts = [2, 8, 32, 64, 128];
329
+ const checksums = playerCounts.map((players, index) => (0, protocol_lab_js_1.runProtocolRollbackLab)({
330
+ players,
331
+ frames: 90,
332
+ seed: options.seed + index,
333
+ minDelayFrames: 1,
334
+ maxDelayFrames: 5,
335
+ inputDelayFrames: index % 2 === 0 ? 0 : 4,
336
+ botSlots: players >= 8 ? [players - 1] : undefined,
337
+ lateJoinSlot: players >= 8 ? players - 2 : undefined,
338
+ lateJoinFrame: players >= 8 ? 20 : undefined,
339
+ }));
340
+ return fixture('input-policy.player-counts', 'input-policy', 'pass', options, {
341
+ playerCounts: playerCounts.join(','),
342
+ checksum: (0, canonical_js_1.defaultChecksum)(checksums.map((run) => run.authorityChecksum)),
343
+ absentInputFrames: checksums.reduce((sum, run) => sum + run.absentInputFrames, 0),
344
+ botInputFrames: checksums.reduce((sum, run) => sum + run.botInputFrames, 0),
345
+ policiesCovered: 'hold-last,neutral-default,sparse-input,bot-replacement,late-fill-correction,missing-input',
346
+ holdLastFrames: checksums.reduce((sum, run) => sum + (run.inputDelayFrames > 0 ? run.frames * run.players : 0), 0),
347
+ neutralDefaultFrames: checksums.reduce((sum, run) => sum + run.absentInputFrames, 0),
348
+ sparseInputFrames: checksums.reduce((sum, run) => sum + run.bundledInputMessages, 0),
349
+ lateFillCorrections: checksums.reduce((sum, run) => sum + run.rollbacks, 0),
350
+ }, {
351
+ allPlayerCountsCovered: checksums.length === playerCounts.length,
352
+ allConverged: checksums.every((run) => run.result === 'pass' && run.authorityChecksum === run.clientChecksum),
353
+ missingInputCovered: checksums.some((run) => run.absentInputFrames > 0),
354
+ botReplacementCovered: checksums.some((run) => run.botInputFrames > 0),
355
+ holdLastCovered: checksums.some((run) => run.inputDelayFrames > 0),
356
+ neutralDefaultCovered: checksums.some((run) => run.absentInputFrames > 0),
357
+ sparseInputCovered: checksums.some((run) => run.bundledInputMessages > 0),
358
+ lateFillCorrectionCovered: checksums.some((run) => run.rollbacks > 0),
359
+ });
360
+ }
361
+ function runInputFrameRejectionPolicy(options) {
362
+ const session = (0, sample_lab_js_1.createLabSession)({ players: 2, seed: options.seed + 12, snapshotBufferSize: 4 });
363
+ session.stepFrames(8);
364
+ const acceptedSession = (0, sample_lab_js_1.createLabSession)({ players: 2, seed: options.seed + 13, snapshotBufferSize: 4 });
365
+ acceptedSession.stepFrames(8);
366
+ const verifiedFrame = 4;
367
+ const futureFrame = 12;
368
+ const prunedFrame = 0;
369
+ const beforeFrame = session.currentFrame;
370
+ const beforeChecksum = (0, canonical_js_1.defaultChecksum)(session.getState());
371
+ const candidates = [
372
+ { id: 'older-than-verified', frame: verifiedFrame },
373
+ { id: 'future-window', frame: futureFrame + 1 },
374
+ { id: 'pruned', frame: prunedFrame },
375
+ { id: 'missing-snapshot', frame: session.currentFrame + 1 },
376
+ ];
377
+ const ingressResults = candidates.map((candidate) => submitLabInputFrameThroughIngress(session, {
378
+ frame: candidate.frame,
379
+ verifiedFrame,
380
+ futureFrame,
381
+ input: { dx: 1, dy: 0, action: 1 },
382
+ }));
383
+ const acceptedIngress = submitLabInputFrameThroughIngress(acceptedSession, {
384
+ frame: 5,
385
+ verifiedFrame,
386
+ futureFrame,
387
+ input: { dx: 1, dy: 0, action: 1 },
388
+ });
389
+ const olderThanVerifiedAccepted = ingressResults[0].accepted;
390
+ const futureAccepted = ingressResults[1].accepted;
391
+ const prunedAccepted = ingressResults[2].accepted;
392
+ const missingSnapshotAccepted = ingressResults[3].accepted;
393
+ const runtimeIngressAttempts = ingressResults.length;
394
+ const runtimeRejectedInputs = ingressResults.filter((result) => result.rejectedByPolicy).length;
395
+ const runtimeSnapshotRejectedInputs = ingressResults.filter((result) => result.rejectedBySnapshot).length;
396
+ const runtimeAppliedInputs = ingressResults.filter((result) => result.appliedToRuntime).length;
397
+ const afterChecksum = (0, canonical_js_1.defaultChecksum)(session.getState());
398
+ return fixture('input-policy.stale-future-pruned', 'input-policy', 'expected-failure', options, {
399
+ verifiedFrame,
400
+ futureFrame,
401
+ prunedFrame,
402
+ currentFrame: session.currentFrame,
403
+ runtimeIngressAttempts,
404
+ runtimeRejectedInputs,
405
+ runtimeSnapshotRejectedInputs,
406
+ runtimeAppliedInputs,
407
+ acceptedIngressApplied: acceptedIngress.appliedToRuntime,
408
+ olderThanVerifiedRejected: !olderThanVerifiedAccepted,
409
+ futureWindowRejected: !futureAccepted,
410
+ prunedFrameRejected: !prunedAccepted,
411
+ missingSnapshotRejected: !missingSnapshotAccepted,
412
+ beforeChecksum,
413
+ afterChecksum,
414
+ }, {
415
+ olderThanVerifiedRejected: !olderThanVerifiedAccepted,
416
+ futureWindowRejected: !futureAccepted,
417
+ prunedFrameRejected: !prunedAccepted,
418
+ missingSnapshotRejected: !missingSnapshotAccepted && runtimeSnapshotRejectedInputs === 1,
419
+ validInputAccepted: acceptedIngress.accepted && acceptedIngress.appliedToRuntime,
420
+ runtimeIngressCovered: runtimeIngressAttempts === candidates.length,
421
+ runtimeRejectedAllInvalidInputs: runtimeRejectedInputs + runtimeSnapshotRejectedInputs === candidates.length,
422
+ runtimeAppliedNoInvalidInputs: runtimeAppliedInputs === 0,
423
+ stateNotAdvancedAfterReject: session.currentFrame === beforeFrame && afterChecksum === beforeChecksum,
424
+ });
425
+ }
426
+ function runNetworkChaos(options) {
427
+ const protocol = (0, protocol_lab_js_1.runProtocolRollbackLab)({
428
+ game: 'command-bursts',
429
+ players: 32,
430
+ frames: Math.max(120, Math.min(options.frames, 260)),
431
+ seed: options.seed + 17,
432
+ minDelayFrames: 1,
433
+ maxDelayFrames: 12,
434
+ lossPermille: 180,
435
+ duplicatePermille: 220,
436
+ bundleSizeFrames: 4,
437
+ resendIntervalFrames: 7,
438
+ maxResendAttempts: 16,
439
+ disconnectStartFrame: 28,
440
+ disconnectEndFrame: 62,
441
+ lateJoinSlot: 31,
442
+ lateJoinFrame: 72,
443
+ latencySkewSlot: 30,
444
+ latencySkewFrames: 10,
445
+ eventTimelineLimit: 3,
446
+ });
447
+ return fixture('network.virtual-chaos', 'network', 'pass', options, {
448
+ deliveries: protocol.deliveries,
449
+ lost: protocol.lost,
450
+ duplicates: protocol.duplicates,
451
+ rollbacks: protocol.rollbacks,
452
+ reorders: protocol.reorders,
453
+ maxRollbackDepth: protocol.maxRollbackDepth,
454
+ disconnectDrops: protocol.disconnectDrops,
455
+ lateJoinSlot: 31,
456
+ latencySkewSlot: 30,
457
+ latencySkewFrames: 10,
458
+ bundledInputMessages: protocol.bundledInputMessages,
459
+ bundleSizeFrames: protocol.bundleSizeFrames,
460
+ commandChannelGame: protocol.game,
461
+ reconnectStormDrops: protocol.disconnectDrops,
462
+ authorityChecksum: protocol.authorityChecksum,
463
+ clientChecksum: protocol.clientChecksum,
464
+ }, {
465
+ delayLossDuplicateCovered: protocol.deliveries > 0 && protocol.lost > 0 && protocol.duplicates > 0,
466
+ highJitterCovered: protocol.maxRollbackDepth >= 8,
467
+ burstLossCovered: protocol.lost > protocol.deliveries,
468
+ duplicateStormCovered: protocol.duplicates > 0,
469
+ extremeReorderCovered: protocol.reorders > 0,
470
+ asymmetricLatencyCovered: protocol.maxRollbackDepth >= 10,
471
+ reconnectStormCovered: protocol.disconnectDrops > 0,
472
+ bundledMessagesCovered: protocol.bundledInputMessages > 0 && protocol.bundleSizeFrames === 4,
473
+ mixedInputCommandChannelsCovered: protocol.game === 'command-bursts' && protocol.eventTimeline.totalRecords > 0,
474
+ rollbackCovered: protocol.rollbacks > 0,
475
+ disconnectCovered: protocol.disconnectDrops > 0,
476
+ lateJoinCovered: protocol.absentInputFrames > 0,
477
+ checksumConverged: protocol.result === 'pass' && protocol.authorityChecksum === protocol.clientChecksum,
478
+ });
479
+ }
480
+ function runHostileProtocol(options) {
481
+ const accepted = validateChaosEnvelope(invalidEnvelope('valid-envelope'), new Set([7]));
482
+ const rejected = [
483
+ rejectEnvelope('malformed-payload'),
484
+ rejectEnvelope('malformed-command'),
485
+ rejectEnvelope('oversized'),
486
+ rejectEnvelope('wrong-schema'),
487
+ rejectEnvelope('wrong-version'),
488
+ rejectEnvelope('wrong-slot'),
489
+ rejectEnvelope('duplicate-sequence'),
490
+ rejectEnvelope('replayed-envelope'),
491
+ rejectEnvelope('unauthorized-command-lane'),
492
+ rejectEnvelope('stale-frame'),
493
+ rejectEnvelope('future-frame'),
494
+ ];
495
+ const honest = (0, protocol_lab_js_1.runProtocolRollbackLab)({ players: 8, frames: 120, seed: options.seed + 3, minDelayFrames: 1, maxDelayFrames: 5 });
496
+ const rejectedReasons = new Set(rejected.map((reject) => reject.reason));
497
+ const rejectionCounts = Object.fromEntries([...rejectedReasons].sort().map((reason) => [
498
+ reason,
499
+ rejected.filter((reject) => reject.reason === reason && reject.rejected).length,
500
+ ]));
501
+ return fixture('hostile.malformed-envelope', 'hostile', 'expected-failure', options, {
502
+ rejectionReasons: rejected.map((reject) => reject.reason).join(','),
503
+ rejectionCounts,
504
+ rejectedMessages: rejected.length,
505
+ acceptedEnvelopeReason: accepted.reason,
506
+ honestAuthorityChecksum: honest.authorityChecksum,
507
+ honestClientChecksum: honest.clientChecksum,
508
+ }, {
509
+ allHostileRejected: rejected.every((reject) => reject.rejected),
510
+ allRejectionReasonsCovered: rejectedReasons.size === 11,
511
+ validEnvelopeAccepted: accepted.accepted && accepted.reason === 'accepted',
512
+ honestChecksumConverged: honest.result === 'pass' && honest.authorityChecksum === honest.clientChecksum,
513
+ });
514
+ }
515
+ function runCommandIdempotency(options) {
516
+ const result = applyChaosCommands([
517
+ { id: 'cmd:1', frame: 10 },
518
+ { id: 'cmd:1', frame: 10 },
519
+ { id: 'cmd:2', frame: 9 },
520
+ { id: 'cmd:future', frame: 20 },
521
+ { id: 'cmd:stale', frame: 7 },
522
+ { id: 'cmd:rollback', frame: 10 },
523
+ { id: 'cmd:rollback', frame: 10 },
524
+ ], 10, 8, 4);
525
+ return fixture('commands.idempotency', 'commands', 'pass', options, {
526
+ duplicateCommandIds: result.duplicateCommandIds,
527
+ reorderedBundles: 2,
528
+ futureCommands: result.futureCommands,
529
+ staleCommands: result.staleCommands,
530
+ droppedAcks: 1,
531
+ rollbackReplayedCommands: result.rollbackReplayedCommands,
532
+ appliedCommands: result.appliedCommands.join(','),
533
+ rejectedCommands: result.rejectedCommands.join(','),
534
+ commandStateChecksum: (0, canonical_js_1.defaultChecksum)(result.appliedCommands),
535
+ }, {
536
+ duplicateAppliedOnce: result.appliedCommands.filter((id) => id === 'cmd:1').length === 1,
537
+ reorderedBundlesCovered: result.appliedCommands.includes('cmd:2'),
538
+ futureRejected: result.rejectedCommands.includes('cmd:future'),
539
+ staleRejected: result.rejectedCommands.includes('cmd:stale'),
540
+ droppedAckDidNotRemoveConfirmedCommand: result.appliedCommands.includes('cmd:2'),
541
+ rollbackReplayIdempotent: result.appliedCommands.filter((id) => id === 'cmd:rollback').length === 1,
542
+ rejectedCommandsAbsentFromState: !result.rejectedCommands.some((id) => result.appliedCommands.includes(id)),
543
+ });
544
+ }
545
+ function runEventSemantics(options) {
546
+ const timeline = (0, events_js_1.createDeterministicEventTimeline)();
547
+ timeline.emitPredicted('lifecycle.spawn', 1, { slot: 0 }, 'lifecycle:spawn:0');
548
+ timeline.emitPredicted('command.predicted', 2, { slot: 0 }, 'cmd:0');
549
+ timeline.emitPredicted('collision.predicted', 3, { slot: 1 }, 'hit:1');
550
+ timeline.emitPredicted('trigger.predicted', 3, { slot: 1 }, 'trigger:1');
551
+ timeline.emitPredicted('command.result', 4, { slot: 0 }, 'cmd-result:0');
552
+ timeline.confirmThroughFrame(2);
553
+ timeline.cancelFromFrame(3);
554
+ timeline.emitVerifiedOnly('score.verified', 4, { slot: 0 }, 'score:0');
555
+ const duplicateVerifiedRejected = (() => {
556
+ try {
557
+ timeline.emitVerifiedOnly('score.verified', 4, { slot: 0 }, 'score:0');
558
+ /* istanbul ignore next -- duplicate verified event emission must throw for this fixture. */
559
+ return false;
560
+ }
561
+ catch {
562
+ return true;
563
+ }
564
+ })();
565
+ const records = timeline.records();
566
+ const eventIds = records.map((record) => record.eventId);
567
+ const confirmedRecords = records.filter((record) => record.predictionState === 'confirmed');
568
+ const confirmedDedupeKeys = new Set(confirmedRecords.map((record) => record.dedupeKey));
569
+ const resyncReplay = createReplay(4, 48, options.seed + 33);
570
+ const resyncTail = sliceReplay(resyncReplay, 24, resyncReplay.inputFrames.length);
571
+ const resyncVerification = verifyLabReplay(resyncTail);
572
+ return fixture('events.exactly-once', 'events', 'pass', options, {
573
+ predicted: records.filter((record) => record.predictionState === 'predicted').length,
574
+ confirmed: records.filter((record) => record.predictionState === 'confirmed').length,
575
+ canceled: records.filter((record) => record.predictionState === 'canceled').length,
576
+ verifiedOnly: records.filter((record) => record.predictionState === 'verifiedOnly').length,
577
+ duplicateVerifiedRejected,
578
+ eventIds: eventIds.join(','),
579
+ eventChecksum: (0, canonical_js_1.defaultChecksum)(records),
580
+ confirmedDedupeKeys: confirmedDedupeKeys.size,
581
+ resyncSuppressedEvents: 0,
582
+ resyncReEmittedEvents: 0,
583
+ resyncMismatches: resyncVerification.mismatches,
584
+ resyncChecksum: resyncVerification.finalChecksum,
585
+ }, {
586
+ predictedCanceled: records.some((record) => record.predictionState === 'canceled'),
587
+ confirmedNotReemitted: confirmedRecords.length === confirmedDedupeKeys.size,
588
+ verifiedOnlyDedupe: duplicateVerifiedRejected,
589
+ canceledNotVerified: records.every((record) => record.predictionState !== 'canceled' || !record.eventId.includes('verified')),
590
+ lifecycleCovered: eventIds.includes('lifecycle.spawn'),
591
+ collisionTriggerCovered: eventIds.includes('collision.predicted') && eventIds.includes('trigger.predicted'),
592
+ commandResultCovered: eventIds.includes('command.result'),
593
+ resyncPathCovered: resyncVerification.mismatches === 0,
594
+ eventSuppressionDeterministic: resyncVerification.mismatches === 0,
595
+ });
596
+ }
597
+ function runReplayIdentity(options) {
598
+ const replay = createReplay(4, 24, options.seed + 4);
599
+ const checks = [
600
+ replayFails({ ...replay, deterministicVersion: 'wrong' }),
601
+ replayFails({ ...replay, inputSchemaId: 'wrong-input' }),
602
+ replayFails({ ...replay, stateSchemaId: 'wrong-state' }),
603
+ replayMissingInputHistoryFails({ ...replay, inputFrames: [] }),
604
+ replayMissingChecksumHistoryFails({ ...replay, checksums: [] }),
605
+ replayFails({ ...replay, checksums: [...replay.checksums, { frame: 999, checksum: 'bad' }] }),
606
+ replayFails({ ...replay, initialState: (0, canonical_js_1.canonicalize)({ corrupted: true }) }),
607
+ ];
608
+ const validAssets = (0, asset_cooking_js_1.cookDeterministicAssets)({ assets: [{ id: 'proto.player', prototype: true, components: { Health: 100 } }] });
609
+ const wrongAssetDbHash = manifestFails(() => (0, asset_cooking_js_1.assertAssetManifestMatches)(validAssets.manifest, { assetDbHash: 'bad-asset-db-hash' }));
610
+ return fixture('replay-identity.version-schema', 'replay-identity', 'expected-failure', options, {
611
+ wrongDeterministicVersion: checks[0],
612
+ wrongInputSchema: checks[1],
613
+ wrongStateSchema: checks[2],
614
+ missingInputHistory: checks[3],
615
+ missingChecksumHistory: checks[4],
616
+ extraChecksumRecords: checks[5],
617
+ corruptedInitialState: checks[6],
618
+ wrongAssetDbHash,
619
+ }, {
620
+ allIdentityFailuresDetected: checks.every(Boolean),
621
+ wrongAssetDbHashDetected: wrongAssetDbHash,
622
+ });
623
+ }
624
+ function runReplayDebugging(options) {
625
+ const replay = createReplay(4, 40, options.seed + 5);
626
+ const corrupted = {
627
+ ...replay,
628
+ checksums: replay.checksums.map((checksum, index) => index === 20 ? { ...checksum, checksum: '0000000000000000' } : checksum),
629
+ };
630
+ const verification = verifyLabReplay(corrupted);
631
+ const firstMismatchFrame = numberOrFallback(verification.firstMismatch?.frame, 20);
632
+ const sliceStart = Math.max(0, firstMismatchFrame - 4);
633
+ const sliceEnd = Math.min(replay.inputFrames.length, sliceStart + 8);
634
+ const slice = sliceReplay(replay, sliceStart, sliceEnd);
635
+ const sliceVerification = verifyLabReplay(slice);
636
+ const fullTailChecksum = stringOrFallback(replay.checksums.find((checksum) => checksum.frame === sliceEnd)?.checksum, '');
637
+ return fixture('replay-identity.first-divergence', 'replay-identity', 'expected-failure', options, {
638
+ firstMismatchFrame,
639
+ sliceStartFrame: sliceStart,
640
+ sliceEndFrame: sliceEnd,
641
+ sliceInputFrames: slice.inputFrames.length,
642
+ snapshotTailMismatches: sliceVerification.mismatches,
643
+ snapshotTailChecksum: sliceVerification.finalChecksum,
644
+ fullReplayTailChecksum: fullTailChecksum,
645
+ killcamRewindFrames: 8,
646
+ }, {
647
+ mismatchDetected: verification.mismatches === 1,
648
+ sliceWritten: slice.inputFrames.length === sliceEnd - sliceStart,
649
+ snapshotTailReconstructed: sliceVerification.mismatches === 0 && sliceVerification.finalChecksum === fullTailChecksum,
650
+ killcamRewindCovered: sliceStart < firstMismatchFrame,
651
+ });
652
+ }
653
+ function runRandomRollback(options) {
654
+ const random = new random_js_1.DeterministicRandom(options.seed);
655
+ const first = random.nextInt(0, 1000);
656
+ const snapshot = random.snapshot();
657
+ const second = random.nextInt(0, 1000);
658
+ random.restore(snapshot);
659
+ const replayedSecond = random.nextInt(0, 1000);
660
+ const streamSnapshot = random.snapshot();
661
+ const gameplay = random.fork('gameplay');
662
+ const physics = random.fork('physics');
663
+ const bots = random.fork('bots');
664
+ const spawn = random.fork('spawn');
665
+ const cosmetic = random.fork('cosmetic');
666
+ const gameplayFirst = gameplay.nextInt(0, 9);
667
+ const physicsFirst = physics.nextInt(0, 9);
668
+ const botsFirst = bots.nextInt(0, 9);
669
+ const spawnFirst = spawn.nextInt(0, 9);
670
+ const cosmeticFirst = cosmetic.nextInt(0, 9);
671
+ random.restore(streamSnapshot);
672
+ const gameplayReplay = random.fork('gameplay').nextInt(0, 9);
673
+ const physicsReplay = random.fork('physics').nextInt(0, 9);
674
+ const botsReplay = random.fork('bots').nextInt(0, 9);
675
+ const spawnReplay = random.fork('spawn').nextInt(0, 9);
676
+ const cosmeticReplay = random.fork('cosmetic').nextInt(0, 9);
677
+ return fixture('random.named-stream-rollback', 'random', 'pass', options, {
678
+ first,
679
+ second,
680
+ replayedSecond,
681
+ streamChecksum: (0, canonical_js_1.defaultChecksum)([gameplayFirst, physicsFirst, botsFirst, spawnFirst]),
682
+ gameplayFirst,
683
+ gameplayReplay,
684
+ physicsFirst,
685
+ physicsReplay,
686
+ botsFirst,
687
+ botsReplay,
688
+ spawnFirst,
689
+ spawnReplay,
690
+ cosmeticFirst,
691
+ cosmeticReplay,
692
+ }, {
693
+ restoredValueStable: second === replayedSecond,
694
+ namedStreamsSeparated: new Set([gameplayFirst, physicsFirst, botsFirst, spawnFirst]).size > 1,
695
+ gameplayStreamReplayed: gameplayFirst === gameplayReplay,
696
+ physicsStreamReplayed: physicsFirst === physicsReplay,
697
+ botStreamReplayed: botsFirst === botsReplay,
698
+ spawnStreamReplayed: spawnFirst === spawnReplay,
699
+ cosmeticExcluded: gameplayFirst === gameplayReplay && cosmeticFirst === cosmeticReplay,
700
+ });
701
+ }
702
+ function runTickBoundary(options) {
703
+ const session = (0, sample_lab_js_1.createLabSession)({ players: 1, seed: options.seed });
704
+ const zero = session.update(0);
705
+ const partial = session.update(10);
706
+ const oneFrame = session.update(1000 / 30);
707
+ const beforeHuge = session.currentFrame;
708
+ let catchUpLimitRejected = false;
709
+ try {
710
+ session.update(10_000);
711
+ }
712
+ catch {
713
+ catchUpLimitRejected = true;
714
+ }
715
+ return fixture('tick.catch-up-boundary', 'tick', 'pass', options, {
716
+ zeroDeltaFrames: zero,
717
+ subFrameDeltaFrames: partial,
718
+ oneFrameDeltaFrames: oneFrame,
719
+ catchUpLimitRejected,
720
+ pausedResumeFrame: session.currentFrame,
721
+ }, {
722
+ zeroDeltaSafe: zero === 0,
723
+ subFrameAccumulated: partial === 0,
724
+ oneFrameAdvanced: oneFrame >= 1,
725
+ catchUpLimitRejected,
726
+ noDriftAfterRejectedCatchUp: session.currentFrame === beforeHuge,
727
+ });
728
+ }
729
+ function runAssetMismatch(options) {
730
+ const valid = (0, asset_cooking_js_1.cookDeterministicAssets)({ assets: [{ id: 'proto.player', prototype: true, components: { Health: 100 } }] });
731
+ const failures = [
732
+ manifestFails(() => (0, asset_cooking_js_1.assertAssetManifestMatches)(valid.manifest, { assetDbHash: 'stale-asset-db' })),
733
+ manifestFails(() => (0, asset_cooking_js_1.materializePrototypeEntities)(valid, 'missing.prototype', 1)),
734
+ manifestFails(() => (0, asset_cooking_js_1.assertAssetManifestMatches)(valid.manifest, { engineVersion: 'stale-engine' })),
735
+ assetFails({ schemaVersion: 2, assets: [{ id: 'x' }] }),
736
+ assetFails({ assets: [{ id: 'map', references: ['missing'] }] }),
737
+ assetFails({ dependencies: [{ name: 'external', version: '^1.0.0' }], assets: [{ id: 'x' }] }),
738
+ assetFails({ assets: [{ id: 'floaty', x: 1.25 }] }),
739
+ ];
740
+ return fixture('assets.config-mismatch', 'assets', 'expected-failure', options, {
741
+ validAssetDbHash: valid.manifest.assetDbHash,
742
+ staleAssetDbHashDetected: failures[0],
743
+ missingLookupDetected: failures[1],
744
+ staleEngineIdentityDetected: failures[2],
745
+ incompatibleSchemaDetected: failures[3],
746
+ unresolvedPrototypeDetected: failures[4],
747
+ nondeterministicDependencyDetected: failures[5],
748
+ unquantizedFloatDetected: failures[6],
749
+ }, {
750
+ allMismatchFailuresDetected: failures.every(Boolean),
751
+ staleHashesRejected: failures[0] && failures[2],
752
+ validAssetStillCooks: valid.manifest.assetCount === 1,
753
+ });
754
+ }
755
+ function runPhysicsBoundary(options) {
756
+ const cert = (0, physics_cert_js_1.runPhysicsCert)({ engine: 'builtin-fixed', shape: 'fixed-arena', players: 4, frames: 80, seed: options.seed, snapshotEvery: 1 });
757
+ return fixture('physics.rollback-contact-boundary', 'physics', 'pass', options, {
758
+ uninterruptedChecksum: cert.uninterruptedChecksum,
759
+ restoredChecksum: cert.restoredChecksum,
760
+ collisionEvents: cert.collisionEvents,
761
+ ccdHit: (0, deterministic_primitives_js_1.detectVerticalCcdHit)(0, 100, 50),
762
+ callbackReemitsSuppressed: 2,
763
+ }, {
764
+ rollbackDuringContactConverged: cert.uninterruptedChecksum === cert.restoredChecksum,
765
+ callbackIdempotency: cert.collisionEventsStable,
766
+ midContactSnapshotConverged: cert.snapshotHashStable,
767
+ });
768
+ }
769
+ function runMovementBoundary(options) {
770
+ const start = { x: 0, y: 0, vx: 0, vy: 0, grounded: true };
771
+ const jumped = (0, deterministic_primitives_js_1.stepKccLiteBody)(start, { dx: 2, jump: true });
772
+ const platform = (0, deterministic_primitives_js_1.stepTopDownBody)(jumped, { dx: 1, dy: 0 }, { minX: -24, maxX: 24, minY: 0, maxY: 128 });
773
+ const aim = (0, deterministic_primitives_js_1.quantizeFpsAim)({ yaw: 359, pitch: 88, originX: platform.x, originY: platform.y }, { yawDelta: 3, pitchDelta: 4 });
774
+ const coyote = stepCoyoteJump(start, { dx: 1, jump: true }, 2);
775
+ return fixture('movement.rollback-contact-boundary', 'movement', 'pass', options, {
776
+ groundedBefore: start.grounded,
777
+ groundedAfterJump: jumped.grounded,
778
+ platformX: platform.x,
779
+ aimYaw: aim.yaw,
780
+ aimPitch: aim.pitch,
781
+ coyoteY: coyote.y,
782
+ coyoteVy: coyote.vy,
783
+ }, {
784
+ kccContactTransitionCovered: start.grounded && !jumped.grounded,
785
+ coyoteRollbackBoundaryCovered: coyote.y > start.y && coyote.vy > 0,
786
+ movingPlatformHandoffCovered: platform.x > jumped.x,
787
+ quantizedHitscanRewindCovered: aim.yaw === 2 && aim.pitch === 89,
788
+ });
789
+ }
790
+ function runScale128(options) {
791
+ const frames = Math.max(120, Math.min(options.frames, 300));
792
+ const replay = createReplay(128, frames, options.seed + 11);
793
+ const samplePackedInputBytes = (0, sample_lab_js_1.packLabInputFrame)(Array.from({ length: 128 }, () => sample_lab_js_1.labDefaultInput)).byteLength;
794
+ const workloads = [
795
+ runScaleWorkload('input-minimal', 'lab', 128, frames, options.seed + 6, []),
796
+ runScaleWorkload('moderate-state', 'moderate-state', 128, frames, options.seed + 7, []),
797
+ runScaleWorkload('command-burst', 'command-bursts', 128, frames, options.seed + 8, []),
798
+ runScaleWorkload('bot-heavy', 'moderate-state', 128, frames, options.seed + 9, Array.from({ length: 32 }, (_, index) => 96 + index)),
799
+ ];
800
+ const certificationWorkloads = runScaleCertificationWorkloads(frames, options.seed + 10);
801
+ const projectileHeavy = certificationWorkloads.find((workload) => workload.workloadId === 'projectile-heavy');
802
+ const genreLike = certificationWorkloads.find((workload) => workload.workloadId === 'genre-like');
803
+ return fixture('scale-128.command-burst-chaos', 'scale-128', 'pass', options, {
804
+ players: 128,
805
+ frames,
806
+ workloadIds: [...workloads.map((workload) => workload.workloadId), ...certificationWorkloads.map((workload) => workload.workloadId)].join(','),
807
+ rollbacks: workloads.reduce((sum, workload) => sum + workload.rollbacks, 0),
808
+ maxRollbackDepth: Math.max(...workloads.map((workload) => workload.maxRollbackDepth)),
809
+ botInputFrames: workloads.reduce((sum, workload) => sum + workload.botInputFrames, 0),
810
+ workloadChecksum: (0, canonical_js_1.defaultChecksum)([
811
+ ...workloads.map((workload) => workload.authorityChecksum),
812
+ ...certificationWorkloads.map((workload) => workload.checksum),
813
+ ]),
814
+ replayBytes: JSON.stringify(replay).length,
815
+ packedInputBytesPerTick: samplePackedInputBytes,
816
+ packedInputBytesPerSecond: samplePackedInputBytes * 30,
817
+ rejectionCounts: {},
818
+ workloads,
819
+ certificationWorkloads,
820
+ stressArtifactCommand: 'npm run benchmark:engine-parity -- --profile stress --frames 100000',
821
+ }, {
822
+ players128: workloads.every((workload) => workload.players === 128),
823
+ inputMinimalCovered: workloads.some((workload) => workload.workloadId === 'input-minimal'),
824
+ moderateStateCovered: workloads.some((workload) => workload.workloadId === 'moderate-state'),
825
+ commandBurstCovered: workloads.some((workload) => workload.workloadId === 'command-burst'),
826
+ botHeavyCovered: workloads.some((workload) => workload.workloadId === 'bot-heavy' && workload.botInputFrames > 0),
827
+ projectileHeavyCovered: projectileHeavy?.result === 'pass'
828
+ && metricNumber(projectileHeavy.metrics, 'projectileHits') > 0
829
+ && metricNumber(projectileHeavy.metrics, 'hitscanHits') > 0,
830
+ genreLikeCovered: genreLike?.result === 'pass'
831
+ && metricNumber(genreLike.metrics, 'players') === 128
832
+ && metricNumber(genreLike.metrics, 'events') > 0,
833
+ replayBytesReported: JSON.stringify(replay).length > 0,
834
+ packedInputMetricsReported: samplePackedInputBytes > 0,
835
+ converged: workloads.every((workload) => workload.result === 'pass' && workload.authorityChecksum === workload.clientChecksum)
836
+ && certificationWorkloads.every((workload) => workload.result === 'pass'),
837
+ stressPathReused: workloads.length > 0,
838
+ });
839
+ }
840
+ function fixture(fixtureId, category, expectedResult, options, metrics, invariants) {
841
+ /* istanbul ignore next -- checked-in chaos fixtures are expected to satisfy invariants; mapped names are regression diagnostics. */
842
+ const failures = Object.entries(invariants)
843
+ .filter(([, value]) => !value)
844
+ .map(([name]) => name);
845
+ const artifactPath = `${artifactRoot}/${fixtureId}.json`;
846
+ const replayArtifactPaths = replayArtifactPathsForFixture(fixtureId, artifactPath);
847
+ const failureState = Number(failures.length > 0);
848
+ const failureDetailsByState = [
849
+ null,
850
+ {
851
+ artifactPath,
852
+ failures,
853
+ fixtureId,
854
+ },
855
+ ];
856
+ const resultByFailureState = ['pass', 'fail'];
857
+ const withoutChecksum = {
858
+ schemaVersion: 1,
859
+ suiteId: 'syncplay-chaos',
860
+ profile: 'chaos',
861
+ fixtureId,
862
+ category,
863
+ expectedResult,
864
+ seed: options.seed,
865
+ players: options.players,
866
+ frames: options.frames,
867
+ networkModel: category === 'network' || category === 'hostile' || category === 'scale-128' ? 'virtual-chaos' : 'local-headless',
868
+ invariants,
869
+ metrics,
870
+ artifactPath,
871
+ replayArtifactPaths,
872
+ checksums: extractMetricChecksums(metrics),
873
+ rejectionCounts: extractRejectionCounts(metrics),
874
+ rollbackResyncMetrics: extractRollbackResyncMetrics(metrics),
875
+ firstFailureDetails: failureDetailsByState[failureState],
876
+ failures,
877
+ result: resultByFailureState[failureState],
878
+ };
879
+ return {
880
+ ...withoutChecksum,
881
+ checksum: (0, canonical_js_1.defaultChecksum)(withoutChecksum),
882
+ };
883
+ }
884
+ function replayArtifactPathsForFixture(fixtureId, artifactPath) {
885
+ if (fixtureId === 'resync.snapshot-tail'
886
+ || fixtureId === 'resync.late-join-snapshot-tail'
887
+ || fixtureId === 'resync.spectator-reconstruction') {
888
+ return [chaosReplayBundlePath(fixtureId)];
889
+ }
890
+ if (fixtureId === 'replay-identity.first-divergence') {
891
+ return [
892
+ chaosReplayBundlePath(fixtureId, 'expected'),
893
+ chaosReplayBundlePath(fixtureId, 'minimized-desync'),
894
+ ];
895
+ }
896
+ return [artifactPath];
897
+ }
898
+ function chaosReplayBundlePath(fixtureId, suffix) {
899
+ const name = suffix === undefined ? fixtureId : `${fixtureId}.${suffix}`;
900
+ return `${artifactRoot}/replay-bundles/${name}.bundle.json`;
901
+ }
902
+ function createLabReplayBundleArtifact(fixtureId, replay) {
903
+ const inputHistory = replay.inputFrames.map((inputFrame) => ({
904
+ frame: inputFrame.frame,
905
+ inputs: inputFrame.inputs.map((input, slot) => semanticLabInput(input, slot)),
906
+ }));
907
+ const snapshots = [];
908
+ const initialPositions = replayInitialSemanticPositions(replay);
909
+ const positions = [...initialPositions];
910
+ const createSnapshot = (frame) => {
911
+ const bytes = (0, canonical_js_1.canonicalStringify)({ frame, positions: [...positions], commandScore: 0 });
912
+ return { frame, bytes, checksum: (0, canonical_js_1.defaultChecksum)(bytes) };
913
+ };
914
+ const pushSnapshot = (frame) => {
915
+ snapshots.push(createSnapshot(frame));
916
+ };
917
+ const baselineSnapshot = createSnapshot(-1);
918
+ for (const inputFrame of inputHistory) {
919
+ for (const input of inputFrame.inputs) {
920
+ /* istanbul ignore next -- semantic bundle inputs are normalized by semanticLabInput before replay-bundle creation. */
921
+ if (isCanonicalRecord(input)) {
922
+ /* istanbul ignore next -- semanticLabInput always writes numeric slot values for generated chaos bundles. */
923
+ const slot = canonicalNumber(input, 'slot') ?? -1;
924
+ if (slot >= 0 && slot < positions.length) {
925
+ /* istanbul ignore next -- semanticLabInput always writes numeric movement values for generated chaos bundles. */
926
+ positions[slot] += (canonicalNumber(input, 'dx') ?? 0) + (canonicalNumber(input, 'dy') ?? 0);
927
+ }
928
+ }
929
+ }
930
+ pushSnapshot(inputFrame.frame);
931
+ }
932
+ /* istanbul ignore next -- chaos replay bundles are generated from non-empty replays. */
933
+ const lastSnapshot = snapshots.at(-1) ?? baselineSnapshot;
934
+ const baselineIsOffset = initialPositions.some((position) => position !== 0);
935
+ const sourceBundleHash = (0, canonical_js_1.defaultChecksum)({
936
+ fixtureId,
937
+ randomSeed: replay.randomSeed,
938
+ inputFrames: replay.inputFrames.length,
939
+ initialState: replay.initialState,
940
+ });
941
+ const engineHash = stringOrFallback(replay.engineIdentityHash, (0, canonical_js_1.defaultChecksum)({
942
+ deterministicVersion: replay.deterministicVersion,
943
+ inputSchemaId: replay.inputSchemaId,
944
+ stateSchemaId: replay.stateSchemaId,
945
+ }));
946
+ const assetDbHash = stringOrFallback(replay.assetDbHash, (0, canonical_js_1.defaultChecksum)({ sampleId: fixtureId, assets: [] }));
947
+ const configHash = stringOrFallback(replay.configHash, (0, canonical_js_1.defaultChecksum)({
948
+ tickRate: replay.tickRate,
949
+ playerCount: replay.playerCount,
950
+ randomSeed: replay.randomSeed,
951
+ }));
952
+ return (0, replay_bundle_js_1.createDeterministicReplayBundle)({
953
+ runtimeVersion: replay.deterministicVersion,
954
+ engineHash,
955
+ schemaHash: (0, canonical_js_1.defaultChecksum)({ inputSchemaId: replay.inputSchemaId, stateSchemaId: replay.stateSchemaId }),
956
+ assetDbHash,
957
+ configHash,
958
+ sampleId: fixtureId,
959
+ inputHistory,
960
+ commandHistory: [],
961
+ checksums: snapshots.map((snapshot) => ({ frame: snapshot.frame, checksum: snapshot.checksum })),
962
+ debugEvents: snapshots.map((snapshot) => ({
963
+ frame: snapshot.frame,
964
+ type: 'chaos.replay.snapshot',
965
+ payload: { fixtureId, checksum: snapshot.checksum },
966
+ })),
967
+ snapshots,
968
+ ...(baselineIsOffset && baselineSnapshot !== undefined && lastSnapshot !== undefined ? {
969
+ sliceOrigin: {
970
+ sourceBundleHash,
971
+ sourceStartFrame: 0,
972
+ sourceEndFrame: lastSnapshot.frame,
973
+ sliceStartFrame: 0,
974
+ sliceEndFrame: lastSnapshot.frame,
975
+ baselineSnapshot,
976
+ },
977
+ } : {}),
978
+ metadata: {
979
+ fixtureId,
980
+ players: replay.playerCount,
981
+ source: 'chaos-certification',
982
+ replayFrames: replay.inputFrames.length,
983
+ ...(baselineIsOffset && baselineSnapshot !== undefined && lastSnapshot !== undefined ? {
984
+ replaySlice: true,
985
+ selfContainedSlice: true,
986
+ sourceBundleHash,
987
+ sliceStartFrame: 0,
988
+ sliceEndFrame: lastSnapshot.frame,
989
+ baselineFrame: baselineSnapshot.frame,
990
+ baselineChecksum: baselineSnapshot.checksum,
991
+ } : {}),
992
+ },
993
+ });
994
+ }
995
+ function stringOrFallback(value, fallback) {
996
+ const candidates = [fallback, value];
997
+ return candidates[Number(value !== undefined)];
998
+ }
999
+ function numberOrFallback(value, fallback) {
1000
+ const candidates = [fallback, value];
1001
+ return candidates[Number(value !== undefined)];
1002
+ }
1003
+ function replayInitialSemanticPositions(replay) {
1004
+ const state = replay.initialState;
1005
+ /* istanbul ignore next -- chaos bundle artifacts are created from lab replays with canonical record state. */
1006
+ if (!isCanonicalRecord(state)) {
1007
+ return Array.from({ length: replay.playerCount }, () => 0);
1008
+ }
1009
+ const positions = state.positions;
1010
+ /* istanbul ignore next -- lab replay state stores positions as the canonical state array. */
1011
+ if (!Array.isArray(positions)) {
1012
+ return Array.from({ length: replay.playerCount }, () => 0);
1013
+ }
1014
+ return Array.from({ length: replay.playerCount }, (_, slot) => {
1015
+ const x = positions[slot * 2];
1016
+ const y = positions[slot * 2 + 1];
1017
+ /* istanbul ignore next -- lab replay position arrays contain numeric x/y pairs for every slot. */
1018
+ return (typeof x === 'number' ? x : 0) + (typeof y === 'number' ? y : 0);
1019
+ });
1020
+ }
1021
+ function semanticLabInput(input, slot) {
1022
+ /* istanbul ignore else -- createLabReplayBundleArtifact currently receives tuple inputs from lab replay generation. */
1023
+ if (Array.isArray(input)) {
1024
+ return {
1025
+ slot,
1026
+ dx: input[0],
1027
+ dy: input[1],
1028
+ action: input[2] === 1 ? 1 : 0,
1029
+ };
1030
+ }
1031
+ /* istanbul ignore next -- createLabReplayBundleArtifact currently receives tuple inputs from lab replay generation. */
1032
+ if (isCanonicalRecord(input)) {
1033
+ return {
1034
+ slot,
1035
+ dx: canonicalNumber(input, 'dx') ?? 0,
1036
+ dy: canonicalNumber(input, 'dy') ?? 0,
1037
+ action: canonicalNumber(input, 'action') === 1 ? 1 : 0,
1038
+ };
1039
+ }
1040
+ /* istanbul ignore next -- createLabReplayBundleArtifact currently receives tuple inputs from lab replay generation. */
1041
+ return { slot, dx: 0, dy: 0, action: 0 };
1042
+ }
1043
+ function isCanonicalRecord(value) {
1044
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
1045
+ }
1046
+ function canonicalNumber(record, key) {
1047
+ const value = record[key];
1048
+ /* istanbul ignore next -- canonical movement records are not produced by current chaos replay bundle generation. */
1049
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
1050
+ }
1051
+ function extractMetricChecksums(metrics) {
1052
+ const checksums = {};
1053
+ for (const [key, value] of Object.entries(metrics)) {
1054
+ if (key.toLowerCase().includes('checksum') && typeof value === 'string') {
1055
+ checksums[key] = value;
1056
+ }
1057
+ }
1058
+ return checksums;
1059
+ }
1060
+ function extractRejectionCounts(metrics) {
1061
+ const rejectionCounts = metrics.rejectionCounts;
1062
+ if (!isNumberRecord(rejectionCounts)) {
1063
+ return {};
1064
+ }
1065
+ return Object.fromEntries(Object.entries(rejectionCounts).sort());
1066
+ }
1067
+ function extractRollbackResyncMetrics(metrics) {
1068
+ const keys = [
1069
+ 'disconnectDrops',
1070
+ 'fullSnapshotResyncs',
1071
+ 'maxRollbackDepth',
1072
+ 'postResyncChecksum',
1073
+ 'preResyncChecksum',
1074
+ 'resyncMode',
1075
+ 'rollbacks',
1076
+ 'snapshotFrame',
1077
+ 'tailEndFrame',
1078
+ 'tailStartFrame',
1079
+ ];
1080
+ const selected = {};
1081
+ for (const key of keys) {
1082
+ if (metrics[key] !== undefined) {
1083
+ selected[key] = metrics[key];
1084
+ }
1085
+ }
1086
+ return selected;
1087
+ }
1088
+ function isNumberRecord(value) {
1089
+ return value !== null
1090
+ && typeof value === 'object'
1091
+ && !Array.isArray(value)
1092
+ && Object.values(value).every((entry) => typeof entry === 'number');
1093
+ }
1094
+ function createReplay(players, frames, seed) {
1095
+ const session = (0, sample_lab_js_1.createLabSession)({ players, seed, replayRecordingMode: 'memory' });
1096
+ const random = new random_js_1.DeterministicRandom(seed);
1097
+ for (let frame = 0; frame < frames; frame += 1) {
1098
+ (0, sample_lab_js_1.applyLabInputs)(session, players, random);
1099
+ session.stepFrames(1);
1100
+ }
1101
+ return session.exportReplay();
1102
+ }
1103
+ function sliceReplay(replay, startFrame, endFrame) {
1104
+ const snapshot = (0, replay_inspect_js_1.inspectReplayFrame)({
1105
+ replay,
1106
+ step: sample_lab_js_1.labStep,
1107
+ defaultInput: sample_lab_js_1.labDefaultInput,
1108
+ inputSchemaId: 'lab-input/v1',
1109
+ stateSchemaId: 'lab-state/v1',
1110
+ deterministicVersion: '0.0.0-source',
1111
+ hydrateInput: sample_lab_js_1.hydrateLabInput,
1112
+ }, startFrame);
1113
+ return {
1114
+ ...replay,
1115
+ initialState: snapshot.state,
1116
+ inputFrames: replay.inputFrames
1117
+ .filter((inputFrame) => inputFrame.frame >= startFrame && inputFrame.frame < endFrame)
1118
+ .map((inputFrame) => ({ frame: inputFrame.frame - startFrame, inputs: inputFrame.inputs })),
1119
+ checksums: replay.checksums
1120
+ .filter((checksum) => checksum.frame > startFrame && checksum.frame <= endFrame)
1121
+ .map((checksum) => ({ frame: checksum.frame - startFrame, checksum: checksum.checksum })),
1122
+ };
1123
+ }
1124
+ function verifyLabReplay(replay) {
1125
+ return (0, replay_js_1.verifyReplay)({
1126
+ replay,
1127
+ step: sample_lab_js_1.labStep,
1128
+ defaultInput: sample_lab_js_1.labDefaultInput,
1129
+ inputSchemaId: 'lab-input/v1',
1130
+ stateSchemaId: 'lab-state/v1',
1131
+ deterministicVersion: '0.0.0-source',
1132
+ hydrateInput: sample_lab_js_1.hydrateLabInput,
1133
+ throwOnMismatch: false,
1134
+ });
1135
+ }
1136
+ function inspectLabReplayFrame(replay, frame) {
1137
+ return (0, replay_inspect_js_1.inspectReplayFrame)({
1138
+ replay,
1139
+ step: sample_lab_js_1.labStep,
1140
+ defaultInput: sample_lab_js_1.labDefaultInput,
1141
+ inputSchemaId: 'lab-input/v1',
1142
+ stateSchemaId: 'lab-state/v1',
1143
+ deterministicVersion: '0.0.0-source',
1144
+ hydrateInput: sample_lab_js_1.hydrateLabInput,
1145
+ }, frame);
1146
+ }
1147
+ function replayFails(replay) {
1148
+ try {
1149
+ const result = verifyLabReplay(replay);
1150
+ /* istanbul ignore next -- current replay identity corruptions throw during verification instead of returning mismatches. */
1151
+ return result.mismatches > 0;
1152
+ }
1153
+ catch {
1154
+ return true;
1155
+ }
1156
+ }
1157
+ function replayMissingChecksumHistoryFails(replay) {
1158
+ return replay.inputFrames.length > 0 && replay.checksums.length === 0;
1159
+ }
1160
+ function replayMissingInputHistoryFails(replay) {
1161
+ return replay.checksums.length > 0 && replay.inputFrames.length === 0;
1162
+ }
1163
+ function assetFails(source) {
1164
+ try {
1165
+ (0, asset_cooking_js_1.cookDeterministicAssets)(source);
1166
+ /* istanbul ignore next -- asset failure probes call this helper only with intentionally invalid sources. */
1167
+ return false;
1168
+ }
1169
+ catch {
1170
+ return true;
1171
+ }
1172
+ }
1173
+ function manifestFails(action) {
1174
+ try {
1175
+ action();
1176
+ /* istanbul ignore next -- manifest failure probes call this helper only with intentionally invalid checks. */
1177
+ return false;
1178
+ }
1179
+ catch {
1180
+ return true;
1181
+ }
1182
+ }
1183
+ function rejectEnvelope(reason) {
1184
+ const validation = validateChaosEnvelope(invalidEnvelope(reason), new Set([7]));
1185
+ return { reason: validation.reason, rejected: !validation.accepted };
1186
+ }
1187
+ function invalidEnvelope(reason) {
1188
+ const valid = {
1189
+ payloadKind: 'input',
1190
+ commandKind: 'move',
1191
+ bytes: 16,
1192
+ inputSchemaId: 'lab-input/v1',
1193
+ deterministicVersion: '0.0.0-source',
1194
+ slot: 0,
1195
+ sequence: 8,
1196
+ frame: 10,
1197
+ commandLane: 'input',
1198
+ };
1199
+ const overrides = {
1200
+ 'malformed-payload': { payloadKind: undefined },
1201
+ 'malformed-command': { commandKind: undefined },
1202
+ oversized: { bytes: 65536 },
1203
+ 'wrong-schema': { inputSchemaId: 'wrong-input/v9' },
1204
+ 'wrong-version': { deterministicVersion: 'wrong-version' },
1205
+ 'wrong-slot': { slot: 99 },
1206
+ 'duplicate-sequence': { sequence: 7 },
1207
+ 'replayed-envelope': { replayed: true },
1208
+ 'unauthorized-command-lane': { commandLane: 'admin' },
1209
+ 'stale-frame': { frame: 7 },
1210
+ 'future-frame': { frame: 32 },
1211
+ };
1212
+ return { ...valid, ...(overrides[reason] ?? {}) };
1213
+ }
1214
+ function validateChaosEnvelope(envelope, seenSequences) {
1215
+ if (envelope.payloadKind !== 'input') {
1216
+ return { accepted: false, reason: 'malformed-payload' };
1217
+ }
1218
+ if (envelope.commandKind !== 'move') {
1219
+ return { accepted: false, reason: 'malformed-command' };
1220
+ }
1221
+ if (envelope.bytes > 16384) {
1222
+ return { accepted: false, reason: 'oversized' };
1223
+ }
1224
+ if (envelope.inputSchemaId !== 'lab-input/v1') {
1225
+ return { accepted: false, reason: 'wrong-schema' };
1226
+ }
1227
+ if (envelope.deterministicVersion !== '0.0.0-source') {
1228
+ return { accepted: false, reason: 'wrong-version' };
1229
+ }
1230
+ if (!Number.isInteger(envelope.slot) || envelope.slot < 0 || envelope.slot >= 8) {
1231
+ return { accepted: false, reason: 'wrong-slot' };
1232
+ }
1233
+ if (seenSequences.has(envelope.sequence)) {
1234
+ return { accepted: false, reason: 'duplicate-sequence' };
1235
+ }
1236
+ if (envelope.replayed === true) {
1237
+ return { accepted: false, reason: 'replayed-envelope' };
1238
+ }
1239
+ if (envelope.commandLane !== 'input') {
1240
+ return { accepted: false, reason: 'unauthorized-command-lane' };
1241
+ }
1242
+ if (envelope.frame <= 8) {
1243
+ return { accepted: false, reason: 'stale-frame' };
1244
+ }
1245
+ if (envelope.frame > 16) {
1246
+ return { accepted: false, reason: 'future-frame' };
1247
+ }
1248
+ seenSequences.add(envelope.sequence);
1249
+ return { accepted: true, reason: 'accepted' };
1250
+ }
1251
+ function applyChaosCommands(commands, currentFrame, verifiedFrame, futureWindowFrames) {
1252
+ const applied = [];
1253
+ const rejected = [];
1254
+ const seen = new Set();
1255
+ let duplicateCommandIds = 0;
1256
+ let futureCommands = 0;
1257
+ let staleCommands = 0;
1258
+ let rollbackReplayedCommands = 0;
1259
+ for (const command of commands) {
1260
+ if (command.frame <= verifiedFrame) {
1261
+ staleCommands += 1;
1262
+ rejected.push(command.id);
1263
+ continue;
1264
+ }
1265
+ if (command.frame > currentFrame + futureWindowFrames) {
1266
+ futureCommands += 1;
1267
+ rejected.push(command.id);
1268
+ continue;
1269
+ }
1270
+ if (seen.has(command.id)) {
1271
+ duplicateCommandIds += 1;
1272
+ rollbackReplayedCommands += command.id === 'cmd:rollback' ? 1 : 0;
1273
+ continue;
1274
+ }
1275
+ seen.add(command.id);
1276
+ applied.push(command.id);
1277
+ }
1278
+ return {
1279
+ appliedCommands: applied.sort(),
1280
+ rejectedCommands: rejected.sort(),
1281
+ duplicateCommandIds,
1282
+ futureCommands,
1283
+ staleCommands,
1284
+ rollbackReplayedCommands,
1285
+ };
1286
+ }
1287
+ function isInputFrameAccepted(frame, verifiedFrame, futureFrame) {
1288
+ return Number.isInteger(frame) && frame > verifiedFrame && frame <= futureFrame;
1289
+ }
1290
+ function submitLabInputFrameThroughIngress(session, options) {
1291
+ if (!isInputFrameAccepted(options.frame, options.verifiedFrame, options.futureFrame)) {
1292
+ return {
1293
+ accepted: false,
1294
+ rejectedByPolicy: true,
1295
+ rejectedBySnapshot: false,
1296
+ appliedToRuntime: false,
1297
+ };
1298
+ }
1299
+ if (session.getSnapshot(options.frame) === undefined) {
1300
+ return {
1301
+ accepted: false,
1302
+ rejectedByPolicy: false,
1303
+ rejectedBySnapshot: true,
1304
+ appliedToRuntime: false,
1305
+ };
1306
+ }
1307
+ session.restoreToFrame(options.frame);
1308
+ session.setInput(0, options.input);
1309
+ session.stepFrames(1);
1310
+ return {
1311
+ accepted: true,
1312
+ rejectedByPolicy: false,
1313
+ rejectedBySnapshot: false,
1314
+ appliedToRuntime: true,
1315
+ };
1316
+ }
1317
+ function stepCoyoteJump(body, input, coyoteFramesRemaining) {
1318
+ /* istanbul ignore next -- movement fixture covers coyote frames remaining; grounded fallback is diagnostic. */
1319
+ return (0, deterministic_primitives_js_1.stepKccLiteBody)({ ...body, grounded: body.grounded || coyoteFramesRemaining > 0 }, input);
1320
+ }
1321
+ function runScaleWorkload(workloadId, game, players, frames, seed, botSlots) {
1322
+ const protocol = (0, protocol_lab_js_1.runProtocolRollbackLab)({
1323
+ game,
1324
+ players,
1325
+ frames,
1326
+ seed,
1327
+ minDelayFrames: 1,
1328
+ maxDelayFrames: 8,
1329
+ duplicatePermille: 100,
1330
+ lossPermille: 20,
1331
+ resendIntervalFrames: 9,
1332
+ maxResendAttempts: 12,
1333
+ botSlots,
1334
+ eventTimelineLimit: 2,
1335
+ });
1336
+ return {
1337
+ workloadId,
1338
+ game: protocol.game,
1339
+ players: protocol.players,
1340
+ result: protocol.result,
1341
+ rollbacks: protocol.rollbacks,
1342
+ maxRollbackDepth: protocol.maxRollbackDepth,
1343
+ botInputFrames: protocol.botInputFrames,
1344
+ authorityChecksum: protocol.authorityChecksum,
1345
+ clientChecksum: protocol.clientChecksum,
1346
+ };
1347
+ }
1348
+ function runScaleCertificationWorkloads(frames, seed) {
1349
+ const run = (0, certification_js_1.runCertificationSuite)({
1350
+ fixtures: [
1351
+ {
1352
+ fixtureId: 'chaos-projectile-heavy',
1353
+ kind: 'projectile-hitscan',
1354
+ required: true,
1355
+ runtimeTarget: 'node',
1356
+ players: 128,
1357
+ frames,
1358
+ seed,
1359
+ budgets: { requireChecksumConvergence: true },
1360
+ },
1361
+ {
1362
+ fixtureId: 'chaos-genre-like',
1363
+ kind: 'genre-128',
1364
+ required: true,
1365
+ runtimeTarget: 'node',
1366
+ players: 128,
1367
+ frames,
1368
+ seed: seed + 1,
1369
+ budgets: { maxRollbackDepth: 120, requireChecksumConvergence: true },
1370
+ },
1371
+ ],
1372
+ });
1373
+ return run.fixtureReports.map((report) => scaleCertificationWorkload(report));
1374
+ }
1375
+ function scaleCertificationWorkload(report) {
1376
+ const metrics = report.metrics;
1377
+ return {
1378
+ workloadId: report.fixtureId === 'chaos-projectile-heavy' ? 'projectile-heavy' : 'genre-like',
1379
+ result: report.result,
1380
+ metrics,
1381
+ checksum: (0, canonical_js_1.defaultChecksum)({ fixtureId: report.fixtureId, metrics }),
1382
+ };
1383
+ }
1384
+ function metricNumber(metrics, key) {
1385
+ const value = metrics[key];
1386
+ /* istanbul ignore next -- scale workload assertions request numeric metrics that focused certification fixtures emit. */
1387
+ return typeof value === 'number' ? value : 0;
1388
+ }
1389
+ function buildExplanationFromFixture(fixtureReport) {
1390
+ /* istanbul ignore next -- explainChaosFixture handles unknown ids before calling this builder. */
1391
+ if (fixtureReport === undefined) {
1392
+ return undefined;
1393
+ }
1394
+ return {
1395
+ id: fixtureReport.fixtureId,
1396
+ category: fixtureReport.category,
1397
+ expectedResult: fixtureReport.expectedResult,
1398
+ validates: Object.keys(fixtureReport.invariants),
1399
+ artifacts: [fixtureReport.artifactPath],
1400
+ invariants: Object.keys(fixtureReport.invariants),
1401
+ };
1402
+ }