@dreamboard-games/sdk 0.4.0-alpha.1 → 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 (78) hide show
  1. package/REFERENCE.md +1215 -125
  2. package/dist/{ResourceCounter-BTXklCEZ.d.ts → ResourceCounter-BuiNstXs.d.ts} +5 -1
  3. package/dist/{ThemeProvider-DvMl17Ny.d.ts → ThemeProvider-B1sIoBlX.d.ts} +1 -1
  4. package/dist/{attributes-BrZtP9az.d.ts → attributes-DbvyMbXw.d.ts} +17 -2
  5. package/dist/authoring/index.d.ts +66 -0
  6. package/dist/authoring/index.js +560 -0
  7. package/dist/authoring/index.js.map +1 -0
  8. package/dist/browser-interaction.d.ts +205 -5
  9. package/dist/browser-interaction.js +14 -2
  10. package/dist/{chunk-7W6TQO3X.js → chunk-45FI6XII.js} +613 -366
  11. package/dist/chunk-45FI6XII.js.map +1 -0
  12. package/dist/{chunk-3K362ZKR.js → chunk-6JQFGSLD.js} +202 -38
  13. package/dist/chunk-6JQFGSLD.js.map +1 -0
  14. package/dist/{chunk-XXBSIPJX.js → chunk-CP35NPPY.js} +131 -59
  15. package/dist/chunk-CP35NPPY.js.map +1 -0
  16. package/dist/{chunk-XJYUSD5F.js → chunk-FWVV2X74.js} +2 -2
  17. package/dist/{chunk-IUGKW2GS.js → chunk-HWJS3VOJ.js} +6 -2
  18. package/dist/chunk-HWJS3VOJ.js.map +1 -0
  19. package/dist/chunk-K5ZRJTZZ.js +328 -0
  20. package/dist/chunk-K5ZRJTZZ.js.map +1 -0
  21. package/dist/{chunk-7LZZEI25.js → chunk-N6ZDIMRB.js} +1 -1
  22. package/dist/chunk-N6ZDIMRB.js.map +1 -0
  23. package/dist/chunk-O6M6PN4E.js +1225 -0
  24. package/dist/chunk-O6M6PN4E.js.map +1 -0
  25. package/dist/{chunk-AEUXCFGM.js → chunk-PF7L4BMG.js} +1 -8
  26. package/dist/{chunk-AEUXCFGM.js.map → chunk-PF7L4BMG.js.map} +1 -1
  27. package/dist/{chunk-CZMFAS3E.js → chunk-WOWFONLZ.js} +517 -1274
  28. package/dist/chunk-WOWFONLZ.js.map +1 -0
  29. package/dist/{chunk-MALRKLRR.js → chunk-WUIVY2KE.js} +301 -12
  30. package/dist/chunk-WUIVY2KE.js.map +1 -0
  31. package/dist/chunk-ZAS2SENW.js +12 -0
  32. package/dist/chunk-ZAS2SENW.js.map +1 -0
  33. package/dist/codegen.d.ts +11 -3
  34. package/dist/codegen.js +1 -1
  35. package/dist/{components-Dqr_LuZJ.d.ts → components-BbWPnZCF.d.ts} +37 -6
  36. package/dist/{hex-board-view-UCUjMALd.d.ts → hex-board-view-Kz9Q_zsv.d.ts} +1 -1
  37. package/dist/index.js +2 -1
  38. package/dist/package-set.d.ts +2 -2
  39. package/dist/package-set.js +2 -1
  40. package/dist/player-state-Cqpyeql0.d.ts +371 -0
  41. package/dist/protocol-BP193lva.d.ts +267 -0
  42. package/dist/reducer/advanced.js +1 -1
  43. package/dist/reducer-contract.js +1 -1
  44. package/dist/reducer.js +2 -2
  45. package/dist/reducer.js.map +1 -1
  46. package/dist/runtime/primitives.d.ts +10 -10
  47. package/dist/runtime/primitives.js +9 -7
  48. package/dist/runtime/runtime-api.d.ts +2 -2
  49. package/dist/runtime/workspace-contract.d.ts +23 -12
  50. package/dist/runtime/workspace-contract.js +7 -6
  51. package/dist/runtime-api-BJharVaj.d.ts +198 -0
  52. package/dist/runtime-api-C6aWo9c0.d.ts +95 -0
  53. package/dist/runtime.d.ts +35 -31
  54. package/dist/runtime.js +87 -797
  55. package/dist/runtime.js.map +1 -1
  56. package/dist/testing.d.ts +254 -140
  57. package/dist/testing.js +964 -149
  58. package/dist/testing.js.map +1 -1
  59. package/dist/types-CYeN0wLK.d.ts +29 -0
  60. package/dist/{types-BqzHkNMA.d.ts → types-DJj5MJkl.d.ts} +32 -5
  61. package/dist/types.d.ts +13 -8
  62. package/dist/ui/components.d.ts +4 -5
  63. package/dist/ui/components.js +19 -1
  64. package/dist/ui/player-state.d.ts +2 -365
  65. package/dist/{ui-contract-W2-kxObe.d.ts → ui-contract-CY7cxsCC.d.ts} +16 -26
  66. package/dist/ui.d.ts +7 -8
  67. package/dist/ui.js +20 -2
  68. package/package.json +25 -19
  69. package/dist/chunk-3K362ZKR.js.map +0 -1
  70. package/dist/chunk-7LZZEI25.js.map +0 -1
  71. package/dist/chunk-7W6TQO3X.js.map +0 -1
  72. package/dist/chunk-CZMFAS3E.js.map +0 -1
  73. package/dist/chunk-IUGKW2GS.js.map +0 -1
  74. package/dist/chunk-MALRKLRR.js.map +0 -1
  75. package/dist/chunk-XXBSIPJX.js.map +0 -1
  76. package/dist/hex-color-MhOyuY-o.d.ts +0 -8
  77. package/dist/runtime-api-BXd70c2e.d.ts +0 -490
  78. /package/dist/{chunk-XJYUSD5F.js.map → chunk-FWVV2X74.js.map} +0 -0
package/dist/testing.js CHANGED
@@ -2,14 +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";
5
18
  import {
6
19
  BROWSER_INTERACTION_ACTUATOR_KINDS,
7
20
  DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
8
21
  browserInteractionEffectSchema
9
- } from "./chunk-MALRKLRR.js";
22
+ } from "./chunk-WUIVY2KE.js";
10
23
  import {
11
24
  RuntimeJsonSchema
12
- } from "./chunk-AEUXCFGM.js";
25
+ } from "./chunk-PF7L4BMG.js";
13
26
  import "./chunk-PZ5AY32C.js";
14
27
 
15
28
  // src/testing/definitions.ts
@@ -37,9 +50,9 @@ function matchPartial(actual, expected, path = "value") {
37
50
  if (!(key in actual)) {
38
51
  return `${path}.${key} is missing`;
39
52
  }
40
- const mismatch = matchPartial(actualValue, expectedValue, `${path}.${key}`);
41
- if (mismatch) {
42
- return mismatch;
53
+ const mismatch2 = matchPartial(actualValue, expectedValue, `${path}.${key}`);
54
+ if (mismatch2) {
55
+ return mismatch2;
43
56
  }
44
57
  }
45
58
  return null;
@@ -59,9 +72,9 @@ function assertDescriptorMatches(descriptor, opts) {
59
72
  if (!opts) {
60
73
  return;
61
74
  }
62
- const mismatch = matchPartial(descriptor, opts, "interaction");
63
- if (mismatch) {
64
- throw new Error(mismatch);
75
+ const mismatch2 = matchPartial(descriptor, opts, "interaction");
76
+ if (mismatch2) {
77
+ throw new Error(mismatch2);
65
78
  }
66
79
  }
67
80
  function createSubmissionErrorMatcher(actual, expected, options) {
@@ -178,9 +191,9 @@ function createExpectApi(options = {}) {
178
191
  }
179
192
  },
180
193
  toMatchObject: (expected) => {
181
- const mismatch = matchPartial(actual, expected);
182
- if (mismatch) {
183
- throw new Error(mismatch);
194
+ const mismatch2 = matchPartial(actual, expected);
195
+ if (mismatch2) {
196
+ throw new Error(mismatch2);
184
197
  }
185
198
  },
186
199
  toBeDefined: () => {
@@ -422,7 +435,7 @@ function resolvePlayerIds(options) {
422
435
  function isWireZoneHandles(value) {
423
436
  return typeof value === "object" && value !== null;
424
437
  }
425
- function buildPluginSnapshot(options) {
438
+ function buildPluginFrame(options) {
426
439
  const projection = options.bundle.projectSeatsDynamic({
427
440
  state: options.state,
428
441
  playerIds: [options.playerId]
@@ -457,28 +470,25 @@ function buildPluginSnapshot(options) {
457
470
  ];
458
471
  })
459
472
  );
473
+ const availableInteractions = hydrateRefs(
474
+ seat?.availableInteractionRefs
475
+ );
460
476
  return {
477
+ gameVersion: options.version,
478
+ actionSetVersion: computePluginActionSetVersion({
479
+ gameVersion: options.version,
480
+ availableInteractions
481
+ }),
482
+ perspectivePlayerId: options.playerId || null,
461
483
  view: seat?.view ?? null,
462
- gameplay: {
484
+ flow: {
463
485
  currentPhase: flow.currentPhase,
464
486
  currentStage: projection.currentStage ?? null,
465
487
  activePlayers: flow.activePlayers,
466
- simultaneousPhase: projection.simultaneousPhase ?? null,
467
- availableInteractions: hydrateRefs(
468
- seat?.availableInteractionRefs
469
- ),
470
- zones
471
- },
472
- lobby: null,
473
- notifications: [],
474
- session: {
475
- sessionId: options.sessionId,
476
- controllablePlayerIds: [...options.playerIds],
477
- controllingPlayerId: options.playerId || null,
478
- userId: options.userId
488
+ simultaneousPhase: projection.simultaneousPhase ?? null
479
489
  },
480
- history: null,
481
- syncId: options.version
490
+ availableInteractions,
491
+ zones
482
492
  };
483
493
  }
484
494
  function createTestRuntime(options) {
@@ -499,44 +509,37 @@ function createTestRuntime(options) {
499
509
  baseState,
500
510
  explicitPlayerIds: options.playerIds
501
511
  });
502
- const userId = options.userId ?? "test-user";
503
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
+ };
504
520
  let version = 0;
505
521
  let submissionCounter = 0;
506
522
  let currentPlayerId = playerIds[0] ?? "";
507
523
  const diagnosticEvents = [];
508
524
  let lastDispatch = null;
509
- const stateListeners = /* @__PURE__ */ new Set();
510
- const sessionListeners = /* @__PURE__ */ new Set();
511
- const toReadySessionState = (snapshot) => ({
512
- ...snapshot.session,
513
- status: "ready"
525
+ const toReadySessionState = (frame) => ({
526
+ status: "ready",
527
+ sessionId,
528
+ controllingPlayerId: frame.perspectivePlayerId
514
529
  });
515
- let lastPluginSnapshot;
530
+ let lastPluginFrame;
516
531
  let lastSessionState;
517
532
  const applyCurrentState = () => {
518
- const previousSession = lastSessionState;
519
533
  version += 1;
520
- lastPluginSnapshot = buildPluginSnapshot({
534
+ lastPluginFrame = buildPluginFrame({
521
535
  state: currentState,
522
536
  bundle: options.bundle,
523
537
  playerId: currentPlayerId,
524
- playerIds,
525
- userId,
526
- sessionId,
527
538
  version,
528
539
  expectedPhase: version === 1 ? options.phase : void 0,
529
540
  baseId: options.baseId
530
541
  });
531
- lastSessionState = toReadySessionState(lastPluginSnapshot);
532
- for (const listener of stateListeners) {
533
- listener(lastPluginSnapshot);
534
- }
535
- if (!previousSession || previousSession.controllingPlayerId !== lastSessionState.controllingPlayerId || previousSession.controllablePlayerIds.join("\0") !== lastSessionState.controllablePlayerIds.join("\0")) {
536
- for (const listener of sessionListeners) {
537
- listener(lastSessionState);
538
- }
539
- }
542
+ lastSessionState = toReadySessionState(lastPluginFrame);
540
543
  };
541
544
  applyCurrentState();
542
545
  const validate = async (playerId, interactionId, params = {}) => {
@@ -615,38 +618,16 @@ function createTestRuntime(options) {
615
618
  interactionId
616
619
  });
617
620
  };
618
- const setControllingPlayer = (playerId) => {
619
- if (!playerIds.includes(playerId)) {
620
- throw new Error(`Unknown controlling player '${playerId}'.`);
621
- }
622
- currentPlayerId = playerId;
623
- applyCurrentState();
624
- };
625
621
  const runtime = {
626
- validateInteraction: validate,
627
- submitInteraction: submit,
622
+ validateInteraction: (interactionId, params) => validate(currentPlayerId, interactionId, params),
623
+ submitInteraction: (interactionId, params) => submit(currentPlayerId, interactionId, params),
628
624
  getSessionState: () => lastSessionState,
629
- disconnect: () => void 0,
630
- switchPlayer: (playerId) => {
631
- setControllingPlayer(playerId);
632
- },
633
- getSnapshot: () => lastPluginSnapshot,
634
- subscribeToState: (listener) => {
635
- stateListeners.add(listener);
636
- return () => {
637
- stateListeners.delete(listener);
638
- };
639
- },
640
- _subscribeToSessionState: (listener) => {
641
- sessionListeners.add(listener);
642
- return () => {
643
- sessionListeners.delete(listener);
644
- };
645
- }
625
+ disconnect: () => void 0
646
626
  };
647
627
  return {
648
628
  runtime,
649
- getSnapshot: () => lastPluginSnapshot,
629
+ getFrame: () => lastPluginFrame,
630
+ getSessionDescriptor: () => sessionDescriptor,
650
631
  players: () => [...playerIds],
651
632
  seat: (index) => {
652
633
  if (!Number.isInteger(index) || index < 0 || index >= playerIds.length) {
@@ -670,8 +651,7 @@ function createTestRuntime(options) {
670
651
  diagnosticEvents.length = 0;
671
652
  lastDispatch = null;
672
653
  }
673
- },
674
- setControllingPlayer
654
+ }
675
655
  };
676
656
  }
677
657
 
@@ -719,10 +699,22 @@ function digestUIFixtureJson(value) {
719
699
  }
720
700
  function digestUIFixtureRequest(request) {
721
701
  return digestUIFixtureJson({
722
- digestVersion: "ui-replay-request@1",
702
+ digestVersion: "ui-replay-request@2",
703
+ request
704
+ });
705
+ }
706
+ function digestUIFixtureTransportRequest(request) {
707
+ return digestUIFixtureJson({
708
+ digestVersion: "ui-fixture-runtime-command@2",
723
709
  request
724
710
  });
725
711
  }
712
+ function digestPluginProtocolTape(tape) {
713
+ return digestUIFixtureJson({
714
+ digestVersion: "plugin-protocol-tape@2",
715
+ tape
716
+ });
717
+ }
726
718
  function canonicalizeUIScenarioFixture(fixture) {
727
719
  return canonicalizeUIFixtureJson({
728
720
  ...fixture,
@@ -744,11 +736,843 @@ function sha256(value) {
744
736
  return `sha256:${createHash("sha256").update(value).digest("hex")}`;
745
737
  }
746
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;
1408
+ },
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")}`;
1557
+ };
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
+
747
1570
  // src/testing/ui-fixture/schema.ts
748
1571
  import { z } from "zod";
749
- var UI_SCENARIO_FIXTURE_SCHEMA_VERSION = 1;
750
- var UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION = 1;
751
- var UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR = 2;
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;
752
1576
  var digestSchema = z.string().refine(isSha256Digest, {
753
1577
  message: "Expected a sha256:<64 lowercase hex> digest."
754
1578
  });
@@ -761,55 +1585,12 @@ var environmentSchema = z.object({
761
1585
  timezone: z.literal("UTC"),
762
1586
  viewportTags: z.array(z.enum(["desktop", "tablet", "phone", "touch"]))
763
1587
  }).strict();
764
- var pluginSessionSchema = z.object({
765
- sessionId: nonEmptyStringSchema,
766
- controllablePlayerIds: z.array(nonEmptyStringSchema),
767
- controllingPlayerId: nonEmptyStringSchema.nullable(),
768
- userId: nonEmptyStringSchema.nullable()
769
- }).catchall(jsonSchema);
770
- var gameplaySnapshotSchema = z.object({
771
- currentPhase: nonEmptyStringSchema.nullable(),
772
- currentStage: nonEmptyStringSchema.nullable(),
773
- activePlayers: z.array(nonEmptyStringSchema),
774
- simultaneousPhase: jsonSchema.nullable().optional(),
775
- availableInteractions: z.array(jsonSchema),
776
- zones: z.record(z.string(), jsonSchema)
777
- }).catchall(jsonSchema);
778
- var uiFixturePluginStateSnapshotSchema = z.object({
779
- view: jsonSchema.nullable(),
780
- gameplay: gameplaySnapshotSchema,
781
- lobby: jsonSchema.nullable(),
782
- notifications: z.array(jsonSchema),
783
- session: pluginSessionSchema,
784
- history: jsonSchema.nullable(),
785
- syncId: z.number().int().nonnegative()
786
- }).strict();
787
- var uiFixtureFrameSchema = z.object({
788
- id: nonEmptyStringSchema,
789
- snapshot: uiFixturePluginStateSnapshotSchema,
790
- projectionDigest: digestSchema
791
- }).strict();
792
- var uiFixtureTransportExchangeSchema = z.object({
793
- id: nonEmptyStringSchema,
794
- fromFrameId: nonEmptyStringSchema,
795
- operation: z.enum(["validate", "submit", "refresh"]),
796
- requestDigest: digestSchema,
797
- response: z.union([
798
- z.object({
799
- kind: z.literal("accepted"),
800
- nextFrameId: nonEmptyStringSchema
801
- }).strict(),
802
- z.object({
803
- kind: z.literal("rejected"),
804
- diagnostics: z.array(
805
- z.object({
806
- code: nonEmptyStringSchema,
807
- message: nonEmptyStringSchema
808
- }).strict()
809
- )
810
- }).strict()
811
- ])
812
- }).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
+ );
813
1594
  var browserInteractionIntentRequestSchema = z.object({
814
1595
  surface: nonEmptyStringSchema,
815
1596
  scopeId: nonEmptyStringSchema.optional(),
@@ -887,6 +1668,7 @@ var uiScenarioFixtureSchema = z.object({
887
1668
  browserInteractionProtocol: z.literal(
888
1669
  DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION
889
1670
  ),
1671
+ pluginRuntimeProtocol: z.literal(DREAMBOARD_PLUGIN_PROTOCOL_VERSION),
890
1672
  id: nonEmptyStringSchema,
891
1673
  title: nonEmptyStringSchema,
892
1674
  gameId: nonEmptyStringSchema,
@@ -904,8 +1686,7 @@ var uiScenarioFixtureSchema = z.object({
904
1686
  playerId: nonEmptyStringSchema.optional()
905
1687
  }).strict(),
906
1688
  environment: environmentSchema,
907
- frames: z.array(uiFixtureFrameSchema).min(1),
908
- transport: z.array(uiFixtureTransportExchangeSchema),
1689
+ protocol: pluginProtocolTapeSchema,
909
1690
  replay: z.array(uiScenarioReplayStepSchema),
910
1691
  expected: z.object({
911
1692
  initialProjectionDigest: digestSchema,
@@ -918,6 +1699,7 @@ var uiScenarioFixtureBundleIndexSchema = z.object({
918
1699
  schemaVersion: z.literal(UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION),
919
1700
  bundleId: nonEmptyStringSchema,
920
1701
  sdkCommit: nonEmptyStringSchema,
1702
+ pluginRuntimeProtocol: z.literal(DREAMBOARD_PLUGIN_PROTOCOL_VERSION),
921
1703
  browserInteractionProtocol: z.literal(
922
1704
  DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION
923
1705
  ),
@@ -963,21 +1745,29 @@ function assertSupportedBrowserInteractionProtocol(version) {
963
1745
  function validateUIScenarioFixtureReferences(fixture) {
964
1746
  assertSupportedBrowserInteractionProtocol(fixture.browserInteractionProtocol);
965
1747
  const frameIds = /* @__PURE__ */ new Set();
966
- for (const frame of fixture.frames) {
1748
+ const stepIds = /* @__PURE__ */ new Set();
1749
+ for (const frame of fixture.protocol.frames) {
967
1750
  if (frameIds.has(frame.id)) {
968
1751
  throw new Error(`Duplicate fixture frame id '${frame.id}'.`);
969
1752
  }
970
1753
  frameIds.add(frame.id);
971
1754
  }
972
- for (const exchange of fixture.transport) {
973
- if (!frameIds.has(exchange.fromFrameId)) {
974
- throw new Error(
975
- `Transport exchange '${exchange.id}' references missing fromFrameId '${exchange.fromFrameId}'.`
976
- );
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;
977
1767
  }
978
- if (exchange.response.kind === "accepted" && !frameIds.has(exchange.response.nextFrameId)) {
1768
+ if (!frameIds.has(step.fromFrameId)) {
979
1769
  throw new Error(
980
- `Transport exchange '${exchange.id}' references missing nextFrameId '${exchange.response.nextFrameId}'.`
1770
+ `Protocol step '${step.id}' references missing fromFrameId '${step.fromFrameId}'.`
981
1771
  );
982
1772
  }
983
1773
  }
@@ -1003,10 +1793,12 @@ function validateUIScenarioFixtureReferences(fixture) {
1003
1793
 
1004
1794
  // src/testing/ui-fixture/compiler.ts
1005
1795
  function compileUIScenarioFixture(options) {
1006
- const firstFrame = options.frames[0];
1007
- const finalFrame = options.frames[options.frames.length - 1];
1796
+ const firstFrame = options.protocol.frames[0];
1797
+ const finalFrame = lastPublishedFrame(options.protocol);
1008
1798
  if (!firstFrame || !finalFrame) {
1009
- throw new Error("compileUIScenarioFixture requires at least one frame.");
1799
+ throw new Error(
1800
+ "compileUIScenarioFixture requires at least one protocol frame."
1801
+ );
1010
1802
  }
1011
1803
  const replay = options.replay ?? [];
1012
1804
  const submissionDigest = options.expected?.submissionDigest ?? digestUIFixtureJson({
@@ -1024,8 +1816,9 @@ function compileUIScenarioFixture(options) {
1024
1816
  });
1025
1817
  return parseUIScenarioFixture(
1026
1818
  canonicalizeUIScenarioFixture({
1027
- schemaVersion: 1,
1819
+ schemaVersion: 2,
1028
1820
  browserInteractionProtocol: DREAMBOARD_BROWSER_INTERACTION_PROTOCOL_VERSION,
1821
+ pluginRuntimeProtocol: DREAMBOARD_PLUGIN_PROTOCOL_VERSION,
1029
1822
  id: options.id,
1030
1823
  title: options.title,
1031
1824
  gameId: options.gameId,
@@ -1033,8 +1826,7 @@ function compileUIScenarioFixture(options) {
1033
1826
  source: options.source,
1034
1827
  viewer: options.viewer,
1035
1828
  environment: options.environment,
1036
- frames: options.frames,
1037
- transport: [...options.transport ?? []],
1829
+ protocol: options.protocol,
1038
1830
  replay,
1039
1831
  expected: {
1040
1832
  initialProjectionDigest: options.expected?.initialProjectionDigest ?? firstFrame.projectionDigest,
@@ -1045,6 +1837,16 @@ function compileUIScenarioFixture(options) {
1045
1837
  })
1046
1838
  );
1047
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
+ }
1048
1850
  function assertDeterministicUIScenarioFixture(first, second) {
1049
1851
  const firstBytes = serializeUIScenarioFixture(first);
1050
1852
  const secondBytes = serializeUIScenarioFixture(second);
@@ -1118,8 +1920,10 @@ function assertUIStepExpectationSatisfied(stepId, expected, actual) {
1118
1920
  }
1119
1921
  }
1120
1922
  export {
1923
+ FixturePluginRuntime,
1121
1924
  StaleContractArtifactError,
1122
1925
  UI_SCENARIO_FIXTURE_BUNDLE_SCHEMA_VERSION,
1926
+ UI_SCENARIO_FIXTURE_PLUGIN_RUNTIME_PROTOCOL,
1123
1927
  UI_SCENARIO_FIXTURE_SCHEMA_VERSION,
1124
1928
  UI_SCENARIO_FIXTURE_SUPPORTED_BROWSER_PROTOCOL_MAJOR,
1125
1929
  assertDeterministicUIScenarioFixture,
@@ -1129,23 +1933,34 @@ export {
1129
1933
  canonicalUIFixtureJson,
1130
1934
  canonicalizeUIFixtureJson,
1131
1935
  canonicalizeUIScenarioFixture,
1936
+ compareUIParityObservations,
1937
+ compilePluginProtocolTape,
1132
1938
  compileUIScenarioFixture,
1939
+ createDeterministicIdFactory,
1133
1940
  createExpectApi,
1941
+ createFixtureHostHarness,
1942
+ createReducerScenarioRunner,
1134
1943
  createTestRuntime,
1944
+ createUIParityObservationFromFixture,
1945
+ defaultFixtureEnvironmentInit,
1135
1946
  defineBase,
1136
1947
  defineScenario,
1948
+ digestPluginProtocolTape,
1137
1949
  digestUIFixtureJson,
1138
1950
  digestUIFixtureRequest,
1951
+ digestUIFixtureTransportRequest,
1139
1952
  digestUIScenarioFixture,
1953
+ fixtureEnvironmentInitFor,
1140
1954
  isSha256Digest,
1141
1955
  isStaleContractArtifactError,
1956
+ parseUIParityObservationV1,
1142
1957
  parseUIScenarioFixture,
1143
1958
  parseUIScenarioFixtureBundleIndex,
1959
+ pluginProtocolTapeSchema,
1144
1960
  portableSemanticReplayStepSchema,
1145
1961
  serializeUIScenarioFixture,
1146
1962
  uiFixtureFrameSchema,
1147
- uiFixturePluginStateSnapshotSchema,
1148
- uiFixtureTransportExchangeSchema,
1963
+ uiFixtureProtocolStepSchema,
1149
1964
  uiReplayExecutionSchema,
1150
1965
  uiReplayRequestSchema,
1151
1966
  uiResolvedReplayIdentitySchema,