@dreamboard-games/sdk 0.4.0-alpha.0 → 0.4.0-alpha.2

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 (84) hide show
  1. package/README.md +8 -1
  2. package/REFERENCE.md +1849 -379
  3. package/dist/{ResourceCounter-BTXklCEZ.d.ts → ResourceCounter-BuiNstXs.d.ts} +5 -1
  4. package/dist/{ThemeProvider-DvMl17Ny.d.ts → ThemeProvider-B1sIoBlX.d.ts} +1 -1
  5. package/dist/attributes-DbvyMbXw.d.ts +68 -0
  6. package/dist/authoring/index.d.ts +66 -0
  7. package/dist/authoring/index.js +560 -0
  8. package/dist/authoring/index.js.map +1 -0
  9. package/dist/browser-interaction.d.ts +246 -44
  10. package/dist/browser-interaction.js +14 -1
  11. package/dist/{chunk-Q5O7GPVY.js → chunk-45FI6XII.js} +630 -375
  12. package/dist/chunk-45FI6XII.js.map +1 -0
  13. package/dist/{chunk-4G7552LO.js → chunk-6JQFGSLD.js} +216 -47
  14. package/dist/chunk-6JQFGSLD.js.map +1 -0
  15. package/dist/{chunk-NBAEEHAU.js → chunk-CP35NPPY.js} +5926 -5266
  16. package/dist/chunk-CP35NPPY.js.map +1 -0
  17. package/dist/{chunk-2LDZ5C3T.js → chunk-FWVV2X74.js} +2 -2
  18. package/dist/{chunk-MNKDSGIA.js → chunk-HWJS3VOJ.js} +6 -2
  19. package/dist/chunk-HWJS3VOJ.js.map +1 -0
  20. package/dist/chunk-K5ZRJTZZ.js +328 -0
  21. package/dist/chunk-K5ZRJTZZ.js.map +1 -0
  22. package/dist/{chunk-U5KBV2BA.js → chunk-N6ZDIMRB.js} +444 -223
  23. package/dist/chunk-N6ZDIMRB.js.map +1 -0
  24. package/dist/chunk-O6M6PN4E.js +1225 -0
  25. package/dist/chunk-O6M6PN4E.js.map +1 -0
  26. package/dist/chunk-PF7L4BMG.js +117 -0
  27. package/dist/chunk-PF7L4BMG.js.map +1 -0
  28. package/dist/{chunk-P7F4L2FF.js → chunk-WOWFONLZ.js} +609 -1287
  29. package/dist/chunk-WOWFONLZ.js.map +1 -0
  30. package/dist/{chunk-QLG6VEMW.js → chunk-WUIVY2KE.js} +318 -21
  31. package/dist/chunk-WUIVY2KE.js.map +1 -0
  32. package/dist/chunk-ZAS2SENW.js +12 -0
  33. package/dist/chunk-ZAS2SENW.js.map +1 -0
  34. package/dist/codegen.d.ts +11 -1
  35. package/dist/codegen.js +1 -1
  36. package/dist/{components-DaIqi9QM.d.ts → components-BbWPnZCF.d.ts} +37 -70
  37. package/dist/{hex-board-view-UCUjMALd.d.ts → hex-board-view-Kz9Q_zsv.d.ts} +1 -1
  38. package/dist/index.js +2 -1
  39. package/dist/package-set.d.ts +2 -2
  40. package/dist/package-set.js +2 -1
  41. package/dist/player-state-Cqpyeql0.d.ts +371 -0
  42. package/dist/protocol-BP193lva.d.ts +267 -0
  43. package/dist/reducer/advanced.d.ts +2 -2
  44. package/dist/reducer/advanced.js +1 -1
  45. package/dist/reducer-contract.js +1 -1
  46. package/dist/reducer.d.ts +7 -7
  47. package/dist/reducer.js +190 -183
  48. package/dist/reducer.js.map +1 -1
  49. package/dist/runtime/primitives.d.ts +10 -9
  50. package/dist/runtime/primitives.js +9 -6
  51. package/dist/runtime/runtime-api.d.ts +2 -2
  52. package/dist/runtime/workspace-contract.d.ts +28 -76
  53. package/dist/runtime/workspace-contract.js +7 -5
  54. package/dist/runtime-api-BJharVaj.d.ts +198 -0
  55. package/dist/runtime-api-C6aWo9c0.d.ts +95 -0
  56. package/dist/runtime-json-CQ9QbLZ5.d.ts +5 -0
  57. package/dist/runtime.d.ts +80 -11
  58. package/dist/runtime.js +89 -445
  59. package/dist/runtime.js.map +1 -1
  60. package/dist/testing.d.ts +643 -22
  61. package/dist/testing.js +1371 -76
  62. package/dist/testing.js.map +1 -1
  63. package/dist/types-CYeN0wLK.d.ts +29 -0
  64. package/dist/{attributes-BeRyboMS.d.ts → types-DJj5MJkl.d.ts} +32 -55
  65. package/dist/types.d.ts +14 -7
  66. package/dist/ui/components.d.ts +4 -5
  67. package/dist/ui/components.js +19 -1
  68. package/dist/ui/player-state.d.ts +2 -365
  69. package/dist/ui/plugin-styles.css +1 -1
  70. package/dist/{ui-contract-BUC6iS3s.d.ts → ui-contract-CY7cxsCC.d.ts} +17 -26
  71. package/dist/ui.d.ts +8 -13
  72. package/dist/ui.js +20 -2
  73. package/dist/{views-B0hlW6IT.d.ts → views-fn2RuXOx.d.ts} +59 -44
  74. package/package.json +28 -4
  75. package/dist/chunk-4G7552LO.js.map +0 -1
  76. package/dist/chunk-MNKDSGIA.js.map +0 -1
  77. package/dist/chunk-NBAEEHAU.js.map +0 -1
  78. package/dist/chunk-P7F4L2FF.js.map +0 -1
  79. package/dist/chunk-Q5O7GPVY.js.map +0 -1
  80. package/dist/chunk-QLG6VEMW.js.map +0 -1
  81. package/dist/chunk-U5KBV2BA.js.map +0 -1
  82. package/dist/hex-color-MhOyuY-o.d.ts +0 -8
  83. package/dist/runtime-api-BXd70c2e.d.ts +0 -490
  84. /package/dist/{chunk-2LDZ5C3T.js.map → chunk-FWVV2X74.js.map} +0 -0
package/dist/testing.js CHANGED
@@ -2,6 +2,27 @@ import {
2
2
  StaleContractArtifactError,
3
3
  isStaleContractArtifactError
4
4
  } from "./chunk-3OZMHZK3.js";
5
+ import {
6
+ PluginRuntimeBoundary,
7
+ createPluginRuntimeClient
8
+ } from "./chunk-K5ZRJTZZ.js";
9
+ import {
10
+ DREAMBOARD_PLUGIN_PROTOCOL,
11
+ DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
12
+ PluginProtocolFrameSchema,
13
+ PluginProtocolStepSchema,
14
+ PluginProtocolTapeSchema,
15
+ computePluginActionSetVersion,
16
+ materializePluginGameplayFrame
17
+ } from "./chunk-O6M6PN4E.js";
18
+ import {
19
+ BROWSER_INTERACTION_ACTUATOR_KINDS,
20
+ DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
21
+ browserInteractionEffectSchema
22
+ } from "./chunk-WUIVY2KE.js";
23
+ import {
24
+ RuntimeJsonSchema
25
+ } from "./chunk-PF7L4BMG.js";
5
26
  import "./chunk-PZ5AY32C.js";
6
27
 
7
28
  // src/testing/definitions.ts
@@ -29,9 +50,9 @@ function matchPartial(actual, expected, path = "value") {
29
50
  if (!(key in actual)) {
30
51
  return `${path}.${key} is missing`;
31
52
  }
32
- const mismatch = matchPartial(actualValue, expectedValue, `${path}.${key}`);
33
- if (mismatch) {
34
- return mismatch;
53
+ const mismatch2 = matchPartial(actualValue, expectedValue, `${path}.${key}`);
54
+ if (mismatch2) {
55
+ return mismatch2;
35
56
  }
36
57
  }
37
58
  return null;
@@ -51,9 +72,9 @@ function assertDescriptorMatches(descriptor, opts) {
51
72
  if (!opts) {
52
73
  return;
53
74
  }
54
- const mismatch = matchPartial(descriptor, opts, "interaction");
55
- if (mismatch) {
56
- throw new Error(mismatch);
75
+ const mismatch2 = matchPartial(descriptor, opts, "interaction");
76
+ if (mismatch2) {
77
+ throw new Error(mismatch2);
57
78
  }
58
79
  }
59
80
  function createSubmissionErrorMatcher(actual, expected, options) {
@@ -170,9 +191,9 @@ function createExpectApi(options = {}) {
170
191
  }
171
192
  },
172
193
  toMatchObject: (expected) => {
173
- const mismatch = matchPartial(actual, expected);
174
- if (mismatch) {
175
- throw new Error(mismatch);
194
+ const mismatch2 = matchPartial(actual, expected);
195
+ if (mismatch2) {
196
+ throw new Error(mismatch2);
176
197
  }
177
198
  },
178
199
  toBeDefined: () => {
@@ -414,7 +435,7 @@ function resolvePlayerIds(options) {
414
435
  function isWireZoneHandles(value) {
415
436
  return typeof value === "object" && value !== null;
416
437
  }
417
- function buildPluginSnapshot(options) {
438
+ function buildPluginFrame(options) {
418
439
  const projection = options.bundle.projectSeatsDynamic({
419
440
  state: options.state,
420
441
  playerIds: [options.playerId]
@@ -449,28 +470,25 @@ function buildPluginSnapshot(options) {
449
470
  ];
450
471
  })
451
472
  );
473
+ const availableInteractions = hydrateRefs(
474
+ seat?.availableInteractionRefs
475
+ );
452
476
  return {
477
+ gameVersion: options.version,
478
+ actionSetVersion: computePluginActionSetVersion({
479
+ gameVersion: options.version,
480
+ availableInteractions
481
+ }),
482
+ perspectivePlayerId: options.playerId || null,
453
483
  view: seat?.view ?? null,
454
- gameplay: {
484
+ flow: {
455
485
  currentPhase: flow.currentPhase,
456
486
  currentStage: projection.currentStage ?? null,
457
487
  activePlayers: flow.activePlayers,
458
- simultaneousPhase: projection.simultaneousPhase ?? null,
459
- availableInteractions: hydrateRefs(
460
- seat?.availableInteractionRefs
461
- ),
462
- zones
488
+ simultaneousPhase: projection.simultaneousPhase ?? null
463
489
  },
464
- lobby: null,
465
- notifications: [],
466
- session: {
467
- sessionId: options.sessionId,
468
- controllablePlayerIds: [...options.playerIds],
469
- controllingPlayerId: options.playerId || null,
470
- userId: options.userId
471
- },
472
- history: null,
473
- syncId: options.version
490
+ availableInteractions,
491
+ zones
474
492
  };
475
493
  }
476
494
  function createTestRuntime(options) {
@@ -491,44 +509,37 @@ function createTestRuntime(options) {
491
509
  baseState,
492
510
  explicitPlayerIds: options.playerIds
493
511
  });
494
- const userId = options.userId ?? "test-user";
495
512
  const sessionId = options.sessionId ?? "test-session";
513
+ const sessionDescriptor = {
514
+ sessionId,
515
+ players: playerIds.map((playerId) => ({
516
+ playerId,
517
+ displayName: options.displayNameByPlayerId?.[playerId] ?? playerId
518
+ }))
519
+ };
496
520
  let version = 0;
497
521
  let submissionCounter = 0;
498
522
  let currentPlayerId = playerIds[0] ?? "";
499
523
  const diagnosticEvents = [];
500
524
  let lastDispatch = null;
501
- const stateListeners = /* @__PURE__ */ new Set();
502
- const sessionListeners = /* @__PURE__ */ new Set();
503
- const toReadySessionState = (snapshot) => ({
504
- ...snapshot.session,
505
- status: "ready"
525
+ const toReadySessionState = (frame) => ({
526
+ status: "ready",
527
+ sessionId,
528
+ controllingPlayerId: frame.perspectivePlayerId
506
529
  });
507
- let lastPluginSnapshot;
530
+ let lastPluginFrame;
508
531
  let lastSessionState;
509
532
  const applyCurrentState = () => {
510
- const previousSession = lastSessionState;
511
533
  version += 1;
512
- lastPluginSnapshot = buildPluginSnapshot({
534
+ lastPluginFrame = buildPluginFrame({
513
535
  state: currentState,
514
536
  bundle: options.bundle,
515
537
  playerId: currentPlayerId,
516
- playerIds,
517
- userId,
518
- sessionId,
519
538
  version,
520
539
  expectedPhase: version === 1 ? options.phase : void 0,
521
540
  baseId: options.baseId
522
541
  });
523
- lastSessionState = toReadySessionState(lastPluginSnapshot);
524
- for (const listener of stateListeners) {
525
- listener(lastPluginSnapshot);
526
- }
527
- if (!previousSession || previousSession.controllingPlayerId !== lastSessionState.controllingPlayerId || previousSession.controllablePlayerIds.join("\0") !== lastSessionState.controllablePlayerIds.join("\0")) {
528
- for (const listener of sessionListeners) {
529
- listener(lastSessionState);
530
- }
531
- }
542
+ lastSessionState = toReadySessionState(lastPluginFrame);
532
543
  };
533
544
  applyCurrentState();
534
545
  const validate = async (playerId, interactionId, params = {}) => {
@@ -607,38 +618,16 @@ function createTestRuntime(options) {
607
618
  interactionId
608
619
  });
609
620
  };
610
- const setControllingPlayer = (playerId) => {
611
- if (!playerIds.includes(playerId)) {
612
- throw new Error(`Unknown controlling player '${playerId}'.`);
613
- }
614
- currentPlayerId = playerId;
615
- applyCurrentState();
616
- };
617
621
  const runtime = {
618
- validateInteraction: validate,
619
- submitInteraction: submit,
622
+ validateInteraction: (interactionId, params) => validate(currentPlayerId, interactionId, params),
623
+ submitInteraction: (interactionId, params) => submit(currentPlayerId, interactionId, params),
620
624
  getSessionState: () => lastSessionState,
621
- disconnect: () => void 0,
622
- switchPlayer: (playerId) => {
623
- setControllingPlayer(playerId);
624
- },
625
- getSnapshot: () => lastPluginSnapshot,
626
- subscribeToState: (listener) => {
627
- stateListeners.add(listener);
628
- return () => {
629
- stateListeners.delete(listener);
630
- };
631
- },
632
- _subscribeToSessionState: (listener) => {
633
- sessionListeners.add(listener);
634
- return () => {
635
- sessionListeners.delete(listener);
636
- };
637
- }
625
+ disconnect: () => void 0
638
626
  };
639
627
  return {
640
628
  runtime,
641
- getSnapshot: () => lastPluginSnapshot,
629
+ getFrame: () => lastPluginFrame,
630
+ getSessionDescriptor: () => sessionDescriptor,
642
631
  players: () => [...playerIds],
643
632
  seat: (index) => {
644
633
  if (!Number.isInteger(index) || index < 0 || index >= playerIds.length) {
@@ -662,16 +651,1322 @@ function createTestRuntime(options) {
662
651
  diagnosticEvents.length = 0;
663
652
  lastDispatch = null;
664
653
  }
654
+ }
655
+ };
656
+ }
657
+
658
+ // src/testing/ui-fixture/canonical.ts
659
+ import { createHash } from "crypto";
660
+ var sha256DigestPattern = /^sha256:[a-f0-9]{64}$/;
661
+ function isSha256Digest(value) {
662
+ return sha256DigestPattern.test(value);
663
+ }
664
+ function canonicalizeUIFixtureJson(value) {
665
+ if (value === null) {
666
+ return null;
667
+ }
668
+ switch (typeof value) {
669
+ case "boolean":
670
+ case "string":
671
+ return value;
672
+ case "number":
673
+ if (Number.isFinite(value)) {
674
+ return value;
675
+ }
676
+ break;
677
+ case "object": {
678
+ if (Array.isArray(value)) {
679
+ return value.map((item) => canonicalizeUIFixtureJson(item));
680
+ }
681
+ if (Object.getPrototypeOf(value) !== Object.prototype) {
682
+ break;
683
+ }
684
+ const entries = Object.entries(value);
685
+ return Object.fromEntries(
686
+ entries.filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalizeUIFixtureJson(item)])
687
+ );
688
+ }
689
+ }
690
+ throw new Error(
691
+ `UI fixture values must be deterministic JSON. Received ${typeof value}.`
692
+ );
693
+ }
694
+ function canonicalUIFixtureJson(value) {
695
+ return JSON.stringify(canonicalizeUIFixtureJson(value));
696
+ }
697
+ function digestUIFixtureJson(value) {
698
+ return sha256(canonicalUIFixtureJson(value));
699
+ }
700
+ function digestUIFixtureRequest(request) {
701
+ return digestUIFixtureJson({
702
+ digestVersion: "ui-replay-request@2",
703
+ request
704
+ });
705
+ }
706
+ function digestUIFixtureTransportRequest(request) {
707
+ return digestUIFixtureJson({
708
+ digestVersion: "ui-fixture-runtime-command@2",
709
+ request
710
+ });
711
+ }
712
+ function digestPluginProtocolTape(tape) {
713
+ return digestUIFixtureJson({
714
+ digestVersion: "plugin-protocol-tape@2",
715
+ tape
716
+ });
717
+ }
718
+ function canonicalizeUIScenarioFixture(fixture) {
719
+ return canonicalizeUIFixtureJson({
720
+ ...fixture,
721
+ tags: [...fixture.tags].sort(),
722
+ environment: {
723
+ ...fixture.environment,
724
+ viewportTags: [...fixture.environment.viewportTags].sort()
725
+ }
726
+ });
727
+ }
728
+ function serializeUIScenarioFixture(fixture) {
729
+ return `${JSON.stringify(canonicalizeUIScenarioFixture(fixture), null, 2)}
730
+ `;
731
+ }
732
+ function digestUIScenarioFixture(fixture) {
733
+ return sha256(serializeUIScenarioFixture(fixture));
734
+ }
735
+ function sha256(value) {
736
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
737
+ }
738
+
739
+ // src/testing/reducer-scenario/create-reducer-scenario-runner.ts
740
+ function createReducerScenarioRunner(options) {
741
+ return {
742
+ async run(operations) {
743
+ let state = structuredClone(options.initialState);
744
+ let gameVersion = options.initialGameVersion ?? 1;
745
+ const frames = [];
746
+ const exchanges = [];
747
+ const appendFrame = (id) => {
748
+ const dynamicProjection = options.bundle.projectSeatsDynamic({
749
+ state,
750
+ playerIds: [options.viewer.playerId]
751
+ });
752
+ const staticProjection = options.bundle.projectStatic?.() ?? null;
753
+ const flow = readFlowState2(state);
754
+ const materialized = materializePluginGameplayFrame({
755
+ currentPhase: flow.currentPhase,
756
+ activePlayers: flow.activePlayers,
757
+ dynamicProjection,
758
+ staticProjection,
759
+ perspectivePlayerId: options.viewer.playerId,
760
+ gameVersion,
761
+ actionSetVersion: "pending"
762
+ });
763
+ const actionSetVersion = computePluginActionSetVersion({
764
+ gameVersion,
765
+ availableInteractions: materialized.availableInteractions
766
+ });
767
+ const frame = {
768
+ id,
769
+ reducerState: structuredClone(state),
770
+ dynamicProjection,
771
+ staticProjection,
772
+ gameVersion,
773
+ actionSetVersion,
774
+ projectionDigest: digestUIFixtureJson({
775
+ digestVersion: "reducer-scenario-frame@2",
776
+ scenarioId: options.scenarioId,
777
+ frameId: id,
778
+ dynamicProjection,
779
+ staticProjection,
780
+ gameVersion,
781
+ actionSetVersion
782
+ })
783
+ };
784
+ frames.push(frame);
785
+ return frame;
786
+ };
787
+ appendFrame("frame-1");
788
+ for (const operation of operations) {
789
+ const fromFrame = frames[frames.length - 1];
790
+ if (!fromFrame) {
791
+ throw new Error("Reducer scenario runner lost its initial frame.");
792
+ }
793
+ if (operation.operation === "validate") {
794
+ const result2 = await options.bundle.validateInput({
795
+ state,
796
+ input: operation.input
797
+ });
798
+ exchanges.push({
799
+ id: operation.id,
800
+ operation: "validate",
801
+ fromFrameId: fromFrame.id,
802
+ input: operation.input,
803
+ result: result2
804
+ });
805
+ continue;
806
+ }
807
+ const result = await options.bundle.dispatch({
808
+ state,
809
+ input: operation.input
810
+ });
811
+ if (result.kind === "reject") {
812
+ exchanges.push({
813
+ id: operation.id,
814
+ operation: "submit",
815
+ fromFrameId: fromFrame.id,
816
+ input: operation.input,
817
+ result: {
818
+ kind: "rejected",
819
+ diagnostics: [
820
+ {
821
+ code: result.errorCode,
822
+ message: result.message ?? result.errorCode
823
+ }
824
+ ]
825
+ }
826
+ });
827
+ continue;
828
+ }
829
+ state = structuredClone(result.state);
830
+ gameVersion += 1;
831
+ const toFrame = appendFrame(`frame-${frames.length + 1}`);
832
+ exchanges.push({
833
+ id: operation.id,
834
+ operation: "submit",
835
+ fromFrameId: fromFrame.id,
836
+ input: operation.input,
837
+ result: {
838
+ kind: "accepted",
839
+ toFrameId: toFrame.id
840
+ }
841
+ });
842
+ }
843
+ return {
844
+ scenarioId: options.scenarioId,
845
+ gameId: options.gameId,
846
+ viewer: options.viewer,
847
+ frames,
848
+ exchanges
849
+ };
850
+ }
851
+ };
852
+ }
853
+ function readFlowState2(state) {
854
+ return {
855
+ currentPhase: state.domain?.flow?.currentPhase ?? null,
856
+ activePlayers: [...state.domain?.flow?.activePlayers ?? []]
857
+ };
858
+ }
859
+
860
+ // src/testing/ui-fixture/parity.ts
861
+ var topLevelComparisons = [
862
+ { path: "scenarioId", code: "fixture-source-mismatch" },
863
+ { path: "fixtureDigest", code: "candidate-mismatch" },
864
+ { path: "sdkCandidateDigest", code: "candidate-mismatch" },
865
+ { path: "pluginRuntimeProtocol", code: "protocol-mismatch" },
866
+ { path: "browserInteractionProtocol", code: "protocol-mismatch" },
867
+ { path: "environment.project", code: "host-setup-failure" },
868
+ { path: "environment.viewport.width", code: "host-setup-failure" },
869
+ { path: "environment.viewport.height", code: "host-setup-failure" }
870
+ ];
871
+ var checkpointComparisons = [
872
+ { path: "stepId", code: "interaction-resolution-mismatch" },
873
+ { path: "interactionKey", code: "interaction-resolution-mismatch" },
874
+ { path: "interactionId", code: "interaction-resolution-mismatch" },
875
+ { path: "actuatorId", code: "interaction-resolution-mismatch" },
876
+ { path: "descriptorDigest", code: "interaction-resolution-mismatch" },
877
+ { path: "gameVersion", code: "projection-mismatch" },
878
+ { path: "actionSetVersion", code: "projection-mismatch" },
879
+ { path: "perspectivePlayerId", code: "projection-mismatch" },
880
+ { path: "projectionDigest", code: "projection-mismatch" },
881
+ { path: "semanticDigest", code: "semantic-snapshot-mismatch" },
882
+ { path: "draftDigest", code: "draft-mismatch" },
883
+ { path: "submissionDigest", code: "submission-mismatch" },
884
+ { path: "screenshot", code: "visual-mismatch" }
885
+ ];
886
+ function createUIParityObservationFromFixture({
887
+ fixture,
888
+ sdkCandidateDigest,
889
+ environment,
890
+ fixtureDigest = digestUIScenarioFixture(fixture),
891
+ screenshotsByStepId = {},
892
+ diagnostics = []
893
+ }) {
894
+ const frameById = new Map(
895
+ fixture.protocol.frames.map((frame) => [frame.id, frame])
896
+ );
897
+ const firstFrame = fixture.protocol.frames[0];
898
+ if (!firstFrame) {
899
+ throw new Error(
900
+ `Fixture '${fixture.id}' does not contain protocol frames.`
901
+ );
902
+ }
903
+ const checkpoints = fixture.replay.map((step, index) => {
904
+ const expectedFrameId = step.expect.frameId ?? firstFrame.id;
905
+ const frame = frameById.get(expectedFrameId);
906
+ if (!frame) {
907
+ throw new Error(
908
+ `Fixture '${fixture.id}' replay step '${step.stepId}' references missing frame '${expectedFrameId}'.`
909
+ );
910
+ }
911
+ const identity = "expectedIdentity" in step ? step.expectedIdentity : null;
912
+ const projectionDigest = step.expect.projectionDigest ?? frame.projectionDigest;
913
+ const isFinal = index === fixture.replay.length - 1;
914
+ const semanticDigest = step.expect.semanticDigest ?? (isFinal ? fixture.expected.finalSemanticDigest : digestUIFixtureJson({
915
+ scenarioId: fixture.id,
916
+ stepId: step.stepId,
917
+ frameId: frame.id,
918
+ projectionDigest
919
+ }));
920
+ const submissionDigest = step.expect.submissionDigest ?? (isFinal ? fixture.expected.submissionDigest : void 0);
921
+ return {
922
+ stepId: step.stepId,
923
+ interactionKey: identity?.interactionKey,
924
+ interactionId: identity?.interactionId,
925
+ actuatorId: identity?.actuatorId,
926
+ descriptorDigest: identity?.descriptorDigest,
927
+ draftDigest: step.expect.draftDigest ?? identity?.draftDigest,
928
+ gameVersion: frame.frame.gameVersion,
929
+ actionSetVersion: frame.frame.actionSetVersion,
930
+ perspectivePlayerId: frame.frame.perspectivePlayerId,
931
+ projectionDigest,
932
+ semanticDigest,
933
+ submissionDigest,
934
+ screenshot: screenshotsByStepId[step.stepId]
935
+ };
936
+ });
937
+ return {
938
+ schemaVersion: 1,
939
+ scenarioId: fixture.id,
940
+ fixtureDigest,
941
+ sdkCandidateDigest,
942
+ pluginRuntimeProtocol: fixture.pluginRuntimeProtocol,
943
+ browserInteractionProtocol: fixture.browserInteractionProtocol,
944
+ environment,
945
+ checkpoints,
946
+ diagnostics
947
+ };
948
+ }
949
+ function parseUIParityObservationV1(value) {
950
+ const observation = assertRecord(value, "observation");
951
+ if (observation.schemaVersion !== 1) {
952
+ throw new Error("UI parity observation schemaVersion must be 1.");
953
+ }
954
+ assertString(observation.scenarioId, "scenarioId");
955
+ assertString(observation.fixtureDigest, "fixtureDigest");
956
+ assertString(observation.sdkCandidateDigest, "sdkCandidateDigest");
957
+ if (observation.pluginRuntimeProtocol !== 3) {
958
+ throw new Error("UI parity observation pluginRuntimeProtocol must be 3.");
959
+ }
960
+ assertString(
961
+ observation.browserInteractionProtocol,
962
+ "browserInteractionProtocol"
963
+ );
964
+ const environment = assertRecord(observation.environment, "environment");
965
+ assertString(environment.project, "environment.project");
966
+ const viewport = assertRecord(environment.viewport, "environment.viewport");
967
+ assertNumber(viewport.width, "environment.viewport.width");
968
+ assertNumber(viewport.height, "environment.viewport.height");
969
+ const checkpoints = assertArray(observation.checkpoints, "checkpoints");
970
+ for (let index = 0; index < checkpoints.length; index += 1) {
971
+ const checkpoint = assertRecord(
972
+ checkpoints[index],
973
+ `checkpoints[${index}]`
974
+ );
975
+ assertString(checkpoint.stepId, `checkpoints[${index}].stepId`);
976
+ assertOptionalString(
977
+ checkpoint.interactionKey,
978
+ `checkpoints[${index}].interactionKey`
979
+ );
980
+ assertOptionalString(
981
+ checkpoint.interactionId,
982
+ `checkpoints[${index}].interactionId`
983
+ );
984
+ assertOptionalString(
985
+ checkpoint.actuatorId,
986
+ `checkpoints[${index}].actuatorId`
987
+ );
988
+ assertOptionalString(
989
+ checkpoint.descriptorDigest,
990
+ `checkpoints[${index}].descriptorDigest`
991
+ );
992
+ assertOptionalString(
993
+ checkpoint.draftDigest,
994
+ `checkpoints[${index}].draftDigest`
995
+ );
996
+ assertNumber(checkpoint.gameVersion, `checkpoints[${index}].gameVersion`);
997
+ assertString(
998
+ checkpoint.actionSetVersion,
999
+ `checkpoints[${index}].actionSetVersion`
1000
+ );
1001
+ if (checkpoint.perspectivePlayerId !== null && typeof checkpoint.perspectivePlayerId !== "string") {
1002
+ throw new Error(
1003
+ `checkpoints[${index}].perspectivePlayerId must be a string or null.`
1004
+ );
1005
+ }
1006
+ assertString(
1007
+ checkpoint.projectionDigest,
1008
+ `checkpoints[${index}].projectionDigest`
1009
+ );
1010
+ assertString(
1011
+ checkpoint.semanticDigest,
1012
+ `checkpoints[${index}].semanticDigest`
1013
+ );
1014
+ assertOptionalString(
1015
+ checkpoint.submissionDigest,
1016
+ `checkpoints[${index}].submissionDigest`
1017
+ );
1018
+ assertOptionalString(
1019
+ checkpoint.screenshot,
1020
+ `checkpoints[${index}].screenshot`
1021
+ );
1022
+ }
1023
+ const diagnostics = assertArray(observation.diagnostics, "diagnostics");
1024
+ for (let index = 0; index < diagnostics.length; index += 1) {
1025
+ const diagnostic = assertRecord(
1026
+ diagnostics[index],
1027
+ `diagnostics[${index}]`
1028
+ );
1029
+ assertString(diagnostic.code, `diagnostics[${index}].code`);
1030
+ assertString(diagnostic.message, `diagnostics[${index}].message`);
1031
+ }
1032
+ return observation;
1033
+ }
1034
+ function compareUIParityObservations(expected, actual) {
1035
+ for (const comparison of topLevelComparisons) {
1036
+ const result = comparePath(expected, actual, comparison);
1037
+ if (result) {
1038
+ return result;
1039
+ }
1040
+ }
1041
+ if (expected.checkpoints.length !== actual.checkpoints.length) {
1042
+ return mismatch({
1043
+ code: "interaction-resolution-mismatch",
1044
+ path: "checkpoints.length",
1045
+ expected: expected.checkpoints.length,
1046
+ actual: actual.checkpoints.length
1047
+ });
1048
+ }
1049
+ for (let index = 0; index < expected.checkpoints.length; index += 1) {
1050
+ const expectedCheckpoint = expected.checkpoints[index];
1051
+ const actualCheckpoint = actual.checkpoints[index];
1052
+ for (const comparison of checkpointComparisons) {
1053
+ const result = comparePath(
1054
+ expectedCheckpoint,
1055
+ actualCheckpoint,
1056
+ comparison,
1057
+ {
1058
+ prefix: `checkpoints[${index}]`,
1059
+ checkpointIndex: index,
1060
+ stepId: expectedCheckpoint.stepId
1061
+ }
1062
+ );
1063
+ if (result) {
1064
+ return result;
1065
+ }
1066
+ }
1067
+ }
1068
+ if (!sameValue(expected.diagnostics, actual.diagnostics)) {
1069
+ return mismatch({
1070
+ code: "host-setup-failure",
1071
+ path: "diagnostics",
1072
+ expected: expected.diagnostics,
1073
+ actual: actual.diagnostics
1074
+ });
1075
+ }
1076
+ return { ok: true };
1077
+ }
1078
+ function comparePath(expected, actual, comparison, options = {}) {
1079
+ const expectedValue = getPath(expected, comparison.path);
1080
+ const actualValue = getPath(actual, comparison.path);
1081
+ if (sameValue(expectedValue, actualValue)) {
1082
+ return null;
1083
+ }
1084
+ const path = options.prefix ? `${options.prefix}.${comparison.path}` : comparison.path;
1085
+ return mismatch({
1086
+ code: comparison.code,
1087
+ path,
1088
+ expected: expectedValue,
1089
+ actual: actualValue,
1090
+ checkpointIndex: options.checkpointIndex,
1091
+ stepId: options.stepId
1092
+ });
1093
+ }
1094
+ function mismatch(input) {
1095
+ return {
1096
+ ok: false,
1097
+ failure: {
1098
+ ...input,
1099
+ message: `${input.code} at ${input.path}`
1100
+ }
1101
+ };
1102
+ }
1103
+ function sameValue(left, right) {
1104
+ if (left === void 0 || right === void 0) {
1105
+ return left === right;
1106
+ }
1107
+ return canonicalUIFixtureJson(left) === canonicalUIFixtureJson(right);
1108
+ }
1109
+ function getPath(value, path) {
1110
+ return path.split(".").reduce((current, key) => {
1111
+ if (current === null || typeof current !== "object") {
1112
+ return void 0;
1113
+ }
1114
+ return current[key];
1115
+ }, value);
1116
+ }
1117
+ function assertRecord(value, path) {
1118
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1119
+ throw new Error(`${path} must be an object.`);
1120
+ }
1121
+ return value;
1122
+ }
1123
+ function assertArray(value, path) {
1124
+ if (!Array.isArray(value)) {
1125
+ throw new Error(`${path} must be an array.`);
1126
+ }
1127
+ return value;
1128
+ }
1129
+ function assertString(value, path) {
1130
+ if (typeof value !== "string" || value.length === 0) {
1131
+ throw new Error(`${path} must be a non-empty string.`);
1132
+ }
1133
+ }
1134
+ function assertOptionalString(value, path) {
1135
+ if (value !== void 0 && (typeof value !== "string" || value.length === 0)) {
1136
+ throw new Error(`${path} must be a non-empty string when present.`);
1137
+ }
1138
+ }
1139
+ function assertNumber(value, path) {
1140
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1141
+ throw new Error(`${path} must be a finite number.`);
1142
+ }
1143
+ }
1144
+
1145
+ // src/testing/ui-fixture/create-fixture-host-harness.ts
1146
+ var DEFAULT_CHANNEL_ID = "fixture-channel";
1147
+ function createFixtureHostHarness(options) {
1148
+ const strict = options.strict ?? true;
1149
+ const latencyMs = options.latencyMs ?? 0;
1150
+ const channelId = options.channelId ?? DEFAULT_CHANNEL_ID;
1151
+ const events = [];
1152
+ const frameById = new Map(
1153
+ options.tape.frames.map((frame) => [frame.id, frame])
1154
+ );
1155
+ let eventSequence = 0;
1156
+ let envelopeSequence = 0;
1157
+ let stepCursor = 0;
1158
+ let currentFrameId = null;
1159
+ let onMessage = null;
1160
+ let firstFixtureError = null;
1161
+ const pendingHandlers = /* @__PURE__ */ new Set();
1162
+ const record = (event) => {
1163
+ const entry = {
1164
+ sequence: ++eventSequence,
1165
+ atMs: options.nowMs?.() ?? eventSequence,
1166
+ ...event
1167
+ };
1168
+ events.push(entry);
1169
+ options.onEvent?.(entry);
1170
+ };
1171
+ const createFixtureError = (code, message) => {
1172
+ record({ kind: "diagnostic" });
1173
+ const error = new Error(message);
1174
+ error.name = "FixtureHostHarnessError";
1175
+ Object.assign(error, { errorCode: code });
1176
+ return error;
1177
+ };
1178
+ const rememberFixtureError = (error) => {
1179
+ if (firstFixtureError) {
1180
+ return;
1181
+ }
1182
+ firstFixtureError = error instanceof Error ? error : createFixtureError(
1183
+ "fixture-unknown-error",
1184
+ "Fixture host failed with an unknown error."
1185
+ );
1186
+ };
1187
+ const throwFixtureError = () => {
1188
+ if (firstFixtureError) {
1189
+ throw firstFixtureError;
1190
+ }
1191
+ };
1192
+ const sendHostPayload = (payload) => {
1193
+ if (!onMessage) {
1194
+ return;
1195
+ }
1196
+ onMessage({
1197
+ protocol: DREAMBOARD_PLUGIN_PROTOCOL,
1198
+ version: DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
1199
+ channelId,
1200
+ sequence: ++envelopeSequence,
1201
+ payload
1202
+ });
1203
+ };
1204
+ const sendInit = () => {
1205
+ sendHostPayload({
1206
+ type: "runtime.init",
1207
+ session: options.tape.session
1208
+ });
1209
+ };
1210
+ const publishFrame = (frameId) => {
1211
+ const frame = frameById.get(frameId);
1212
+ if (!frame) {
1213
+ throw createFixtureError(
1214
+ "fixture-missing-frame",
1215
+ `Plugin protocol tape references missing frame '${frameId}'.`
1216
+ );
1217
+ }
1218
+ currentFrameId = frame.id;
1219
+ record({
1220
+ kind: "frame-sent",
1221
+ frameId: frame.id,
1222
+ projectionDigest: frame.projectionDigest
1223
+ });
1224
+ sendHostPayload({
1225
+ type: "gameplay.frame",
1226
+ frame: frame.frame
1227
+ });
1228
+ };
1229
+ const drainHostFrames = () => {
1230
+ while (options.tape.steps[stepCursor]?.kind === "host.frame") {
1231
+ const step = options.tape.steps[stepCursor];
1232
+ stepCursor += 1;
1233
+ publishFrame(step.frameId);
1234
+ }
1235
+ };
1236
+ const drainOneHostFrame = () => {
1237
+ if (options.tape.steps[stepCursor]?.kind !== "host.frame") {
1238
+ return;
1239
+ }
1240
+ const step = options.tape.steps[stepCursor];
1241
+ stepCursor += 1;
1242
+ publishFrame(step.frameId);
1243
+ };
1244
+ const waitForConfiguredLatency = async () => {
1245
+ if (latencyMs <= 0) {
1246
+ await Promise.resolve();
1247
+ return;
1248
+ }
1249
+ await new Promise((resolve) => setTimeout(resolve, latencyMs));
1250
+ };
1251
+ const consumeExpectedStep = (kind, requestDigest) => {
1252
+ const step = options.tape.steps[stepCursor];
1253
+ if (!step) {
1254
+ throw createFixtureError(
1255
+ "fixture-protocol-exhausted",
1256
+ `Plugin protocol tape has no remaining step for ${kind}.`
1257
+ );
1258
+ }
1259
+ if (step.kind !== kind) {
1260
+ throw createFixtureError(
1261
+ "fixture-protocol-step-mismatch",
1262
+ `Expected protocol step '${step.kind}' but received '${kind}'.`
1263
+ );
1264
+ }
1265
+ if (step.fromFrameId !== currentFrameId) {
1266
+ throw createFixtureError(
1267
+ "fixture-protocol-frame-mismatch",
1268
+ `Expected protocol from frame '${step.fromFrameId}' but current frame is '${currentFrameId ?? "<none>"}'.`
1269
+ );
1270
+ }
1271
+ if (step.requestDigest !== requestDigest) {
1272
+ const mode = strict ? "strict" : "compatible";
1273
+ throw createFixtureError(
1274
+ "fixture-protocol-digest-mismatch",
1275
+ `${mode} fixture protocol digest mismatch for ${kind}: expected ${step.requestDigest}, received ${requestDigest}.`
1276
+ );
1277
+ }
1278
+ stepCursor += 1;
1279
+ return step;
1280
+ };
1281
+ const handleValidate = async (command) => {
1282
+ const requestDigest = digestUIFixtureTransportRequest({
1283
+ operation: "validate",
1284
+ basis: command.basis,
1285
+ interactionId: command.interactionId,
1286
+ payload: command.params
1287
+ });
1288
+ const step = consumeExpectedStep("client.validate", requestDigest);
1289
+ record({
1290
+ kind: "validate-received",
1291
+ requestDigest,
1292
+ result: step.response.valid ? "accepted" : "rejected"
1293
+ });
1294
+ await waitForConfiguredLatency();
1295
+ sendHostPayload({
1296
+ type: "interaction.validation-result",
1297
+ requestId: command.requestId,
1298
+ result: step.response
1299
+ });
1300
+ };
1301
+ const handleSubmit = async (command) => {
1302
+ const requestDigest = digestUIFixtureTransportRequest({
1303
+ operation: "submit",
1304
+ basis: command.basis,
1305
+ interactionId: command.interactionId,
1306
+ payload: command.params
1307
+ });
1308
+ const step = consumeExpectedStep("client.submit", requestDigest);
1309
+ record({
1310
+ kind: "submit-received",
1311
+ requestDigest,
1312
+ result: step.response.accepted ? "accepted" : "rejected"
1313
+ });
1314
+ await waitForConfiguredLatency();
1315
+ sendHostPayload({
1316
+ type: "interaction.submit-result",
1317
+ requestId: command.requestId,
1318
+ result: step.response
1319
+ });
1320
+ if (step.response.accepted) {
1321
+ await waitForConfiguredLatency();
1322
+ drainHostFrames();
1323
+ }
1324
+ };
1325
+ const handleClientPayload = (payload) => {
1326
+ const trackHandler = (promise) => {
1327
+ pendingHandlers.add(promise);
1328
+ promise.catch((error) => {
1329
+ rememberFixtureError(error);
1330
+ }).finally(() => {
1331
+ pendingHandlers.delete(promise);
1332
+ });
1333
+ };
1334
+ switch (payload.type) {
1335
+ case "runtime.ready":
1336
+ record({ kind: "ready-received" });
1337
+ break;
1338
+ case "runtime.ack":
1339
+ record({ kind: "ack-received" });
1340
+ break;
1341
+ case "interaction.validate":
1342
+ trackHandler(handleValidate(payload));
1343
+ break;
1344
+ case "interaction.submit":
1345
+ trackHandler(handleSubmit(payload));
1346
+ break;
1347
+ case "runtime.error":
1348
+ record({ kind: "diagnostic" });
1349
+ break;
1350
+ default: {
1351
+ const _exhaustive = payload;
1352
+ return _exhaustive;
1353
+ }
1354
+ }
1355
+ };
1356
+ const transport = {
1357
+ start(nextOnMessage) {
1358
+ onMessage = nextOnMessage;
1359
+ queueMicrotask(() => {
1360
+ sendInit();
1361
+ drainOneHostFrame();
1362
+ });
1363
+ return () => {
1364
+ onMessage = null;
1365
+ };
1366
+ },
1367
+ send(message) {
1368
+ handleClientPayload(message);
1369
+ }
1370
+ };
1371
+ return {
1372
+ transport,
1373
+ tape: options.tape,
1374
+ reset() {
1375
+ eventSequence = 0;
1376
+ envelopeSequence = 0;
1377
+ stepCursor = 0;
1378
+ currentFrameId = null;
1379
+ firstFixtureError = null;
1380
+ pendingHandlers.clear();
1381
+ events.length = 0;
1382
+ if (onMessage) {
1383
+ sendInit();
1384
+ drainOneHostFrame();
1385
+ }
1386
+ },
1387
+ async flush() {
1388
+ await waitForConfiguredLatency();
1389
+ await Promise.resolve();
1390
+ while (pendingHandlers.size > 0) {
1391
+ await Promise.allSettled([...pendingHandlers]);
1392
+ }
1393
+ throwFixtureError();
1394
+ },
1395
+ async advanceHost() {
1396
+ await waitForConfiguredLatency();
1397
+ drainHostFrames();
1398
+ throwFixtureError();
1399
+ },
1400
+ getCurrentFrameId() {
1401
+ if (!currentFrameId) {
1402
+ throw createFixtureError(
1403
+ "fixture-frame-not-started",
1404
+ "Fixture host has not emitted a gameplay frame."
1405
+ );
1406
+ }
1407
+ return currentFrameId;
665
1408
  },
666
- setControllingPlayer
1409
+ getEvents() {
1410
+ return events.map((event) => ({ ...event }));
1411
+ },
1412
+ assertConsumed() {
1413
+ throwFixtureError();
1414
+ if (stepCursor !== options.tape.steps.length) {
1415
+ throw createFixtureError(
1416
+ "fixture-unconsumed-protocol-steps",
1417
+ `Plugin protocol tape consumed ${stepCursor} of ${options.tape.steps.length} protocol step(s).`
1418
+ );
1419
+ }
1420
+ }
1421
+ };
1422
+ }
1423
+
1424
+ // src/testing/ui-fixture/FixturePluginRuntime.tsx
1425
+ import { useMemo } from "react";
1426
+ import { jsx } from "react/jsx-runtime";
1427
+ function FixturePluginRuntime({
1428
+ harness,
1429
+ runtime,
1430
+ children
1431
+ }) {
1432
+ const fixtureRuntime = useMemo(
1433
+ () => runtime ?? createPluginRuntimeClient({ transport: harness.transport }),
1434
+ [harness.transport, runtime]
1435
+ );
1436
+ return /* @__PURE__ */ jsx(PluginRuntimeBoundary, { runtime: fixtureRuntime, children });
1437
+ }
1438
+
1439
+ // src/testing/ui-fixture/compile-plugin-protocol-tape.ts
1440
+ function compilePluginProtocolTape(options) {
1441
+ const frames = options.trace.frames.map(
1442
+ (frame) => materializeTraceFrame(frame, options.trace.viewer.playerId)
1443
+ );
1444
+ const frameById = new Map(frames.map((frame) => [frame.id, frame]));
1445
+ const steps = [];
1446
+ const firstFrame = frames[0];
1447
+ if (!firstFrame) {
1448
+ throw new Error(
1449
+ `Reducer scenario '${options.trace.scenarioId}' did not record frames.`
1450
+ );
1451
+ }
1452
+ steps.push({
1453
+ id: `${firstFrame.id}.host-frame`,
1454
+ kind: "host.frame",
1455
+ frameId: firstFrame.id
1456
+ });
1457
+ for (const exchange of options.trace.exchanges) {
1458
+ const fromFrame = frameById.get(exchange.fromFrameId);
1459
+ if (!fromFrame) {
1460
+ throw new Error(
1461
+ `Reducer exchange '${exchange.id}' references missing frame '${exchange.fromFrameId}'.`
1462
+ );
1463
+ }
1464
+ if (exchange.operation === "validate") {
1465
+ steps.push({
1466
+ id: exchange.id,
1467
+ kind: "client.validate",
1468
+ fromFrameId: exchange.fromFrameId,
1469
+ requestDigest: digestRuntimeCommand(exchange, fromFrame),
1470
+ response: {
1471
+ valid: exchange.result.valid,
1472
+ errorCode: exchange.result.errorCode,
1473
+ message: exchange.result.message
1474
+ }
1475
+ });
1476
+ continue;
1477
+ }
1478
+ steps.push({
1479
+ id: exchange.id,
1480
+ kind: "client.submit",
1481
+ fromFrameId: exchange.fromFrameId,
1482
+ requestDigest: digestRuntimeCommand(exchange, fromFrame),
1483
+ response: exchange.result.kind === "accepted" ? { accepted: true } : {
1484
+ accepted: false,
1485
+ errorCode: exchange.result.diagnostics[0]?.code ?? "fixture-rejected",
1486
+ message: exchange.result.diagnostics[0]?.message
1487
+ }
1488
+ });
1489
+ if (exchange.result.kind === "accepted") {
1490
+ if (!frameById.has(exchange.result.toFrameId)) {
1491
+ throw new Error(
1492
+ `Reducer exchange '${exchange.id}' references missing toFrameId '${exchange.result.toFrameId}'.`
1493
+ );
1494
+ }
1495
+ steps.push({
1496
+ id: `${exchange.id}.host-frame`,
1497
+ kind: "host.frame",
1498
+ frameId: exchange.result.toFrameId
1499
+ });
1500
+ }
1501
+ }
1502
+ return {
1503
+ session: options.session,
1504
+ frames,
1505
+ steps
1506
+ };
1507
+ }
1508
+ function materializeTraceFrame(frame, perspectivePlayerId) {
1509
+ const flow = readFlowState3(frame.reducerState);
1510
+ return {
1511
+ id: frame.id,
1512
+ frame: materializePluginGameplayFrame({
1513
+ currentPhase: flow.currentPhase,
1514
+ activePlayers: flow.activePlayers,
1515
+ dynamicProjection: frame.dynamicProjection,
1516
+ staticProjection: frame.staticProjection,
1517
+ perspectivePlayerId,
1518
+ gameVersion: frame.gameVersion,
1519
+ actionSetVersion: frame.actionSetVersion
1520
+ }),
1521
+ projectionDigest: frame.projectionDigest
1522
+ };
1523
+ }
1524
+ function digestRuntimeCommand(exchange, frame) {
1525
+ return digestUIFixtureTransportRequest({
1526
+ operation: exchange.operation === "validate" ? "validate" : "submit",
1527
+ basis: {
1528
+ gameVersion: frame.frame.gameVersion,
1529
+ actionSetVersion: frame.frame.actionSetVersion,
1530
+ perspectivePlayerId: frame.frame.perspectivePlayerId
1531
+ },
1532
+ interactionId: exchange.input.interactionId,
1533
+ payload: exchange.input.params
1534
+ });
1535
+ }
1536
+ function readFlowState3(state) {
1537
+ return {
1538
+ currentPhase: state.domain?.flow?.currentPhase ?? null,
1539
+ activePlayers: [...state.domain?.flow?.activePlayers ?? []]
1540
+ };
1541
+ }
1542
+
1543
+ // src/testing/ui-fixture/deterministic-environment.ts
1544
+ var defaultFixtureEnvironmentInit = {
1545
+ clockIso: "2026-01-01T00:00:00.000Z",
1546
+ randomSeed: "dreamboard-ui-fixture",
1547
+ locale: "en-US",
1548
+ timezone: "UTC",
1549
+ reducedMotion: true,
1550
+ network: "blocked"
1551
+ };
1552
+ function createDeterministicIdFactory(seed) {
1553
+ let cursor = 0;
1554
+ return () => {
1555
+ cursor += 1;
1556
+ return `fixture-${slug(seed)}-${cursor.toString(36).padStart(4, "0")}`;
667
1557
  };
668
1558
  }
1559
+ function fixtureEnvironmentInitFor(options) {
1560
+ return {
1561
+ ...defaultFixtureEnvironmentInit,
1562
+ clockIso: options.clockIso,
1563
+ randomSeed: options.randomSeed
1564
+ };
1565
+ }
1566
+ function slug(value) {
1567
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 48) || "seed";
1568
+ }
1569
+
1570
+ // src/testing/ui-fixture/schema.ts
1571
+ import { z } from "zod";
1572
+ var UI_SCENARIO_FIXTURE_SCHEMA_VERSION = 2;
1573
+ var UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION = 2;
1574
+ var UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR = 3;
1575
+ var UI_SCENARIO_FIXTURE_PLUGIN_RUNTIME_PROTOCOL = DREAMBOARD_PLUGIN_PROTOCOL_VERSION;
1576
+ var digestSchema = z.string().refine(isSha256Digest, {
1577
+ message: "Expected a sha256:<64 lowercase hex> digest."
1578
+ });
1579
+ var nonEmptyStringSchema = z.string().min(1);
1580
+ var jsonSchema = RuntimeJsonSchema;
1581
+ var environmentSchema = z.object({
1582
+ clockIso: nonEmptyStringSchema,
1583
+ randomSeed: nonEmptyStringSchema,
1584
+ locale: z.literal("en-US"),
1585
+ timezone: z.literal("UTC"),
1586
+ viewportTags: z.array(z.enum(["desktop", "tablet", "phone", "touch"]))
1587
+ }).strict();
1588
+ var uiFixtureFrameSchema = PluginProtocolFrameSchema;
1589
+ var uiFixtureProtocolStepSchema = PluginProtocolStepSchema;
1590
+ var pluginProtocolTapeSchema = PluginProtocolTapeSchema.refine(
1591
+ (tape) => tape.frames.length > 0,
1592
+ "UI fixture protocol tapes require at least one gameplay frame."
1593
+ );
1594
+ var browserInteractionIntentRequestSchema = z.object({
1595
+ surface: nonEmptyStringSchema,
1596
+ scopeId: nonEmptyStringSchema.optional(),
1597
+ interactionKey: nonEmptyStringSchema.optional(),
1598
+ interactionId: nonEmptyStringSchema.optional(),
1599
+ intent: nonEmptyStringSchema,
1600
+ inputKey: nonEmptyStringSchema.optional(),
1601
+ candidateValue: jsonSchema.optional(),
1602
+ candidateValueKey: nonEmptyStringSchema.optional(),
1603
+ actuatorKind: z.enum(BROWSER_INTERACTION_ACTUATOR_KINDS).optional(),
1604
+ allowDisabled: z.boolean().optional()
1605
+ }).strict();
1606
+ var browserInteractionEffectRequestSchema = z.object({
1607
+ surface: nonEmptyStringSchema,
1608
+ scopeId: nonEmptyStringSchema.optional(),
1609
+ interactionKey: nonEmptyStringSchema.optional(),
1610
+ interactionId: nonEmptyStringSchema.optional(),
1611
+ effect: browserInteractionEffectSchema,
1612
+ allowDisabled: z.boolean().optional()
1613
+ }).strict();
1614
+ var uiReplayRequestSchema = z.union([
1615
+ browserInteractionIntentRequestSchema,
1616
+ browserInteractionEffectRequestSchema
1617
+ ]);
1618
+ var uiReplayExecutionSchema = z.union([
1619
+ z.object({
1620
+ kind: z.literal("activate")
1621
+ }).strict(),
1622
+ z.object({
1623
+ kind: z.literal("fill"),
1624
+ value: z.string()
1625
+ }).strict(),
1626
+ z.object({
1627
+ kind: z.literal("drag"),
1628
+ target: browserInteractionEffectRequestSchema
1629
+ }).strict()
1630
+ ]);
1631
+ var uiStepExpectationSchema = z.object({
1632
+ frameId: nonEmptyStringSchema.optional(),
1633
+ projectionDigest: digestSchema.optional(),
1634
+ semanticDigest: digestSchema.optional(),
1635
+ draftDigest: digestSchema.optional(),
1636
+ submissionDigest: digestSchema.optional(),
1637
+ focusedInteractionKey: nonEmptyStringSchema.optional(),
1638
+ visibleInteractionKeys: z.array(nonEmptyStringSchema).optional()
1639
+ }).strict();
1640
+ var uiResolvedReplayIdentitySchema = z.object({
1641
+ stepId: nonEmptyStringSchema,
1642
+ surface: nonEmptyStringSchema,
1643
+ scopeId: nonEmptyStringSchema,
1644
+ interactionKey: nonEmptyStringSchema,
1645
+ interactionId: nonEmptyStringSchema,
1646
+ actuatorId: nonEmptyStringSchema,
1647
+ descriptorDigest: digestSchema.optional(),
1648
+ draftDigest: digestSchema.optional()
1649
+ }).strict();
1650
+ var portableSemanticReplayStepSchema = z.object({
1651
+ stepId: nonEmptyStringSchema,
1652
+ requestDigest: digestSchema,
1653
+ resolve: uiReplayRequestSchema,
1654
+ execute: uiReplayExecutionSchema,
1655
+ expectedIdentity: uiResolvedReplayIdentitySchema.optional(),
1656
+ expect: uiStepExpectationSchema
1657
+ }).strict();
1658
+ var uiScenarioReplayStepSchema = z.union([
1659
+ portableSemanticReplayStepSchema,
1660
+ z.object({
1661
+ stepId: nonEmptyStringSchema,
1662
+ kind: z.literal("assert"),
1663
+ expect: uiStepExpectationSchema
1664
+ }).strict()
1665
+ ]);
1666
+ var uiScenarioFixtureSchema = z.object({
1667
+ schemaVersion: z.literal(UI_SCENARIO_FIXTURE_SCHEMA_VERSION),
1668
+ browserInteractionProtocol: z.literal(
1669
+ DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION
1670
+ ),
1671
+ pluginRuntimeProtocol: z.literal(DREAMBOARD_PLUGIN_PROTOCOL_VERSION),
1672
+ id: nonEmptyStringSchema,
1673
+ title: nonEmptyStringSchema,
1674
+ gameId: nonEmptyStringSchema,
1675
+ tags: z.array(nonEmptyStringSchema),
1676
+ source: z.object({
1677
+ scenarioId: nonEmptyStringSchema,
1678
+ reducerFingerprint: nonEmptyStringSchema,
1679
+ uiContractFingerprint: nonEmptyStringSchema,
1680
+ renderModule: nonEmptyStringSchema,
1681
+ renderModuleDigest: digestSchema,
1682
+ sourceDigest: digestSchema
1683
+ }).strict(),
1684
+ viewer: z.object({
1685
+ seatId: nonEmptyStringSchema,
1686
+ playerId: nonEmptyStringSchema.optional()
1687
+ }).strict(),
1688
+ environment: environmentSchema,
1689
+ protocol: pluginProtocolTapeSchema,
1690
+ replay: z.array(uiScenarioReplayStepSchema),
1691
+ expected: z.object({
1692
+ initialProjectionDigest: digestSchema,
1693
+ finalProjectionDigest: digestSchema,
1694
+ finalSemanticDigest: digestSchema,
1695
+ submissionDigest: digestSchema
1696
+ }).strict()
1697
+ }).strict();
1698
+ var uiScenarioFixtureBundleIndexSchema = z.object({
1699
+ schemaVersion: z.literal(UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION),
1700
+ bundleId: nonEmptyStringSchema,
1701
+ sdkCommit: nonEmptyStringSchema,
1702
+ pluginRuntimeProtocol: z.literal(DREAMBOARD_PLUGIN_PROTOCOL_VERSION),
1703
+ browserInteractionProtocol: z.literal(
1704
+ DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION
1705
+ ),
1706
+ fixtures: z.array(
1707
+ z.object({
1708
+ id: nonEmptyStringSchema,
1709
+ file: nonEmptyStringSchema,
1710
+ sha256: digestSchema,
1711
+ renderModule: nonEmptyStringSchema,
1712
+ renderModuleSha256: digestSchema,
1713
+ components: z.array(nonEmptyStringSchema),
1714
+ capabilities: z.array(nonEmptyStringSchema)
1715
+ }).strict()
1716
+ )
1717
+ }).strict();
1718
+ function parseUIScenarioFixture(value) {
1719
+ const fixture = uiScenarioFixtureSchema.parse(
1720
+ value
1721
+ );
1722
+ validateUIScenarioFixtureReferences(fixture);
1723
+ return fixture;
1724
+ }
1725
+ function parseUIScenarioFixtureBundleIndex(value) {
1726
+ const bundle = uiScenarioFixtureBundleIndexSchema.parse(value);
1727
+ assertSupportedBrowserInteractionProtocol(bundle.browserInteractionProtocol);
1728
+ const ids = /* @__PURE__ */ new Set();
1729
+ for (const entry of bundle.fixtures) {
1730
+ if (ids.has(entry.id)) {
1731
+ throw new Error(`Duplicate fixture id '${entry.id}' in bundle index.`);
1732
+ }
1733
+ ids.add(entry.id);
1734
+ }
1735
+ return bundle;
1736
+ }
1737
+ function assertSupportedBrowserInteractionProtocol(version) {
1738
+ const major = Number(version.split(".")[0]);
1739
+ if (major !== UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR) {
1740
+ throw new Error(
1741
+ `Unsupported browser-interaction protocol '${version}'. Expected major ${UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR}.`
1742
+ );
1743
+ }
1744
+ }
1745
+ function validateUIScenarioFixtureReferences(fixture) {
1746
+ assertSupportedBrowserInteractionProtocol(fixture.browserInteractionProtocol);
1747
+ const frameIds = /* @__PURE__ */ new Set();
1748
+ const stepIds = /* @__PURE__ */ new Set();
1749
+ for (const frame of fixture.protocol.frames) {
1750
+ if (frameIds.has(frame.id)) {
1751
+ throw new Error(`Duplicate fixture frame id '${frame.id}'.`);
1752
+ }
1753
+ frameIds.add(frame.id);
1754
+ }
1755
+ for (const step of fixture.protocol.steps) {
1756
+ if (stepIds.has(step.id)) {
1757
+ throw new Error(`Duplicate protocol step id '${step.id}'.`);
1758
+ }
1759
+ stepIds.add(step.id);
1760
+ if (step.kind === "host.frame") {
1761
+ if (!frameIds.has(step.frameId)) {
1762
+ throw new Error(
1763
+ `Protocol step '${step.id}' references missing frameId '${step.frameId}'.`
1764
+ );
1765
+ }
1766
+ continue;
1767
+ }
1768
+ if (!frameIds.has(step.fromFrameId)) {
1769
+ throw new Error(
1770
+ `Protocol step '${step.id}' references missing fromFrameId '${step.fromFrameId}'.`
1771
+ );
1772
+ }
1773
+ }
1774
+ for (const step of fixture.replay) {
1775
+ if ("resolve" in step) {
1776
+ const actualDigest = digestUIFixtureRequest(
1777
+ step.resolve
1778
+ );
1779
+ if (step.requestDigest !== actualDigest) {
1780
+ throw new Error(
1781
+ `Replay step '${step.stepId}' requestDigest ${step.requestDigest} does not match ${actualDigest}.`
1782
+ );
1783
+ }
1784
+ }
1785
+ const expectedFrameId = step.expect.frameId;
1786
+ if (expectedFrameId && !frameIds.has(expectedFrameId)) {
1787
+ throw new Error(
1788
+ `Replay step '${step.stepId}' expects missing frameId '${expectedFrameId}'.`
1789
+ );
1790
+ }
1791
+ }
1792
+ }
1793
+
1794
+ // src/testing/ui-fixture/compiler.ts
1795
+ function compileUIScenarioFixture(options) {
1796
+ const firstFrame = options.protocol.frames[0];
1797
+ const finalFrame = lastPublishedFrame(options.protocol);
1798
+ if (!firstFrame || !finalFrame) {
1799
+ throw new Error(
1800
+ "compileUIScenarioFixture requires at least one protocol frame."
1801
+ );
1802
+ }
1803
+ const replay = options.replay ?? [];
1804
+ const submissionDigest = options.expected?.submissionDigest ?? digestUIFixtureJson({
1805
+ digestVersion: "ui-submission@1",
1806
+ fixtureId: options.id,
1807
+ replay: replay.map(
1808
+ (step) => "requestDigest" in step ? step.requestDigest : step.stepId
1809
+ )
1810
+ });
1811
+ const finalSemanticDigest = options.expected?.finalSemanticDigest ?? digestUIFixtureJson({
1812
+ digestVersion: "ui-semantic@1",
1813
+ fixtureId: options.id,
1814
+ frameId: finalFrame.id,
1815
+ projectionDigest: finalFrame.projectionDigest
1816
+ });
1817
+ return parseUIScenarioFixture(
1818
+ canonicalizeUIScenarioFixture({
1819
+ schemaVersion: 2,
1820
+ browserInteractionProtocol: DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
1821
+ pluginRuntimeProtocol: DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
1822
+ id: options.id,
1823
+ title: options.title,
1824
+ gameId: options.gameId,
1825
+ tags: [...options.tags ?? []],
1826
+ source: options.source,
1827
+ viewer: options.viewer,
1828
+ environment: options.environment,
1829
+ protocol: options.protocol,
1830
+ replay,
1831
+ expected: {
1832
+ initialProjectionDigest: options.expected?.initialProjectionDigest ?? firstFrame.projectionDigest,
1833
+ finalProjectionDigest: options.expected?.finalProjectionDigest ?? finalFrame.projectionDigest,
1834
+ finalSemanticDigest,
1835
+ submissionDigest
1836
+ }
1837
+ })
1838
+ );
1839
+ }
1840
+ function lastPublishedFrame(protocol) {
1841
+ const frameById = new Map(protocol.frames.map((frame) => [frame.id, frame]));
1842
+ for (let index = protocol.steps.length - 1; index >= 0; index -= 1) {
1843
+ const step = protocol.steps[index];
1844
+ if (step?.kind === "host.frame") {
1845
+ return frameById.get(step.frameId);
1846
+ }
1847
+ }
1848
+ return protocol.frames[protocol.frames.length - 1];
1849
+ }
1850
+ function assertDeterministicUIScenarioFixture(first, second) {
1851
+ const firstBytes = serializeUIScenarioFixture(first);
1852
+ const secondBytes = serializeUIScenarioFixture(second);
1853
+ if (firstBytes !== secondBytes) {
1854
+ throw new Error(
1855
+ `UI scenario fixture '${first.id}' is non-deterministic across compilation runs.`
1856
+ );
1857
+ }
1858
+ }
1859
+ function assertUniqueReplayIdentity(step, candidates) {
1860
+ if (candidates.length !== 1) {
1861
+ throw new Error(
1862
+ `Replay step '${step.stepId}' resolved ${candidates.length} identities; expected exactly one.`
1863
+ );
1864
+ }
1865
+ const candidate = candidates[0];
1866
+ if (!candidate) {
1867
+ throw new Error(`Replay step '${step.stepId}' did not resolve.`);
1868
+ }
1869
+ const expected = step.expectedIdentity;
1870
+ if (!expected) {
1871
+ return candidate;
1872
+ }
1873
+ for (const key of [
1874
+ "surface",
1875
+ "scopeId",
1876
+ "interactionKey",
1877
+ "interactionId",
1878
+ "actuatorId",
1879
+ "descriptorDigest",
1880
+ "draftDigest"
1881
+ ]) {
1882
+ const expectedValue = expected[key];
1883
+ if (expectedValue !== void 0 && candidate[key] !== expectedValue) {
1884
+ throw new Error(
1885
+ `Replay step '${step.stepId}' resolved ${key} '${String(
1886
+ candidate[key]
1887
+ )}', expected '${expectedValue}'.`
1888
+ );
1889
+ }
1890
+ }
1891
+ return candidate;
1892
+ }
1893
+ function assertUIStepExpectationSatisfied(stepId, expected, actual) {
1894
+ for (const key of [
1895
+ "frameId",
1896
+ "projectionDigest",
1897
+ "semanticDigest",
1898
+ "draftDigest",
1899
+ "submissionDigest",
1900
+ "focusedInteractionKey"
1901
+ ]) {
1902
+ if (expected[key] !== void 0 && expected[key] !== actual[key]) {
1903
+ throw new Error(
1904
+ `Replay step '${stepId}' expected ${key} '${expected[key]}', received '${actual[key]}'.`
1905
+ );
1906
+ }
1907
+ }
1908
+ if (expected.visibleInteractionKeys) {
1909
+ const actualKeys = new Set(actual.visibleInteractionKeys ?? []);
1910
+ const missing = expected.visibleInteractionKeys.filter(
1911
+ (key) => !actualKeys.has(key)
1912
+ );
1913
+ if (missing.length > 0) {
1914
+ throw new Error(
1915
+ `Replay step '${stepId}' missing visible interactions: ${missing.join(
1916
+ ", "
1917
+ )}.`
1918
+ );
1919
+ }
1920
+ }
1921
+ }
669
1922
  export {
1923
+ FixturePluginRuntime,
670
1924
  StaleContractArtifactError,
1925
+ UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION,
1926
+ UI_SCENARIO_FIXTURE_PLUGIN_RUNTIME_PROTOCOL,
1927
+ UI_SCENARIO_FIXTURE_SCHEMA_VERSION,
1928
+ UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR,
1929
+ assertDeterministicUIScenarioFixture,
1930
+ assertSupportedBrowserInteractionProtocol,
1931
+ assertUIStepExpectationSatisfied,
1932
+ assertUniqueReplayIdentity,
1933
+ canonicalUIFixtureJson,
1934
+ canonicalizeUIFixtureJson,
1935
+ canonicalizeUIScenarioFixture,
1936
+ compareUIParityObservations,
1937
+ compilePluginProtocolTape,
1938
+ compileUIScenarioFixture,
1939
+ createDeterministicIdFactory,
671
1940
  createExpectApi,
1941
+ createFixtureHostHarness,
1942
+ createReducerScenarioRunner,
672
1943
  createTestRuntime,
1944
+ createUIParityObservationFromFixture,
1945
+ defaultFixtureEnvironmentInit,
673
1946
  defineBase,
674
1947
  defineScenario,
675
- isStaleContractArtifactError
1948
+ digestPluginProtocolTape,
1949
+ digestUIFixtureJson,
1950
+ digestUIFixtureRequest,
1951
+ digestUIFixtureTransportRequest,
1952
+ digestUIScenarioFixture,
1953
+ fixtureEnvironmentInitFor,
1954
+ isSha256Digest,
1955
+ isStaleContractArtifactError,
1956
+ parseUIParityObservationV1,
1957
+ parseUIScenarioFixture,
1958
+ parseUIScenarioFixtureBundleIndex,
1959
+ pluginProtocolTapeSchema,
1960
+ portableSemanticReplayStepSchema,
1961
+ serializeUIScenarioFixture,
1962
+ uiFixtureFrameSchema,
1963
+ uiFixtureProtocolStepSchema,
1964
+ uiReplayExecutionSchema,
1965
+ uiReplayRequestSchema,
1966
+ uiResolvedReplayIdentitySchema,
1967
+ uiScenarioFixtureBundleIndexSchema,
1968
+ uiScenarioFixtureSchema,
1969
+ uiScenarioReplayStepSchema,
1970
+ uiStepExpectationSchema
676
1971
  };
677
1972
  //# sourceMappingURL=testing.js.map