@serverkgg/bridge 0.0.8 → 0.0.10

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 (72) hide show
  1. package/package.json +3 -3
  2. package/src/cli/validate.ts +2 -0
  3. package/src/context/bridgeContext.ts +17 -0
  4. package/src/context/index.ts +1 -0
  5. package/src/core/bridgeDownloadError.ts +10 -0
  6. package/src/core/bridgeHold.ts +12 -0
  7. package/src/core/bridgeKind.ts +3 -0
  8. package/src/core/bridgeProbeError.ts +16 -0
  9. package/src/core/bridgeSetup.ts +20 -0
  10. package/src/core/bridgeUserError.test.ts +23 -0
  11. package/src/core/bridgeUserError.ts +12 -0
  12. package/src/core/bridgeValue.test.ts +29 -0
  13. package/src/core/bridgeValue.ts +12 -0
  14. package/src/core/index.ts +5 -0
  15. package/src/guides/blocks.ts +8 -0
  16. package/src/guides/markdown.test.ts +18 -0
  17. package/src/guides/markdown.ts +7 -1
  18. package/src/manifest/index.ts +1 -0
  19. package/src/manifest/manifest.test.ts +132 -0
  20. package/src/manifest/manifest.ts +21 -0
  21. package/src/manifest/presence.ts +56 -0
  22. package/src/manifest/primitives.ts +6 -0
  23. package/src/modules/bridge.ts +65 -2
  24. package/src/modules/bridgeDriver.ts +4 -0
  25. package/src/modules/bridgeModule.ts +32 -3
  26. package/src/panel/bridgeDetail.ts +49 -0
  27. package/src/panel/bridgeLayout.ts +1 -0
  28. package/src/panel/bridgePanel.ts +30 -0
  29. package/src/panel/bridgeUploadMode.ts +4 -0
  30. package/src/panel/index.ts +2 -0
  31. package/src/probes/a2s.test.ts +71 -0
  32. package/src/probes/a2s.ts +38 -11
  33. package/src/protocol/bridgeEgress.ts +8 -1
  34. package/src/protocol/bridgeEvents.ts +4 -0
  35. package/src/protocol/bridgeFrame.ts +33 -1
  36. package/src/protocol/bridgeLimits.ts +12 -0
  37. package/src/protocol/bridgeManifest.ts +2 -0
  38. package/src/protocol/bridgeSetup.ts +97 -0
  39. package/src/protocol/index.ts +1 -0
  40. package/src/runtime/bridgeActions.ts +7 -0
  41. package/src/runtime/bridgeConfigReader.ts +12 -8
  42. package/src/runtime/bridgeContext.ts +28 -2
  43. package/src/runtime/bridgeDriver.test.ts +748 -3
  44. package/src/runtime/bridgeDriver.ts +198 -4
  45. package/src/runtime/bridgeEgress.ts +3 -0
  46. package/src/runtime/bridgeEvents.test.ts +96 -0
  47. package/src/runtime/bridgeEvents.ts +11 -6
  48. package/src/runtime/bridgeFiles.test.ts +625 -15
  49. package/src/runtime/bridgeFiles.ts +152 -45
  50. package/src/runtime/bridgeInstall.test.ts +118 -0
  51. package/src/runtime/bridgeInstall.ts +55 -0
  52. package/src/runtime/bridgeNet.ts +32 -3
  53. package/src/runtime/bridgePending.test.ts +148 -0
  54. package/src/runtime/bridgePending.ts +43 -0
  55. package/src/runtime/bridgeRegistry.test.ts +172 -1
  56. package/src/runtime/bridgeRegistry.ts +40 -5
  57. package/src/runtime/bridgeSecrets.test.ts +121 -0
  58. package/src/runtime/bridgeSecrets.ts +43 -0
  59. package/src/runtime/bridgeServer.test.ts +598 -0
  60. package/src/runtime/bridgeServer.ts +42 -3
  61. package/src/runtime/bridgeSetup.test.ts +189 -0
  62. package/src/runtime/bridgeSetup.ts +63 -0
  63. package/src/runtime/bridgeSignal.test.ts +114 -0
  64. package/src/runtime/bridgeSignal.ts +31 -0
  65. package/src/runtime/bridgeSupervisor.test.ts +111 -0
  66. package/src/runtime/bridgeSupervisor.ts +55 -5
  67. package/src/runtime/bridgeZip.ts +179 -58
  68. package/src/runtime/serve.ts +115 -4
  69. package/src/utils/index.ts +1 -0
  70. package/src/utils/stripAnsi.ts +12 -0
  71. package/src/wire/panel.test.ts +259 -0
  72. package/src/wire/panel.ts +72 -0
@@ -1,7 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { BridgeKind, PortJoin, PortProtocol } from "../core";
2
+ import { BridgeKind, BridgeSetupStepKind, PortJoin, PortProtocol } from "../core";
3
+ import { GuideOpenTab } from "../guides";
3
4
  import type { ManifestPort, ManifestPortActivation } from "../manifest";
4
- import type { BridgeDriver } from "../modules";
5
+ import type { BridgeDriver, BridgePendingModule, BridgeSetupModule } from "../modules";
5
6
  import {
6
7
  BridgeConfirm,
7
8
  BridgeControl,
@@ -9,8 +10,11 @@ import {
9
10
  BridgeFormTarget,
10
11
  BridgeLayout,
11
12
  type BridgeSectionAction,
13
+ type BridgeSectionUpload,
14
+ BridgeUploadMode,
12
15
  type BridgeVisibleWhen,
13
16
  } from "../panel";
17
+ import type { BridgeSetupStep } from "../protocol";
14
18
  import { validateDriver, validatePortActivation } from "./bridgeDriver";
15
19
 
16
20
  const text = (value: string) => ({
@@ -647,6 +651,747 @@ describe("validatePortActivation", () => {
647
651
  ],
648
652
  driverWithVariables(),
649
653
  ),
650
- ).toThrow('port "bedrock" activeWhen[0] names "CROSSPLAY", which no variables form or catalog declares');
654
+ ).toThrow('port "bedrock" activeWhen[0] names "CROSSPLAY", which no variables form, catalog or detail declares');
655
+ });
656
+ });
657
+
658
+ const driverWithDetail = (): BridgeDriver => ({
659
+ install: {
660
+ kind: BridgeKind.Install,
661
+ run: async () => {
662
+ return;
663
+ },
664
+ },
665
+ lifecycle: {
666
+ kind: BridgeKind.Lifecycle,
667
+ command: async () => [
668
+ "run",
669
+ ],
670
+ },
671
+ modules: {
672
+ modpackStatus: {
673
+ kind: BridgeKind.Detail,
674
+ read: async () => null,
675
+ actions: {
676
+ update: async () => null,
677
+ },
678
+ },
679
+ },
680
+ panel: {
681
+ tabs: [
682
+ {
683
+ id: "version",
684
+ title: text("Version"),
685
+ sections: [
686
+ {
687
+ layout: BridgeLayout.Form,
688
+ id: "version",
689
+ target: BridgeFormTarget.Variables,
690
+ fields: [
691
+ field("MC_VERSION"),
692
+ ],
693
+ clears: [
694
+ {
695
+ variable: "MODPACK",
696
+ warning: text("Applying this removes the modpack"),
697
+ },
698
+ ],
699
+ },
700
+ {
701
+ layout: BridgeLayout.Detail,
702
+ id: "modpack-status",
703
+ module: "modpackStatus",
704
+ variable: "MODPACK",
705
+ reinstall: true,
706
+ related: {
707
+ tab: "modpacks",
708
+ label: text("Browse modpacks"),
709
+ },
710
+ actions: [
711
+ {
712
+ id: "update",
713
+ label: text("Update"),
714
+ },
715
+ ],
716
+ },
717
+ ],
718
+ },
719
+ {
720
+ id: "modpacks",
721
+ title: text("Modpacks"),
722
+ sections: [
723
+ {
724
+ layout: BridgeLayout.Actions,
725
+ id: "noop",
726
+ module: "noop",
727
+ actions: [
728
+ {
729
+ id: "ping",
730
+ label: text("Ping"),
731
+ },
732
+ ],
733
+ },
734
+ ],
735
+ },
736
+ ],
737
+ },
738
+ });
739
+
740
+ const withNoop = (driver: BridgeDriver): BridgeDriver => ({
741
+ ...driver,
742
+ modules: {
743
+ ...driver.modules,
744
+ noop: {
745
+ kind: BridgeKind.Actions,
746
+ actions: {
747
+ ping: async () => {
748
+ return;
749
+ },
750
+ },
751
+ },
752
+ },
753
+ });
754
+
755
+ const detailSectionOf = (driver: BridgeDriver) => {
756
+ const section = driver.panel?.tabs[0]?.sections[1];
757
+
758
+ if (section?.layout !== BridgeLayout.Detail) {
759
+ throw new Error("fixture is missing its detail section");
760
+ }
761
+
762
+ return section;
763
+ };
764
+
765
+ const clearingFormOf = (driver: BridgeDriver) => {
766
+ const section = driver.panel?.tabs[0]?.sections[0];
767
+
768
+ if (section?.layout !== BridgeLayout.Form) {
769
+ throw new Error("fixture is missing its form section");
770
+ }
771
+
772
+ return section;
773
+ };
774
+
775
+ describe("detail sections", () => {
776
+ test("accepts a detail section on a detail module", () => {
777
+ expect(() => validateDriver(withNoop(driverWithDetail()))).not.toThrow();
778
+ });
779
+
780
+ test("rejects a section pointing at a module that is not a detail module", () => {
781
+ const driver = withNoop(driverWithDetail());
782
+
783
+ driver.modules = {
784
+ ...driver.modules,
785
+ modpackStatus: {
786
+ kind: BridgeKind.Collection,
787
+ list: async () => [],
788
+ },
789
+ };
790
+
791
+ expect(() => validateDriver(driver)).toThrow('references "modpackStatus", which is not a detail module');
792
+ });
793
+
794
+ test("rejects an action the module does not implement", () => {
795
+ const driver = withNoop(driverWithDetail());
796
+
797
+ detailSectionOf(driver).actions = [
798
+ {
799
+ id: "reinstall",
800
+ label: text("Reinstall"),
801
+ },
802
+ ];
803
+
804
+ expect(() => validateDriver(driver)).toThrow(
805
+ 'declares action "reinstall", which module "modpackStatus" does not implement',
806
+ );
807
+ });
808
+
809
+ test("rejects a download action, since a detail has no rows to serve", () => {
810
+ const driver = withNoop(driverWithDetail());
811
+
812
+ detailSectionOf(driver).actions = [
813
+ {
814
+ id: "update",
815
+ label: text("Update"),
816
+ download: true,
817
+ },
818
+ ];
819
+
820
+ expect(() => validateDriver(driver)).toThrow('declares action "update" as a download');
821
+ });
822
+
823
+ test("rejects a related tab the panel does not declare", () => {
824
+ const driver = withNoop(driverWithDetail());
825
+
826
+ detailSectionOf(driver).related = {
827
+ tab: "worlds",
828
+ label: text("Worlds"),
829
+ };
830
+
831
+ expect(() => validateDriver(driver)).toThrow('relates to tab "worlds", which the panel does not declare');
832
+ });
833
+
834
+ test("counts a detail variable as declared for port activation", () => {
835
+ expect(() =>
836
+ validatePortActivation(
837
+ [
838
+ port([
839
+ {
840
+ variable: "MODPACK",
841
+ empty: false,
842
+ },
843
+ ]),
844
+ ],
845
+ driverWithDetail(),
846
+ ),
847
+ ).not.toThrow();
848
+ });
849
+ });
850
+
851
+ describe("form clears", () => {
852
+ test("accepts a clear of a variable a detail section declares", () => {
853
+ expect(() => validateDriver(withNoop(driverWithDetail()))).not.toThrow();
854
+ });
855
+
856
+ test("rejects a clear of a variable nothing declares", () => {
857
+ const driver = withNoop(driverWithDetail());
858
+
859
+ clearingFormOf(driver).clears = [
860
+ {
861
+ variable: "CROSSPLAY",
862
+ warning: text("Applying this turns crossplay off"),
863
+ },
864
+ ];
865
+
866
+ expect(() => validateDriver(driver)).toThrow(
867
+ 'clears "CROSSPLAY", which no variables form, catalog or detail declares',
868
+ );
869
+ });
870
+
871
+ test("rejects a clear of one of the form's own fields", () => {
872
+ const driver = withNoop(driverWithDetail());
873
+
874
+ clearingFormOf(driver).clears = [
875
+ {
876
+ variable: "MC_VERSION",
877
+ warning: text("Applying this resets the version"),
878
+ },
879
+ ];
880
+
881
+ expect(() => validateDriver(driver)).toThrow('clears "MC_VERSION", which is one of its own fields');
882
+ });
883
+
884
+ test("rejects clears on a form that writes settings", () => {
885
+ const driver = withNoop(driverWithDetail());
886
+ const form = clearingFormOf(driver);
887
+
888
+ form.target = BridgeFormTarget.Settings;
889
+ form.module = "config";
890
+
891
+ driver.modules = {
892
+ ...driver.modules,
893
+ config: {
894
+ kind: BridgeKind.Settings,
895
+ read: async () => ({}),
896
+ write: async () => {
897
+ return;
898
+ },
899
+ },
900
+ };
901
+
902
+ expect(() => validateDriver(driver)).toThrow("clears variables but does not target variables");
903
+ });
904
+ });
905
+
906
+ describe("table uploads", () => {
907
+ const driverWithUpload = (upload: BridgeSectionUpload): BridgeDriver => ({
908
+ install: {
909
+ kind: BridgeKind.Install,
910
+ run: async () => {
911
+ return;
912
+ },
913
+ },
914
+ lifecycle: {
915
+ kind: BridgeKind.Lifecycle,
916
+ command: async () => [
917
+ "run",
918
+ ],
919
+ },
920
+ modules: {
921
+ modpackPending: {
922
+ kind: BridgeKind.Collection,
923
+ list: async () => [],
924
+ add: async () => {
925
+ return;
926
+ },
927
+ },
928
+ },
929
+ panel: {
930
+ tabs: [
931
+ {
932
+ id: "modpacks",
933
+ title: text("Modpacks"),
934
+ sections: [
935
+ {
936
+ layout: BridgeLayout.Table,
937
+ id: "pending",
938
+ module: "modpackPending",
939
+ columns: [
940
+ {
941
+ key: "name",
942
+ label: text("Name"),
943
+ },
944
+ ],
945
+ upload,
946
+ },
947
+ ],
948
+ },
949
+ ],
950
+ },
951
+ });
952
+
953
+ const upload = (mode?: BridgeUploadMode): BridgeSectionUpload => ({
954
+ label: text("Upload"),
955
+ extensions: [
956
+ "jar",
957
+ ],
958
+ staging: ".serverk-staging/pending-upload",
959
+ mode,
960
+ });
961
+
962
+ test("accepts an upload that declares no mode", () => {
963
+ expect(() => validateDriver(driverWithUpload(upload()))).not.toThrow();
964
+ });
965
+
966
+ test("accepts both declared upload modes", () => {
967
+ expect(() => validateDriver(driverWithUpload(upload(BridgeUploadMode.Archive)))).not.toThrow();
968
+ expect(() => validateDriver(driverWithUpload(upload(BridgeUploadMode.File)))).not.toThrow();
969
+ });
970
+
971
+ test("rejects an upload mode the platform does not know", () => {
972
+ const section = upload("stream" as BridgeUploadMode);
973
+
974
+ expect(() => validateDriver(driverWithUpload(section))).toThrow('declares an unknown upload mode "stream"');
975
+ });
976
+ });
977
+
978
+ describe("the pending module", () => {
979
+ const driverWithPending = (pending: BridgePendingModule): BridgeDriver => ({
980
+ install: {
981
+ kind: BridgeKind.Install,
982
+ run: async () => {
983
+ return;
984
+ },
985
+ },
986
+ lifecycle: {
987
+ kind: BridgeKind.Lifecycle,
988
+ command: async () => [
989
+ "run",
990
+ ],
991
+ },
992
+ pending,
993
+ });
994
+
995
+ const pendingWith = (overrides: Partial<BridgePendingModule>): BridgePendingModule => ({
996
+ kind: BridgeKind.Pending,
997
+ staging: ".serverk-staging/pending-upload",
998
+ extensions: [
999
+ "jar",
1000
+ ],
1001
+ list: async () => [],
1002
+ add: async () => {
1003
+ return;
1004
+ },
1005
+ ...overrides,
1006
+ });
1007
+
1008
+ test("accepts a driver that declares no pending module", () => {
1009
+ expect(() =>
1010
+ validateDriver({
1011
+ install: {
1012
+ kind: BridgeKind.Install,
1013
+ run: async () => {
1014
+ return;
1015
+ },
1016
+ },
1017
+ lifecycle: {
1018
+ kind: BridgeKind.Lifecycle,
1019
+ command: async () => [
1020
+ "run",
1021
+ ],
1022
+ },
1023
+ }),
1024
+ ).not.toThrow();
1025
+ });
1026
+
1027
+ test("accepts a pending module declaring a staging path and one extension", () => {
1028
+ expect(() => validateDriver(driverWithPending(pendingWith({})))).not.toThrow();
1029
+ });
1030
+
1031
+ test("rejects a pending module carrying another kind", () => {
1032
+ const pending = pendingWith({
1033
+ kind: BridgeKind.Collection as BridgeKind.Pending,
1034
+ });
1035
+
1036
+ expect(() => validateDriver(driverWithPending(pending))).toThrow(
1037
+ 'the pending module declares kind "collection", which is not "pending"',
1038
+ );
1039
+ });
1040
+
1041
+ test("rejects a pending module with no staging path", () => {
1042
+ expect(() =>
1043
+ validateDriver(
1044
+ driverWithPending(
1045
+ pendingWith({
1046
+ staging: "",
1047
+ }),
1048
+ ),
1049
+ ),
1050
+ ).toThrow("the pending module declares no staging path");
1051
+ });
1052
+
1053
+ test("rejects a staging path longer than the platform takes", () => {
1054
+ expect(() =>
1055
+ validateDriver(
1056
+ driverWithPending(
1057
+ pendingWith({
1058
+ staging: "x".repeat(129),
1059
+ }),
1060
+ ),
1061
+ ),
1062
+ ).toThrow("the pending module staging path is 129 characters — the platform takes at most 128");
1063
+ });
1064
+
1065
+ test("rejects a pending module declaring no extensions", () => {
1066
+ expect(() =>
1067
+ validateDriver(
1068
+ driverWithPending(
1069
+ pendingWith({
1070
+ extensions: [],
1071
+ }),
1072
+ ),
1073
+ ),
1074
+ ).toThrow("the pending module declares no extensions");
1075
+ });
1076
+
1077
+ test("rejects more extensions than the platform takes", () => {
1078
+ expect(() =>
1079
+ validateDriver(
1080
+ driverWithPending(
1081
+ pendingWith({
1082
+ extensions: Array.from({
1083
+ length: 9,
1084
+ }).map((_entry, index) => `e${index}`),
1085
+ }),
1086
+ ),
1087
+ ),
1088
+ ).toThrow("the pending module declares 9 extensions — the platform takes at most 8");
1089
+ });
1090
+
1091
+ test("rejects an extension longer than the platform takes", () => {
1092
+ expect(() =>
1093
+ validateDriver(
1094
+ driverWithPending(
1095
+ pendingWith({
1096
+ extensions: [
1097
+ "x".repeat(17),
1098
+ ],
1099
+ }),
1100
+ ),
1101
+ ),
1102
+ ).toThrow('the pending module extension "xxxxxxxxxxxxxxxxx" is 17 characters — the platform takes at most 16');
1103
+ });
1104
+
1105
+ test("reserves the pending module id for the singleton", () => {
1106
+ expect(() =>
1107
+ validateDriver({
1108
+ install: {
1109
+ kind: BridgeKind.Install,
1110
+ run: async () => {
1111
+ return;
1112
+ },
1113
+ },
1114
+ lifecycle: {
1115
+ kind: BridgeKind.Lifecycle,
1116
+ command: async () => [
1117
+ "run",
1118
+ ],
1119
+ },
1120
+ modules: {
1121
+ pending: {
1122
+ kind: BridgeKind.Collection,
1123
+ list: async () => [],
1124
+ },
1125
+ },
1126
+ }),
1127
+ ).toThrow('module id "pending" is reserved');
1128
+ });
1129
+ });
1130
+
1131
+ const driverWithSetup = (setup: BridgeSetupModule): BridgeDriver => ({
1132
+ install: {
1133
+ kind: BridgeKind.Install,
1134
+ run: async () => {
1135
+ return;
1136
+ },
1137
+ },
1138
+ lifecycle: {
1139
+ kind: BridgeKind.Lifecycle,
1140
+ command: async () => [
1141
+ "run",
1142
+ ],
1143
+ },
1144
+ setup,
1145
+ panel: {
1146
+ tabs: [
1147
+ {
1148
+ id: "settings",
1149
+ title: text("Settings"),
1150
+ sections: [
1151
+ {
1152
+ layout: BridgeLayout.Form,
1153
+ id: "server",
1154
+ target: BridgeFormTarget.Variables,
1155
+ fields: [
1156
+ field("SERVER_NAME"),
1157
+ field("MOTD"),
1158
+ ],
1159
+ },
1160
+ ],
1161
+ },
1162
+ ],
1163
+ },
1164
+ });
1165
+
1166
+ const driverStep = (id: string) => ({
1167
+ kind: BridgeSetupStepKind.Driver as const,
1168
+ id,
1169
+ title: text(id),
1170
+ });
1171
+
1172
+ const setupWith = (steps: BridgeSetupStep[]): BridgeSetupModule => ({
1173
+ kind: BridgeKind.Setup,
1174
+ steps,
1175
+ });
1176
+
1177
+ describe("the setup singleton", () => {
1178
+ test("takes a driver step with both locales", () => {
1179
+ expect(() =>
1180
+ validateDriver(
1181
+ driverWithSetup(
1182
+ setupWith([
1183
+ driverStep("sign-in"),
1184
+ ]),
1185
+ ),
1186
+ ),
1187
+ ).not.toThrow();
1188
+ });
1189
+
1190
+ test("rejects a setup that declares no steps", () => {
1191
+ expect(() => validateDriver(driverWithSetup(setupWith([])))).toThrow("the setup module declares no steps");
1192
+ });
1193
+
1194
+ test("rejects more steps than the platform takes", () => {
1195
+ expect(() =>
1196
+ validateDriver(
1197
+ driverWithSetup(
1198
+ setupWith(
1199
+ Array.from({
1200
+ length: 9,
1201
+ }).map((_entry, index) => driverStep(`step-${index}`)),
1202
+ ),
1203
+ ),
1204
+ ),
1205
+ ).toThrow("the setup module declares 9 steps — the platform takes at most 8");
1206
+ });
1207
+
1208
+ test("rejects a step id that is not kebab-case", () => {
1209
+ expect(() =>
1210
+ validateDriver(
1211
+ driverWithSetup(
1212
+ setupWith([
1213
+ driverStep("signIn"),
1214
+ ]),
1215
+ ),
1216
+ ),
1217
+ ).toThrow('the setup module step id "signIn" must be kebab-case');
1218
+ });
1219
+
1220
+ test("rejects the same step id twice", () => {
1221
+ expect(() =>
1222
+ validateDriver(
1223
+ driverWithSetup(
1224
+ setupWith([
1225
+ driverStep("sign-in"),
1226
+ driverStep("sign-in"),
1227
+ ]),
1228
+ ),
1229
+ ),
1230
+ ).toThrow('the setup module declares step "sign-in" more than once');
1231
+ });
1232
+
1233
+ test("rejects a title that is missing a locale", () => {
1234
+ expect(() =>
1235
+ validateDriver(
1236
+ driverWithSetup(
1237
+ setupWith([
1238
+ {
1239
+ kind: BridgeSetupStepKind.Driver,
1240
+ id: "sign-in",
1241
+ title: {
1242
+ ar: "",
1243
+ en: "Sign in",
1244
+ },
1245
+ },
1246
+ ]),
1247
+ ),
1248
+ ),
1249
+ ).toThrow('the setup module step "sign-in" title needs both arabic and english text');
1250
+ });
1251
+
1252
+ test("takes a form step that names an existing form section and its own fields", () => {
1253
+ expect(() =>
1254
+ validateDriver(
1255
+ driverWithSetup(
1256
+ setupWith([
1257
+ {
1258
+ kind: BridgeSetupStepKind.Form,
1259
+ id: "name",
1260
+ title: text("Name"),
1261
+ tab: "settings",
1262
+ section: "server",
1263
+ fields: [
1264
+ "SERVER_NAME",
1265
+ ],
1266
+ },
1267
+ ]),
1268
+ ),
1269
+ ),
1270
+ ).not.toThrow();
1271
+ });
1272
+
1273
+ test("rejects a form step that names a section the panel does not declare", () => {
1274
+ expect(() =>
1275
+ validateDriver(
1276
+ driverWithSetup(
1277
+ setupWith([
1278
+ {
1279
+ kind: BridgeSetupStepKind.Form,
1280
+ id: "name",
1281
+ title: text("Name"),
1282
+ tab: "settings",
1283
+ section: "missing",
1284
+ },
1285
+ ]),
1286
+ ),
1287
+ ),
1288
+ ).toThrow(
1289
+ 'the setup module step "name" names tab "settings" section "missing", which is not a form section of this panel',
1290
+ );
1291
+ });
1292
+
1293
+ test("rejects a form step that names a field the section does not declare", () => {
1294
+ expect(() =>
1295
+ validateDriver(
1296
+ driverWithSetup(
1297
+ setupWith([
1298
+ {
1299
+ kind: BridgeSetupStepKind.Form,
1300
+ id: "name",
1301
+ title: text("Name"),
1302
+ tab: "settings",
1303
+ section: "server",
1304
+ fields: [
1305
+ "PASSWORD",
1306
+ ],
1307
+ },
1308
+ ]),
1309
+ ),
1310
+ ),
1311
+ ).toThrow('the setup module step "name" names field "PASSWORD", which section "server" does not declare');
1312
+ });
1313
+
1314
+ test("takes an open step that names an existing panel tab", () => {
1315
+ expect(() =>
1316
+ validateDriver(
1317
+ driverWithSetup(
1318
+ setupWith([
1319
+ {
1320
+ kind: BridgeSetupStepKind.Open,
1321
+ id: "tune",
1322
+ title: text("Tune"),
1323
+ target: {
1324
+ tab: GuideOpenTab.Panel,
1325
+ tabId: "settings",
1326
+ },
1327
+ },
1328
+ ]),
1329
+ ),
1330
+ ),
1331
+ ).not.toThrow();
1332
+ });
1333
+
1334
+ test("rejects an open step that names a panel tab the panel does not declare", () => {
1335
+ expect(() =>
1336
+ validateDriver(
1337
+ driverWithSetup(
1338
+ setupWith([
1339
+ {
1340
+ kind: BridgeSetupStepKind.Open,
1341
+ id: "tune",
1342
+ title: text("Tune"),
1343
+ target: {
1344
+ tab: GuideOpenTab.Panel,
1345
+ tabId: "world",
1346
+ },
1347
+ },
1348
+ ]),
1349
+ ),
1350
+ ),
1351
+ ).toThrow('the setup module step "tune" opens panel tab "world", which the panel does not declare');
1352
+ });
1353
+
1354
+ test("takes an open step that leaves the panel altogether", () => {
1355
+ expect(() =>
1356
+ validateDriver(
1357
+ driverWithSetup(
1358
+ setupWith([
1359
+ {
1360
+ kind: BridgeSetupStepKind.Open,
1361
+ id: "invite",
1362
+ title: text("Invite"),
1363
+ target: {
1364
+ tab: GuideOpenTab.Access,
1365
+ },
1366
+ },
1367
+ ]),
1368
+ ),
1369
+ ),
1370
+ ).not.toThrow();
1371
+ });
1372
+
1373
+ test("reserves the setup module id for the singleton", () => {
1374
+ expect(() =>
1375
+ validateDriver({
1376
+ install: {
1377
+ kind: BridgeKind.Install,
1378
+ run: async () => {
1379
+ return;
1380
+ },
1381
+ },
1382
+ lifecycle: {
1383
+ kind: BridgeKind.Lifecycle,
1384
+ command: async () => [
1385
+ "run",
1386
+ ],
1387
+ },
1388
+ modules: {
1389
+ setup: {
1390
+ kind: BridgeKind.Collection,
1391
+ list: async () => [],
1392
+ },
1393
+ },
1394
+ }),
1395
+ ).toThrow('module id "setup" is reserved');
651
1396
  });
652
1397
  });