@serverkgg/bridge 0.1.1 → 0.1.3

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 (121) hide show
  1. package/package.json +3 -2
  2. package/rules/package.md +60 -0
  3. package/src/blog/articleFrontmatter.test.ts +10 -10
  4. package/src/blog/compile.test.ts +9 -9
  5. package/src/bot/botDeploy.test.ts +16 -16
  6. package/src/bot/botEnvironment.test.ts +8 -8
  7. package/src/bot/botIdentity.test.ts +12 -12
  8. package/src/bot/botPanel.test.ts +41 -11
  9. package/src/bot/botPanel.ts +13 -5
  10. package/src/bot/botState.test.ts +3 -3
  11. package/src/bot/botTerminal.test.ts +5 -5
  12. package/src/bot/botVariables.test.ts +8 -8
  13. package/src/bot/detect/botToken.test.ts +6 -6
  14. package/src/bot/detect/detect.test.ts +21 -21
  15. package/src/bot/detect/detectTable.test.ts +5 -5
  16. package/src/bot/detect/envExample.test.ts +2 -2
  17. package/src/bot/detect/envScan.test.ts +4 -4
  18. package/src/bot/detect/inferBotPlatform.test.ts +4 -4
  19. package/src/bot/detect/keys.test.ts +3 -3
  20. package/src/bot/detect/sensitiveKey.test.ts +5 -5
  21. package/src/bot/detect/serverkJson.test.ts +5 -5
  22. package/src/catalogs/curseforge.test.ts +35 -35
  23. package/src/catalogs/thunderstore.test.ts +9 -9
  24. package/src/cli/detectTable.test.ts +3 -3
  25. package/src/cli/link.test.ts +11 -11
  26. package/src/cli/main.ts +8 -0
  27. package/src/cli/rules.test.ts +198 -0
  28. package/src/cli/rules.ts +188 -0
  29. package/src/cli/validate.test.ts +123 -29
  30. package/src/cli/validate.ts +37 -1
  31. package/src/codecs/ini.test.ts +13 -13
  32. package/src/codecs/javaProperties.test.ts +11 -11
  33. package/src/codecs/sourceCfg.test.ts +14 -14
  34. package/src/codecs/xmlProperties.test.ts +10 -10
  35. package/src/context/bridgeContext.ts +6 -1
  36. package/src/core/bridgeAction.ts +2 -0
  37. package/src/core/bridgeActivity.ts +9 -0
  38. package/src/core/bridgeAvailability.ts +10 -0
  39. package/src/core/bridgePort.test.ts +6 -6
  40. package/src/core/bridgeUserError.test.ts +13 -3
  41. package/src/core/bridgeUserError.ts +6 -2
  42. package/src/core/bridgeValue.test.ts +5 -5
  43. package/src/core/describeDownloadFailure.test.ts +58 -0
  44. package/src/core/describeDownloadFailure.ts +69 -0
  45. package/src/core/index.ts +3 -0
  46. package/src/guides/markdown.test.ts +31 -31
  47. package/src/install/installStamp.test.ts +9 -9
  48. package/src/manifest/extension.ts +36 -0
  49. package/src/manifest/index.ts +1 -0
  50. package/src/manifest/manifest.test.ts +208 -90
  51. package/src/manifest/manifest.ts +34 -0
  52. package/src/modules/bridge.ts +8 -1
  53. package/src/modules/bridgeModule.ts +13 -1
  54. package/src/net/loopback.test.ts +10 -10
  55. package/src/panel/bridgeDetail.ts +2 -1
  56. package/src/presence/rosterSync.test.ts +7 -7
  57. package/src/probes/a2s.test.ts +5 -5
  58. package/src/probes/minecraftPing.test.ts +17 -17
  59. package/src/probes/raknetPing.test.ts +13 -13
  60. package/src/protocol/bridgeActivity.ts +54 -0
  61. package/src/protocol/bridgeEvents.ts +30 -0
  62. package/src/protocol/bridgeFrame.ts +10 -1
  63. package/src/protocol/index.ts +1 -0
  64. package/src/rcon/rconAccess.test.ts +20 -20
  65. package/src/rcon/rconAccess.ts +13 -13
  66. package/src/rcon/rconClient.test.ts +49 -49
  67. package/src/rcon/rconFrame.test.ts +14 -14
  68. package/src/rcon/rconIds.test.ts +4 -4
  69. package/src/rcon/rconWriter.test.ts +6 -6
  70. package/src/runtime/bridgeActions.ts +19 -2
  71. package/src/runtime/bridgeActivity.test.ts +318 -0
  72. package/src/runtime/bridgeActivity.ts +268 -0
  73. package/src/runtime/bridgeBackupLease.test.ts +9 -9
  74. package/src/runtime/bridgeConfigReader.test.ts +5 -5
  75. package/src/runtime/bridgeConsoleHandler.test.ts +8 -8
  76. package/src/runtime/bridgeContext.test.ts +11 -5
  77. package/src/runtime/bridgeContext.ts +8 -1
  78. package/src/runtime/bridgeDriver.test.ts +114 -93
  79. package/src/runtime/bridgeDriver.ts +10 -7
  80. package/src/runtime/bridgeEvents.test.ts +15 -15
  81. package/src/runtime/bridgeFiles.test.ts +54 -54
  82. package/src/runtime/bridgeInstall.test.ts +10 -10
  83. package/src/runtime/bridgeLiveConfig.test.ts +6 -6
  84. package/src/runtime/bridgeLog.ts +10 -0
  85. package/src/runtime/bridgePending.test.ts +6 -6
  86. package/src/runtime/bridgeRedaction.test.ts +16 -16
  87. package/src/runtime/bridgeRefusalError.ts +13 -0
  88. package/src/runtime/bridgeRegistry.test.ts +1077 -34
  89. package/src/runtime/bridgeRegistry.ts +464 -65
  90. package/src/runtime/bridgeScopedContext.ts +55 -0
  91. package/src/runtime/bridgeServer.test.ts +424 -24
  92. package/src/runtime/bridgeServer.ts +125 -35
  93. package/src/runtime/bridgeSetup.test.ts +27 -27
  94. package/src/runtime/bridgeSetup.ts +1 -1
  95. package/src/runtime/bridgeSetupHandlers.test.ts +80 -17
  96. package/src/runtime/bridgeSetupHandlers.ts +45 -10
  97. package/src/runtime/bridgeSignal.test.ts +7 -7
  98. package/src/runtime/bridgeSupervisor.test.ts +42 -42
  99. package/src/runtime/serve.ts +45 -3
  100. package/src/steam/avatars.test.ts +13 -13
  101. package/src/steam/steamApp.test.ts +8 -8
  102. package/src/steam/steamcmd.test.ts +44 -13
  103. package/src/steam/steamcmd.ts +27 -4
  104. package/src/steam/steamcmdNarrator.test.ts +4 -4
  105. package/src/steam/workshop.test.ts +5 -5
  106. package/src/utils/changedSettingKeys.test.ts +65 -0
  107. package/src/utils/changedSettingKeys.ts +6 -0
  108. package/src/utils/describeExecFailure.test.ts +34 -0
  109. package/src/utils/describeExecFailure.ts +9 -0
  110. package/src/utils/index.ts +3 -0
  111. package/src/utils/sameSettingValue.test.ts +35 -0
  112. package/src/utils/sameSettingValue.ts +40 -0
  113. package/src/utils/token.test.ts +7 -7
  114. package/src/wine/protonCommand.test.ts +5 -5
  115. package/src/wine/wineCommand.test.ts +6 -6
  116. package/src/wine/winePrefix.test.ts +6 -6
  117. package/src/wire/activity.test.ts +95 -0
  118. package/src/wire/activity.ts +41 -0
  119. package/src/wire/index.ts +1 -0
  120. package/src/wire/panel.test.ts +91 -25
  121. package/src/wire/panel.ts +11 -0
@@ -1,12 +1,30 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import { tmpdir } from "node:os";
3
- import { BridgeAction, BridgeKind, type BridgeRow, BridgeSetupStepKind, type BridgeValues } from "../core";
3
+ import {
4
+ BridgeAction,
5
+ BridgeKind,
6
+ type BridgeRow,
7
+ BridgeSetupStepKind,
8
+ type BridgeUnavailable,
9
+ type BridgeValues,
10
+ type LocalizedText,
11
+ } from "../core";
4
12
  import type { BridgeDriver } from "../modules";
5
- import { BridgeLayout } from "../panel";
6
- import { type BridgeConfig, BridgeConfirm, BridgeEventName } from "../protocol";
13
+ import { BridgeControl, BridgeFormTarget, BridgeLayout } from "../panel";
14
+ import {
15
+ BRIDGE_ACTIVITY_KEYS,
16
+ BRIDGE_TEXT_LENGTH_LIMIT,
17
+ type BridgeConfig,
18
+ BridgeConfirm,
19
+ BridgeEventName,
20
+ } from "../protocol";
21
+ import { bridgeAvailabilitySchema } from "../wire";
22
+ import { type ActivityNormalizeOptions, createActivityReporter } from "./bridgeActivity";
7
23
  import { createContext } from "./bridgeContext";
8
24
  import { createEventRing } from "./bridgeEvents";
9
25
  import { log } from "./bridgeLog";
26
+ import { secretFieldKeys } from "./bridgeRedaction";
27
+ import { BridgeRefusalError } from "./bridgeRefusalError";
10
28
  import { createRegistry } from "./bridgeRegistry";
11
29
  import { createSupervisor } from "./bridgeSupervisor";
12
30
 
@@ -39,7 +57,7 @@ const baseDriver = (): BridgeDriver => ({
39
57
  },
40
58
  });
41
59
 
42
- const registryFor = (driver: BridgeDriver, variables: Record<string, string> = {}) => {
60
+ const harnessFor = (driver: BridgeDriver, variables: Record<string, string> = {}) => {
43
61
  const settings = {
44
62
  ...config(),
45
63
  variables,
@@ -58,6 +76,19 @@ const registryFor = (driver: BridgeDriver, variables: Record<string, string> = {
58
76
 
59
77
  const ring = createEventRing(driver.events, supervisor);
60
78
 
79
+ const activity: ActivityNormalizeOptions = {
80
+ secretKeys: new Set(secretFieldKeys(driver)),
81
+ redact: (text) => text,
82
+ warn: log.warn,
83
+ };
84
+
85
+ const reporter = createActivityReporter({
86
+ ...activity,
87
+ onEmit(event, payload) {
88
+ ring.emit(event, payload);
89
+ },
90
+ });
91
+
61
92
  const context = createContext(settings, supervisor, {
62
93
  onLog: log,
63
94
  onEmit(event, payload) {
@@ -66,16 +97,26 @@ const registryFor = (driver: BridgeDriver, variables: Record<string, string> = {
66
97
  onSetup() {
67
98
  return;
68
99
  },
100
+ onActivity(entry) {
101
+ reporter.report(entry);
102
+ },
69
103
  async onRestart() {
70
104
  return;
71
105
  },
72
106
  });
73
107
 
74
- return createRegistry(driver, context, supervisor, ring);
108
+ return {
109
+ registry: createRegistry(driver, context, supervisor, ring, activity),
110
+ ring,
111
+ };
112
+ };
113
+
114
+ const registryFor = (driver: BridgeDriver, variables: Record<string, string> = {}) => {
115
+ return harnessFor(driver, variables).registry;
75
116
  };
76
117
 
77
118
  describe("actions modules", () => {
78
- test("lists its action ids in the manifest", () => {
119
+ it("lists its action ids in the manifest", () => {
79
120
  const registry = registryFor({
80
121
  ...baseDriver(),
81
122
  modules: {
@@ -110,7 +151,7 @@ describe("actions modules", () => {
110
151
  ]);
111
152
  });
112
153
 
113
- test("hands the whole argument bag to the handler", async () => {
154
+ it("hands the whole argument bag to the handler", async () => {
114
155
  const received: BridgeValues[] = [];
115
156
 
116
157
  const registry = registryFor({
@@ -138,7 +179,7 @@ describe("actions modules", () => {
138
179
  ]);
139
180
  });
140
181
 
141
- test("refuses to run while the game is stopped", async () => {
182
+ it("refuses to run while the game is stopped", async () => {
142
183
  const registry = registryFor({
143
184
  ...baseDriver(),
144
185
  modules: {
@@ -161,7 +202,7 @@ describe("actions modules", () => {
161
202
  ).rejects.toThrow('module "broadcast" needs the game to be running');
162
203
  });
163
204
 
164
- test("rejects a driver that reuses a read action id", () => {
205
+ it("rejects a driver that reuses a read action id", () => {
165
206
  expect(() =>
166
207
  registryFor({
167
208
  ...baseDriver(),
@@ -181,7 +222,7 @@ describe("actions modules", () => {
181
222
  });
182
223
 
183
224
  describe("manifest events", () => {
184
- test("lists the log patterns and the names the driver raises itself", () => {
225
+ it("lists the log patterns and the names the driver raises itself", () => {
185
226
  const registry = registryFor({
186
227
  ...baseDriver(),
187
228
  events: {
@@ -211,7 +252,7 @@ describe("manifest events", () => {
211
252
  ]);
212
253
  });
213
254
 
214
- test("stays on the log patterns when the driver raises nothing itself", () => {
255
+ it("stays on the log patterns when the driver raises nothing itself", () => {
215
256
  const registry = registryFor({
216
257
  ...baseDriver(),
217
258
  events: {
@@ -232,7 +273,7 @@ describe("manifest events", () => {
232
273
  });
233
274
 
234
275
  describe("collection row actions", () => {
235
- test("passes the row and the arguments without the row id", async () => {
276
+ it("passes the row and the arguments without the row id", async () => {
236
277
  const seen: {
237
278
  row: BridgeRow;
238
279
  args: BridgeValues;
@@ -277,7 +318,7 @@ describe("collection row actions", () => {
277
318
  ]);
278
319
  });
279
320
 
280
- test("keeps listing the built-in actions beside the row actions", () => {
321
+ it("keeps listing the built-in actions beside the row actions", () => {
281
322
  const registry = registryFor({
282
323
  ...baseDriver(),
283
324
  modules: {
@@ -357,7 +398,7 @@ describe("offline row actions", () => {
357
398
  },
358
399
  });
359
400
 
360
- test("hands an offline action the id alone when the row is not listed", async () => {
401
+ it("hands an offline action the id alone when the row is not listed", async () => {
361
402
  const seen: BridgeRow[] = [];
362
403
  const registry = registryFor(
363
404
  rosterDriver(
@@ -378,7 +419,7 @@ describe("offline row actions", () => {
378
419
  ]);
379
420
  });
380
421
 
381
- test("still refuses an unlisted row for an action that is not offline", async () => {
422
+ it("still refuses an unlisted row for an action that is not offline", async () => {
382
423
  const registry = registryFor(
383
424
  rosterDriver(
384
425
  false,
@@ -396,7 +437,7 @@ describe("offline row actions", () => {
396
437
  });
397
438
 
398
439
  describe("preview modules", () => {
399
- test("lists the preview action in the manifest", () => {
440
+ it("lists the preview action in the manifest", () => {
400
441
  const registry = registryFor({
401
442
  ...baseDriver(),
402
443
  modules: {
@@ -425,7 +466,7 @@ describe("preview modules", () => {
425
466
  ]);
426
467
  });
427
468
 
428
- test("shows the module the pending values as variables and leaves the rest as they are stored", async () => {
469
+ it("shows the module the pending values as variables and leaves the rest as they are stored", async () => {
429
470
  const seen: {
430
471
  version: string | null;
431
472
  count: string | null;
@@ -479,7 +520,7 @@ describe("preview modules", () => {
479
520
  ]);
480
521
  });
481
522
 
482
- test("returns what the module answers", async () => {
523
+ it("returns what the module answers", async () => {
483
524
  const registry = registryFor({
484
525
  ...baseDriver(),
485
526
  modules: {
@@ -498,7 +539,7 @@ describe("preview modules", () => {
498
539
  },
499
540
  });
500
541
 
501
- expect(await registry.invoke("transition", BridgeAction.Preview)).toEqual({
542
+ expect((await registry.invoke("transition", BridgeAction.Preview)).result).toEqual({
502
543
  confirm: BridgeConfirm.Strong,
503
544
  lines: [
504
545
  {
@@ -509,7 +550,7 @@ describe("preview modules", () => {
509
550
  });
510
551
  });
511
552
 
512
- test("refuses an action a preview module does not answer", async () => {
553
+ it("refuses an action a preview module does not answer", async () => {
513
554
  const registry = registryFor({
514
555
  ...baseDriver(),
515
556
  modules: {
@@ -530,7 +571,7 @@ describe("preview modules", () => {
530
571
  });
531
572
 
532
573
  describe("detail modules", () => {
533
- test("lists read and its own action ids in the manifest", () => {
574
+ it("lists read and its own action ids in the manifest", () => {
534
575
  const registry = registryFor({
535
576
  ...baseDriver(),
536
577
  modules: {
@@ -555,12 +596,13 @@ describe("detail modules", () => {
555
596
  actions: [
556
597
  BridgeAction.Read,
557
598
  "update",
599
+ BridgeAction.Availability,
558
600
  ],
559
601
  },
560
602
  ]);
561
603
  });
562
604
 
563
- test("returns what read answers", async () => {
605
+ it("returns what read answers", async () => {
564
606
  const registry = registryFor({
565
607
  ...baseDriver(),
566
608
  modules: {
@@ -582,13 +624,13 @@ describe("detail modules", () => {
582
624
  },
583
625
  });
584
626
 
585
- expect(await registry.invoke("modpackStatus", BridgeAction.Read)).toMatchObject({
627
+ expect((await registry.invoke("modpackStatus", BridgeAction.Read)).result).toMatchObject({
586
628
  id: "modrinth:abc",
587
629
  title: "Better MC",
588
630
  });
589
631
  });
590
632
 
591
- test("hands the argument bag to an action and returns its outcome", async () => {
633
+ it("hands the argument bag to an action and returns its outcome", async () => {
592
634
  const received: BridgeValues[] = [];
593
635
 
594
636
  const registry = registryFor({
@@ -613,9 +655,11 @@ describe("detail modules", () => {
613
655
  });
614
656
 
615
657
  expect(
616
- await registry.invoke("modpackStatus", "update", {
617
- id: "modrinth:abc",
618
- }),
658
+ (
659
+ await registry.invoke("modpackStatus", "update", {
660
+ id: "modrinth:abc",
661
+ })
662
+ ).result,
619
663
  ).toEqual({
620
664
  variables: {
621
665
  MODPACK: "modrinth:abc:v2",
@@ -629,7 +673,7 @@ describe("detail modules", () => {
629
673
  ]);
630
674
  });
631
675
 
632
- test("refuses an action named after a read", () => {
676
+ it("refuses an action named after a read", () => {
633
677
  expect(() =>
634
678
  registryFor({
635
679
  ...baseDriver(),
@@ -646,7 +690,7 @@ describe("detail modules", () => {
646
690
  ).toThrow('module "modpackStatus" declares action "read", which is reserved for reads');
647
691
  });
648
692
 
649
- test("refuses an action the module does not answer", async () => {
693
+ it("refuses an action the module does not answer", async () => {
650
694
  const registry = registryFor({
651
695
  ...baseDriver(),
652
696
  modules: {
@@ -664,7 +708,7 @@ describe("detail modules", () => {
664
708
  });
665
709
 
666
710
  describe("the setup singleton", () => {
667
- test("rides the manifest as its own block and never joins the panel modules", () => {
711
+ it("rides the manifest as its own block and never joins the panel modules", () => {
668
712
  const steps = [
669
713
  {
670
714
  kind: BridgeSetupStepKind.Driver as const,
@@ -692,13 +736,13 @@ describe("the setup singleton", () => {
692
736
  expect(manifest.modules.some((module) => module.kind === BridgeKind.Setup)).toBe(false);
693
737
  });
694
738
 
695
- test("leaves the manifest block empty for a driver that declares no setup", () => {
739
+ it("leaves the manifest block empty for a driver that declares no setup", () => {
696
740
  expect(registryFor(baseDriver()).manifest().setup).toBeNull();
697
741
  });
698
742
  });
699
743
 
700
744
  describe("protected action dispatch", () => {
701
- test("advertises protection and refuses old callers before touching files", async () => {
745
+ it("advertises protection and refuses old callers before touching files", async () => {
702
746
  let calls = 0;
703
747
  const registry = registryFor({
704
748
  ...baseDriver(),
@@ -724,7 +768,7 @@ describe("protected action dispatch", () => {
724
768
  await registry.invoke("world", "replace", {}, "01993720-0000-7000-8000-000000000001");
725
769
  expect(calls).toBe(1);
726
770
  });
727
- test("rejects protection metadata for unknown actions or running-only modules", () => {
771
+ it("rejects protection metadata for unknown actions or running-only modules", () => {
728
772
  for (const module of [
729
773
  {
730
774
  kind: BridgeKind.Actions as const,
@@ -757,3 +801,1002 @@ describe("protected action dispatch", () => {
757
801
  }
758
802
  });
759
803
  });
804
+
805
+ describe("activity", () => {
806
+ const granted = {
807
+ name: BridgeEventName.AccessGranted,
808
+ text: {
809
+ ar: "أضفت Steve للقائمة البيضاء",
810
+ en: "Added Steve to the whitelist",
811
+ },
812
+ subject: "Steve",
813
+ };
814
+
815
+ const whitelistDriver = (fail = false): BridgeDriver => ({
816
+ ...baseDriver(),
817
+ modules: {
818
+ whitelist: {
819
+ kind: BridgeKind.Actions,
820
+ actions: {
821
+ add: async (context) => {
822
+ context.activity.report(granted);
823
+
824
+ if (fail) {
825
+ throw new Error("the whitelist file is read-only");
826
+ }
827
+ },
828
+ },
829
+ },
830
+ },
831
+ });
832
+
833
+ it("answers an invoke with its result and the entries the handler reported", async () => {
834
+ const { registry, ring } = harnessFor(whitelistDriver());
835
+
836
+ expect(await registry.invoke("whitelist", "add")).toEqual({
837
+ result: null,
838
+ activity: [
839
+ granted,
840
+ ],
841
+ });
842
+ expect(ring.drain(0).events).toEqual([]);
843
+ });
844
+
845
+ it("answers an empty list when the handler reported nothing", async () => {
846
+ const { registry } = harnessFor({
847
+ ...baseDriver(),
848
+ modules: {
849
+ gameplay: {
850
+ kind: BridgeKind.Actions,
851
+ actions: {
852
+ timeDay: async () => {
853
+ return;
854
+ },
855
+ },
856
+ },
857
+ },
858
+ });
859
+
860
+ expect(await registry.invoke("gameplay", "timeDay")).toEqual({
861
+ result: null,
862
+ activity: [],
863
+ });
864
+ });
865
+
866
+ it("sends what a failing handler reported to the events ring", async () => {
867
+ const { registry, ring } = harnessFor(whitelistDriver(true));
868
+
869
+ await expect(registry.invoke("whitelist", "add")).rejects.toThrow("read-only");
870
+
871
+ expect(ring.drain(0).events).toEqual([
872
+ {
873
+ seq: 1,
874
+ event: BridgeEventName.AccessGranted,
875
+ payload: {
876
+ [BRIDGE_ACTIVITY_KEYS.ar]: granted.text.ar,
877
+ [BRIDGE_ACTIVITY_KEYS.en]: granted.text.en,
878
+ [BRIDGE_ACTIVITY_KEYS.subject]: "Steve",
879
+ },
880
+ },
881
+ ]);
882
+ });
883
+
884
+ describe("settings writes", () => {
885
+ const settingsDriver = (options: {
886
+ stored: BridgeValues;
887
+ read?: () => Promise<BridgeValues>;
888
+ report?: boolean;
889
+ }): BridgeDriver => ({
890
+ ...baseDriver(),
891
+ panel: {
892
+ tabs: [
893
+ {
894
+ id: "settings",
895
+ title: {
896
+ ar: "الإعدادات",
897
+ en: "Settings",
898
+ },
899
+ sections: [
900
+ {
901
+ layout: BridgeLayout.Form,
902
+ id: "game",
903
+ target: BridgeFormTarget.Settings,
904
+ module: "gameSettings",
905
+ fields: [
906
+ {
907
+ key: "difficulty",
908
+ control: BridgeControl.Select,
909
+ label: {
910
+ ar: "الصعوبة",
911
+ en: "Difficulty",
912
+ },
913
+ },
914
+ {
915
+ key: "rconPassword",
916
+ control: BridgeControl.Secret,
917
+ label: {
918
+ ar: "كلمة سر RCON",
919
+ en: "RCON password",
920
+ },
921
+ },
922
+ ],
923
+ },
924
+ ],
925
+ },
926
+ ],
927
+ },
928
+ modules: {
929
+ gameSettings: {
930
+ kind: BridgeKind.Settings,
931
+ read: options.read ?? (async () => options.stored),
932
+ write: async (context, values) => {
933
+ Object.assign(options.stored, values);
934
+
935
+ if (options.report) {
936
+ context.activity.report({
937
+ name: BridgeEventName.SettingsApplied,
938
+ text: {
939
+ ar: "تطبّقت الإعدادات",
940
+ en: "Settings applied",
941
+ },
942
+ });
943
+ }
944
+ },
945
+ },
946
+ },
947
+ });
948
+
949
+ it("reports the keys whose value changed when the handler reported nothing", async () => {
950
+ const { registry } = harnessFor(
951
+ settingsDriver({
952
+ stored: {
953
+ difficulty: "easy",
954
+ maxPlayers: 10,
955
+ expRate: "1.000000",
956
+ pvp: "True",
957
+ },
958
+ }),
959
+ );
960
+
961
+ const { activity } = await registry.invoke("gameSettings", BridgeAction.Write, {
962
+ difficulty: "hard",
963
+ maxPlayers: "10",
964
+ expRate: 1,
965
+ pvp: true,
966
+ });
967
+
968
+ expect(activity).toEqual([
969
+ {
970
+ name: BridgeEventName.SettingsChanged,
971
+ text: {
972
+ ar: "تم تغيير الإعدادات",
973
+ en: "Changed the settings",
974
+ },
975
+ details: {
976
+ keys: "difficulty",
977
+ count: 1,
978
+ },
979
+ },
980
+ ]);
981
+ });
982
+
983
+ it("finishes the write even when a stored value cannot be compared", async () => {
984
+ const { registry } = harnessFor(
985
+ settingsDriver({
986
+ stored: {},
987
+ read: async () =>
988
+ ({
989
+ difficulty: {
990
+ toString() {
991
+ throw new Error("not a scalar");
992
+ },
993
+ },
994
+ }) as unknown as BridgeValues,
995
+ }),
996
+ );
997
+
998
+ expect(
999
+ await registry.invoke("gameSettings", BridgeAction.Write, {
1000
+ difficulty: "hard",
1001
+ }),
1002
+ ).toEqual({
1003
+ result: null,
1004
+ activity: [],
1005
+ });
1006
+ });
1007
+
1008
+ it("reports nothing when no value changed", async () => {
1009
+ const { registry } = harnessFor(
1010
+ settingsDriver({
1011
+ stored: {
1012
+ difficulty: "easy",
1013
+ },
1014
+ }),
1015
+ );
1016
+
1017
+ expect(
1018
+ (
1019
+ await registry.invoke("gameSettings", BridgeAction.Write, {
1020
+ difficulty: "easy",
1021
+ })
1022
+ ).activity,
1023
+ ).toEqual([]);
1024
+ });
1025
+
1026
+ it("keeps the handler's own entry instead of the default", async () => {
1027
+ const { registry } = harnessFor(
1028
+ settingsDriver({
1029
+ stored: {
1030
+ difficulty: "easy",
1031
+ },
1032
+ report: true,
1033
+ }),
1034
+ );
1035
+
1036
+ const { activity } = await registry.invoke("gameSettings", BridgeAction.Write, {
1037
+ difficulty: "hard",
1038
+ });
1039
+
1040
+ expect(activity.map((entry) => entry.name)).toEqual([
1041
+ BridgeEventName.SettingsApplied,
1042
+ ]);
1043
+ });
1044
+
1045
+ it("reports every written key when the settings could not be read first", async () => {
1046
+ const { registry } = harnessFor(
1047
+ settingsDriver({
1048
+ stored: {},
1049
+ read: async () => {
1050
+ throw new Error("the rest api is not up yet");
1051
+ },
1052
+ }),
1053
+ );
1054
+
1055
+ const { activity } = await registry.invoke("gameSettings", BridgeAction.Write, {
1056
+ difficulty: "hard",
1057
+ pvp: true,
1058
+ });
1059
+
1060
+ expect(activity.at(0)?.details).toEqual({
1061
+ keys: "difficulty,pvp",
1062
+ count: 2,
1063
+ });
1064
+ });
1065
+
1066
+ it("never carries the value of a secret field", async () => {
1067
+ const secret = "hunter22hunter22";
1068
+
1069
+ const { registry } = harnessFor(
1070
+ settingsDriver({
1071
+ stored: {
1072
+ rconPassword: "old-password-1234",
1073
+ },
1074
+ }),
1075
+ );
1076
+
1077
+ const { activity } = await registry.invoke("gameSettings", BridgeAction.Write, {
1078
+ rconPassword: secret,
1079
+ });
1080
+
1081
+ expect(activity.at(0)?.details).toEqual({
1082
+ keys: "rconPassword",
1083
+ count: 1,
1084
+ });
1085
+ expect(JSON.stringify(activity)).not.toContain(secret);
1086
+ });
1087
+
1088
+ it("drops a detail a handler keyed by a secret field", async () => {
1089
+ const { registry } = harnessFor({
1090
+ ...settingsDriver({
1091
+ stored: {},
1092
+ }),
1093
+ modules: {
1094
+ gameSettings: {
1095
+ kind: BridgeKind.Settings,
1096
+ read: async () => ({}),
1097
+ write: async (context, values) => {
1098
+ context.activity.report({
1099
+ name: BridgeEventName.SecretRotated,
1100
+ text: {
1101
+ ar: "تغيّرت كلمة سر RCON",
1102
+ en: "RCON password rotated",
1103
+ },
1104
+ details: {
1105
+ rconPassword: String(values.rconPassword),
1106
+ },
1107
+ });
1108
+ },
1109
+ },
1110
+ },
1111
+ });
1112
+
1113
+ const { activity } = await registry.invoke("gameSettings", BridgeAction.Write, {
1114
+ rconPassword: "hunter22hunter22",
1115
+ });
1116
+
1117
+ expect(activity).toEqual([
1118
+ {
1119
+ name: BridgeEventName.SecretRotated,
1120
+ text: {
1121
+ ar: "تغيّرت كلمة سر RCON",
1122
+ en: "RCON password rotated",
1123
+ },
1124
+ },
1125
+ ]);
1126
+ });
1127
+ });
1128
+
1129
+ it("reports no default for a catalog install", async () => {
1130
+ const { registry } = harnessFor({
1131
+ ...baseDriver(),
1132
+ modules: {
1133
+ mods: {
1134
+ kind: BridgeKind.Catalog,
1135
+ search: async () => ({
1136
+ hits: [],
1137
+ total: 0,
1138
+ providers: [],
1139
+ categories: [],
1140
+ sorts: [],
1141
+ }),
1142
+ installed: async () => [],
1143
+ install: async (_context, id) => ({
1144
+ id,
1145
+ provider: null,
1146
+ path: `mods/${id}.jar`,
1147
+ title: id,
1148
+ version: null,
1149
+ sizeBytes: 0,
1150
+ enabled: true,
1151
+ gameVersion: null,
1152
+ stale: false,
1153
+ pageUrl: null,
1154
+ icon: null,
1155
+ }),
1156
+ remove: async () => {
1157
+ return;
1158
+ },
1159
+ },
1160
+ },
1161
+ });
1162
+
1163
+ expect(
1164
+ (
1165
+ await registry.invoke("mods", BridgeAction.Install, {
1166
+ id: "sodium",
1167
+ })
1168
+ ).activity,
1169
+ ).toEqual([]);
1170
+ });
1171
+ });
1172
+
1173
+ describe("availability", () => {
1174
+ const activeWorld: LocalizedText = {
1175
+ ar: "ما تقدر تحذف العالم الشغّال",
1176
+ en: "You cannot delete the active world",
1177
+ };
1178
+
1179
+ const alreadyActive: LocalizedText = {
1180
+ ar: "هذا العالم شغّال من قبل",
1181
+ en: "This world is already active",
1182
+ };
1183
+
1184
+ const worldsDriver = (spies: { listed: number; deleted: string[]; hookThrows?: boolean }): BridgeDriver => ({
1185
+ ...baseDriver(),
1186
+ modules: {
1187
+ worlds: {
1188
+ kind: BridgeKind.Collection,
1189
+ list: async () => {
1190
+ spies.listed += 1;
1191
+
1192
+ return [
1193
+ {
1194
+ id: "world",
1195
+ active: true,
1196
+ },
1197
+ {
1198
+ id: "old",
1199
+ active: false,
1200
+ },
1201
+ ];
1202
+ },
1203
+ actions: {
1204
+ delete: async (_context, row) => {
1205
+ spies.deleted.push(row.id);
1206
+ },
1207
+ activate: async () => {
1208
+ return;
1209
+ },
1210
+ },
1211
+ unavailable: async (_context, row): Promise<BridgeUnavailable> => {
1212
+ if (spies.hookThrows) {
1213
+ throw new Error("the level file could not be read");
1214
+ }
1215
+
1216
+ return row.active === true
1217
+ ? {
1218
+ delete: activeWorld,
1219
+ activate: alreadyActive,
1220
+ }
1221
+ : {};
1222
+ },
1223
+ },
1224
+ },
1225
+ });
1226
+
1227
+ const refusalOf = async (promise: Promise<unknown>) => {
1228
+ try {
1229
+ await promise;
1230
+ } catch (error) {
1231
+ return error;
1232
+ }
1233
+
1234
+ throw new Error("the invoke did not refuse");
1235
+ };
1236
+
1237
+ it("advertises availability only for a collection that declares the hook", () => {
1238
+ const registry = registryFor({
1239
+ ...baseDriver(),
1240
+ modules: {
1241
+ players: {
1242
+ kind: BridgeKind.Collection,
1243
+ list: async () => [],
1244
+ actions: {
1245
+ kick: async () => {
1246
+ return;
1247
+ },
1248
+ },
1249
+ },
1250
+ worlds: {
1251
+ kind: BridgeKind.Collection,
1252
+ list: async () => [],
1253
+ actions: {
1254
+ delete: async () => {
1255
+ return;
1256
+ },
1257
+ },
1258
+ unavailable: async () => ({}),
1259
+ },
1260
+ },
1261
+ });
1262
+
1263
+ expect(registry.manifest().modules.map((module) => module.actions)).toEqual([
1264
+ [
1265
+ BridgeAction.List,
1266
+ "kick",
1267
+ ],
1268
+ [
1269
+ BridgeAction.List,
1270
+ "delete",
1271
+ BridgeAction.Availability,
1272
+ ],
1273
+ ]);
1274
+ });
1275
+
1276
+ it("answers the reasons per row and leaves available rows out", async () => {
1277
+ const registry = registryFor(
1278
+ worldsDriver({
1279
+ listed: 0,
1280
+ deleted: [],
1281
+ }),
1282
+ );
1283
+
1284
+ expect((await registry.invoke("worlds", BridgeAction.Availability)).result).toEqual({
1285
+ module: {},
1286
+ subjects: {
1287
+ world: {
1288
+ delete: activeWorld,
1289
+ activate: alreadyActive,
1290
+ },
1291
+ },
1292
+ });
1293
+ });
1294
+
1295
+ it("narrows the answer to one row when asked for its id", async () => {
1296
+ const registry = registryFor(
1297
+ worldsDriver({
1298
+ listed: 0,
1299
+ deleted: [],
1300
+ }),
1301
+ );
1302
+
1303
+ expect(
1304
+ (
1305
+ await registry.invoke("worlds", BridgeAction.Availability, {
1306
+ id: "old",
1307
+ })
1308
+ ).result,
1309
+ ).toEqual({
1310
+ module: {},
1311
+ subjects: {},
1312
+ });
1313
+ });
1314
+
1315
+ it("refuses an unavailable row action before the driver is entered", async () => {
1316
+ const spies = {
1317
+ listed: 0,
1318
+ deleted: [] as string[],
1319
+ };
1320
+ const registry = registryFor(worldsDriver(spies));
1321
+
1322
+ const error = await refusalOf(
1323
+ registry.invoke("worlds", "delete", {
1324
+ id: "world",
1325
+ }),
1326
+ );
1327
+
1328
+ expect(error).toBeInstanceOf(BridgeRefusalError);
1329
+ expect((error as BridgeRefusalError).text).toEqual(activeWorld);
1330
+ expect(spies.deleted).toEqual([]);
1331
+
1332
+ await registry.invoke("worlds", "delete", {
1333
+ id: "old",
1334
+ });
1335
+
1336
+ expect(spies.deleted).toEqual([
1337
+ "old",
1338
+ ]);
1339
+ });
1340
+
1341
+ it("lets the action run when the hook itself fails", async () => {
1342
+ const spies = {
1343
+ listed: 0,
1344
+ deleted: [] as string[],
1345
+ hookThrows: true,
1346
+ };
1347
+ const registry = registryFor(worldsDriver(spies));
1348
+
1349
+ await registry.invoke("worlds", "delete", {
1350
+ id: "world",
1351
+ });
1352
+
1353
+ expect(spies.deleted).toEqual([
1354
+ "world",
1355
+ ]);
1356
+ });
1357
+
1358
+ it("lets an availability read join a list already in flight and never reuses a settled one", async () => {
1359
+ const spies = {
1360
+ listed: 0,
1361
+ deleted: [] as string[],
1362
+ };
1363
+ const registry = registryFor(worldsDriver(spies));
1364
+
1365
+ await Promise.all([
1366
+ registry.invoke("worlds", BridgeAction.List),
1367
+ registry.invoke("worlds", BridgeAction.Availability),
1368
+ ]);
1369
+
1370
+ expect(spies.listed).toBe(1);
1371
+
1372
+ await registry.invoke("worlds", BridgeAction.Availability);
1373
+ await registry.invoke("worlds", BridgeAction.List);
1374
+
1375
+ expect(spies.listed).toBe(3);
1376
+ });
1377
+
1378
+ it("lists fresh for every list read and every action", async () => {
1379
+ const spies = {
1380
+ listed: 0,
1381
+ deleted: [] as string[],
1382
+ };
1383
+ const registry = registryFor(worldsDriver(spies));
1384
+
1385
+ await Promise.all([
1386
+ registry.invoke("worlds", BridgeAction.List),
1387
+ registry.invoke("worlds", BridgeAction.List),
1388
+ ]);
1389
+ await registry.invoke("worlds", "delete", {
1390
+ id: "old",
1391
+ });
1392
+
1393
+ expect(spies.listed).toBe(3);
1394
+ });
1395
+
1396
+ it("forgets a list in flight when any module mutates", async () => {
1397
+ let listed = 0;
1398
+ let release = () => {};
1399
+ const gate = new Promise<void>((resolve) => {
1400
+ release = resolve;
1401
+ });
1402
+
1403
+ const registry = registryFor({
1404
+ ...baseDriver(),
1405
+ modules: {
1406
+ worlds: {
1407
+ kind: BridgeKind.Collection,
1408
+ list: async () => {
1409
+ listed += 1;
1410
+
1411
+ if (listed === 1) {
1412
+ await gate;
1413
+ }
1414
+
1415
+ return [];
1416
+ },
1417
+ actions: {
1418
+ delete: async () => {
1419
+ return;
1420
+ },
1421
+ },
1422
+ unavailable: async () => ({}),
1423
+ },
1424
+ gameplay: {
1425
+ kind: BridgeKind.Actions,
1426
+ actions: {
1427
+ activate: async () => {
1428
+ return;
1429
+ },
1430
+ },
1431
+ },
1432
+ },
1433
+ });
1434
+
1435
+ const first = registry.invoke("worlds", BridgeAction.List);
1436
+
1437
+ await Bun.sleep(0);
1438
+ await registry.invoke("gameplay", "activate");
1439
+ await registry.invoke("worlds", BridgeAction.Availability);
1440
+
1441
+ expect(listed).toBe(2);
1442
+
1443
+ release();
1444
+ await first;
1445
+ });
1446
+
1447
+ it("drops reasons for actions the module cannot refuse and clamps long ones", async () => {
1448
+ const registry = registryFor({
1449
+ ...baseDriver(),
1450
+ modules: {
1451
+ worlds: {
1452
+ kind: BridgeKind.Collection,
1453
+ list: async () => [
1454
+ {
1455
+ id: "world",
1456
+ },
1457
+ ],
1458
+ actions: {
1459
+ delete: async () => {
1460
+ return;
1461
+ },
1462
+ },
1463
+ unavailable: async () => ({
1464
+ list: activeWorld,
1465
+ rename: activeWorld,
1466
+ delete: {
1467
+ ar: "ا".repeat(BRIDGE_TEXT_LENGTH_LIMIT + 20),
1468
+ en: "a".repeat(BRIDGE_TEXT_LENGTH_LIMIT + 20),
1469
+ },
1470
+ }),
1471
+ },
1472
+ },
1473
+ });
1474
+
1475
+ expect((await registry.invoke("worlds", BridgeAction.Availability)).result).toEqual({
1476
+ module: {},
1477
+ subjects: {
1478
+ world: {
1479
+ delete: {
1480
+ ar: "ا".repeat(BRIDGE_TEXT_LENGTH_LIMIT),
1481
+ en: "a".repeat(BRIDGE_TEXT_LENGTH_LIMIT),
1482
+ },
1483
+ },
1484
+ },
1485
+ });
1486
+ });
1487
+
1488
+ it("drops entries the wire would refuse so the answer always validates", async () => {
1489
+ const registry = registryFor({
1490
+ ...baseDriver(),
1491
+ modules: {
1492
+ worlds: {
1493
+ kind: BridgeKind.Collection,
1494
+ list: async () => [
1495
+ {
1496
+ id: "",
1497
+ },
1498
+ {
1499
+ id: "world",
1500
+ },
1501
+ ],
1502
+ actions: {
1503
+ "not a key": async () => {
1504
+ return;
1505
+ },
1506
+ delete: async () => {
1507
+ return;
1508
+ },
1509
+ },
1510
+ unavailable: async () => ({
1511
+ "not a key": activeWorld,
1512
+ delete: activeWorld,
1513
+ }),
1514
+ },
1515
+ },
1516
+ });
1517
+
1518
+ const { result } = await registry.invoke("worlds", BridgeAction.Availability);
1519
+
1520
+ expect(result).toEqual({
1521
+ module: {},
1522
+ subjects: {
1523
+ world: {
1524
+ delete: activeWorld,
1525
+ },
1526
+ },
1527
+ });
1528
+ expect(bridgeAvailabilitySchema.safeParse(result).success).toBe(true);
1529
+ });
1530
+
1531
+ it("enforces the allow-list of a detail and answers its reasons", async () => {
1532
+ const updates: BridgeValues[] = [];
1533
+ let pressable: string[] = [];
1534
+ const stale: LocalizedText = {
1535
+ ar: "المود باك محدّث",
1536
+ en: "The modpack is up to date",
1537
+ };
1538
+
1539
+ const registry = registryFor({
1540
+ ...baseDriver(),
1541
+ modules: {
1542
+ modpackStatus: {
1543
+ kind: BridgeKind.Detail,
1544
+ read: async () => ({
1545
+ id: "modrinth:abc",
1546
+ title: "Better MC",
1547
+ subtitle: null,
1548
+ description: null,
1549
+ image: null,
1550
+ badges: [],
1551
+ stats: [],
1552
+ links: [],
1553
+ stale: false,
1554
+ actions: pressable,
1555
+ unavailable: {
1556
+ update: stale,
1557
+ },
1558
+ }),
1559
+ actions: {
1560
+ update: async (_context, args) => {
1561
+ updates.push(args);
1562
+
1563
+ return null;
1564
+ },
1565
+ repair: async () => null,
1566
+ },
1567
+ },
1568
+ },
1569
+ });
1570
+
1571
+ expect((await registry.invoke("modpackStatus", BridgeAction.Availability)).result).toEqual({
1572
+ module: {
1573
+ update: stale,
1574
+ repair: {
1575
+ ar: "هذا الإجراء غير متاح حاليًا",
1576
+ en: "This action isn't available right now",
1577
+ },
1578
+ },
1579
+ subjects: {},
1580
+ });
1581
+
1582
+ const error = await refusalOf(registry.invoke("modpackStatus", "update"));
1583
+
1584
+ expect(error).toBeInstanceOf(BridgeRefusalError);
1585
+ expect((error as BridgeRefusalError).text).toEqual(stale);
1586
+ expect(updates).toEqual([]);
1587
+
1588
+ pressable = [
1589
+ "update",
1590
+ ];
1591
+
1592
+ await registry.invoke("modpackStatus", "update");
1593
+
1594
+ expect(updates).toHaveLength(1);
1595
+ });
1596
+
1597
+ it("refuses an actions module, a settings write and a catalog removal through their hooks", async () => {
1598
+ const closed: LocalizedText = {
1599
+ ar: "مقفل الحين",
1600
+ en: "Closed right now",
1601
+ };
1602
+ const entered: string[] = [];
1603
+
1604
+ const registry = registryFor({
1605
+ ...baseDriver(),
1606
+ modules: {
1607
+ gameplay: {
1608
+ kind: BridgeKind.Actions,
1609
+ actions: {
1610
+ wipe: async () => {
1611
+ entered.push("wipe");
1612
+ },
1613
+ },
1614
+ unavailable: async () => ({
1615
+ wipe: closed,
1616
+ }),
1617
+ },
1618
+ gameSettings: {
1619
+ kind: BridgeKind.Settings,
1620
+ read: async () => ({}),
1621
+ write: async () => {
1622
+ entered.push("write");
1623
+ },
1624
+ unavailable: async () => ({
1625
+ write: closed,
1626
+ }),
1627
+ },
1628
+ mods: {
1629
+ kind: BridgeKind.Catalog,
1630
+ search: async () => ({
1631
+ hits: [],
1632
+ total: 0,
1633
+ providers: [],
1634
+ categories: [],
1635
+ sorts: [],
1636
+ }),
1637
+ installed: async () => [
1638
+ {
1639
+ id: "jei",
1640
+ provider: null,
1641
+ path: "mods/jei.jar",
1642
+ title: "JEI",
1643
+ version: null,
1644
+ sizeBytes: 1,
1645
+ enabled: true,
1646
+ gameVersion: null,
1647
+ stale: false,
1648
+ pageUrl: null,
1649
+ icon: null,
1650
+ },
1651
+ ],
1652
+ install: async () => {
1653
+ throw new Error("not in this test");
1654
+ },
1655
+ remove: async () => {
1656
+ entered.push("remove");
1657
+ },
1658
+ unavailable: async () => ({
1659
+ remove: closed,
1660
+ }),
1661
+ },
1662
+ },
1663
+ });
1664
+
1665
+ expect(registry.manifest().modules.map((module) => module.actions.includes(BridgeAction.Availability))).toEqual([
1666
+ true,
1667
+ true,
1668
+ true,
1669
+ ]);
1670
+
1671
+ for (const [module, action, args] of [
1672
+ [
1673
+ "gameplay",
1674
+ "wipe",
1675
+ {},
1676
+ ],
1677
+ [
1678
+ "gameSettings",
1679
+ BridgeAction.Write,
1680
+ {
1681
+ motd: "hi",
1682
+ },
1683
+ ],
1684
+ [
1685
+ "mods",
1686
+ BridgeAction.Remove,
1687
+ {
1688
+ id: "jei",
1689
+ },
1690
+ ],
1691
+ ] as const) {
1692
+ const error = await refusalOf(registry.invoke(module, action, args));
1693
+
1694
+ expect(error).toBeInstanceOf(BridgeRefusalError);
1695
+ expect((error as BridgeRefusalError).text).toEqual(closed);
1696
+ }
1697
+
1698
+ expect(entered).toEqual([]);
1699
+
1700
+ expect((await registry.invoke("mods", BridgeAction.Availability)).result).toEqual({
1701
+ module: {},
1702
+ subjects: {
1703
+ jei: {
1704
+ remove: closed,
1705
+ },
1706
+ },
1707
+ });
1708
+ expect((await registry.invoke("gameplay", BridgeAction.Availability)).result).toEqual({
1709
+ module: {
1710
+ wipe: closed,
1711
+ },
1712
+ subjects: {},
1713
+ });
1714
+ });
1715
+
1716
+ it("does not answer availability for a module that declares no hook", async () => {
1717
+ const registry = registryFor({
1718
+ ...baseDriver(),
1719
+ modules: {
1720
+ players: {
1721
+ kind: BridgeKind.Collection,
1722
+ list: async () => [],
1723
+ },
1724
+ },
1725
+ });
1726
+
1727
+ await expect(registry.invoke("players", BridgeAction.Availability)).rejects.toThrow(
1728
+ 'module "players" does not support "availability"',
1729
+ );
1730
+ });
1731
+
1732
+ it("refuses a driver that names a row action availability", () => {
1733
+ expect(() =>
1734
+ registryFor({
1735
+ ...baseDriver(),
1736
+ modules: {
1737
+ worlds: {
1738
+ kind: BridgeKind.Collection,
1739
+ list: async () => [],
1740
+ actions: {
1741
+ availability: async () => {
1742
+ return;
1743
+ },
1744
+ },
1745
+ },
1746
+ },
1747
+ }),
1748
+ ).toThrow('module "worlds" declares action "availability", which is reserved for reads');
1749
+ });
1750
+
1751
+ it("turns the gates that never enter the driver into refusals without text", async () => {
1752
+ const registry = registryFor({
1753
+ ...baseDriver(),
1754
+ modules: {
1755
+ gameplay: {
1756
+ kind: BridgeKind.Actions,
1757
+ requiresRunning: true,
1758
+ actions: {
1759
+ timeDay: async () => {
1760
+ return;
1761
+ },
1762
+ },
1763
+ },
1764
+ world: {
1765
+ kind: BridgeKind.Actions,
1766
+ protectedActions: [
1767
+ "replace",
1768
+ ],
1769
+ actions: {
1770
+ replace: async () => {
1771
+ return;
1772
+ },
1773
+ },
1774
+ },
1775
+ players: {
1776
+ kind: BridgeKind.Collection,
1777
+ list: async () => [],
1778
+ actions: {
1779
+ kick: async () => {
1780
+ return;
1781
+ },
1782
+ },
1783
+ },
1784
+ },
1785
+ });
1786
+
1787
+ for (const invoke of [
1788
+ registry.invoke("missing", "timeDay"),
1789
+ registry.invoke("gameplay", "timeDay"),
1790
+ registry.invoke("world", "replace"),
1791
+ registry.invoke("players", "kick", {
1792
+ id: "meslzy",
1793
+ }),
1794
+ registry.invoke("gameplay", "nightTime"),
1795
+ ]) {
1796
+ const error = await refusalOf(invoke);
1797
+
1798
+ expect(error).toBeInstanceOf(BridgeRefusalError);
1799
+ expect((error as BridgeRefusalError).text).toBeNull();
1800
+ }
1801
+ });
1802
+ });