@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.
- package/package.json +3 -2
- package/rules/package.md +60 -0
- package/src/blog/articleFrontmatter.test.ts +10 -10
- package/src/blog/compile.test.ts +9 -9
- package/src/bot/botDeploy.test.ts +16 -16
- package/src/bot/botEnvironment.test.ts +8 -8
- package/src/bot/botIdentity.test.ts +12 -12
- package/src/bot/botPanel.test.ts +41 -11
- package/src/bot/botPanel.ts +13 -5
- package/src/bot/botState.test.ts +3 -3
- package/src/bot/botTerminal.test.ts +5 -5
- package/src/bot/botVariables.test.ts +8 -8
- package/src/bot/detect/botToken.test.ts +6 -6
- package/src/bot/detect/detect.test.ts +21 -21
- package/src/bot/detect/detectTable.test.ts +5 -5
- package/src/bot/detect/envExample.test.ts +2 -2
- package/src/bot/detect/envScan.test.ts +4 -4
- package/src/bot/detect/inferBotPlatform.test.ts +4 -4
- package/src/bot/detect/keys.test.ts +3 -3
- package/src/bot/detect/sensitiveKey.test.ts +5 -5
- package/src/bot/detect/serverkJson.test.ts +5 -5
- package/src/catalogs/curseforge.test.ts +35 -35
- package/src/catalogs/thunderstore.test.ts +9 -9
- package/src/cli/detectTable.test.ts +3 -3
- package/src/cli/link.test.ts +11 -11
- package/src/cli/main.ts +8 -0
- package/src/cli/rules.test.ts +198 -0
- package/src/cli/rules.ts +188 -0
- package/src/cli/validate.test.ts +123 -29
- package/src/cli/validate.ts +37 -1
- package/src/codecs/ini.test.ts +13 -13
- package/src/codecs/javaProperties.test.ts +11 -11
- package/src/codecs/sourceCfg.test.ts +14 -14
- package/src/codecs/xmlProperties.test.ts +10 -10
- package/src/context/bridgeContext.ts +6 -1
- package/src/core/bridgeAction.ts +2 -0
- package/src/core/bridgeActivity.ts +9 -0
- package/src/core/bridgeAvailability.ts +10 -0
- package/src/core/bridgePort.test.ts +6 -6
- package/src/core/bridgeUserError.test.ts +13 -3
- package/src/core/bridgeUserError.ts +6 -2
- package/src/core/bridgeValue.test.ts +5 -5
- package/src/core/describeDownloadFailure.test.ts +58 -0
- package/src/core/describeDownloadFailure.ts +69 -0
- package/src/core/index.ts +3 -0
- package/src/guides/markdown.test.ts +31 -31
- package/src/install/installStamp.test.ts +9 -9
- package/src/manifest/extension.ts +36 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +208 -90
- package/src/manifest/manifest.ts +34 -0
- package/src/modules/bridge.ts +8 -1
- package/src/modules/bridgeModule.ts +13 -1
- package/src/net/loopback.test.ts +10 -10
- package/src/panel/bridgeDetail.ts +2 -1
- package/src/presence/rosterSync.test.ts +7 -7
- package/src/probes/a2s.test.ts +5 -5
- package/src/probes/minecraftPing.test.ts +17 -17
- package/src/probes/raknetPing.test.ts +13 -13
- package/src/protocol/bridgeActivity.ts +54 -0
- package/src/protocol/bridgeEvents.ts +30 -0
- package/src/protocol/bridgeFrame.ts +10 -1
- package/src/protocol/index.ts +1 -0
- package/src/rcon/rconAccess.test.ts +20 -20
- package/src/rcon/rconAccess.ts +13 -13
- package/src/rcon/rconClient.test.ts +49 -49
- package/src/rcon/rconFrame.test.ts +14 -14
- package/src/rcon/rconIds.test.ts +4 -4
- package/src/rcon/rconWriter.test.ts +6 -6
- package/src/runtime/bridgeActions.ts +19 -2
- package/src/runtime/bridgeActivity.test.ts +318 -0
- package/src/runtime/bridgeActivity.ts +268 -0
- package/src/runtime/bridgeBackupLease.test.ts +9 -9
- package/src/runtime/bridgeConfigReader.test.ts +5 -5
- package/src/runtime/bridgeConsoleHandler.test.ts +8 -8
- package/src/runtime/bridgeContext.test.ts +11 -5
- package/src/runtime/bridgeContext.ts +8 -1
- package/src/runtime/bridgeDriver.test.ts +114 -93
- package/src/runtime/bridgeDriver.ts +10 -7
- package/src/runtime/bridgeEvents.test.ts +15 -15
- package/src/runtime/bridgeFiles.test.ts +54 -54
- package/src/runtime/bridgeInstall.test.ts +10 -10
- package/src/runtime/bridgeLiveConfig.test.ts +6 -6
- package/src/runtime/bridgeLog.ts +10 -0
- package/src/runtime/bridgePending.test.ts +6 -6
- package/src/runtime/bridgeRedaction.test.ts +16 -16
- package/src/runtime/bridgeRefusalError.ts +13 -0
- package/src/runtime/bridgeRegistry.test.ts +1077 -34
- package/src/runtime/bridgeRegistry.ts +464 -65
- package/src/runtime/bridgeScopedContext.ts +55 -0
- package/src/runtime/bridgeServer.test.ts +424 -24
- package/src/runtime/bridgeServer.ts +125 -35
- package/src/runtime/bridgeSetup.test.ts +27 -27
- package/src/runtime/bridgeSetup.ts +1 -1
- package/src/runtime/bridgeSetupHandlers.test.ts +80 -17
- package/src/runtime/bridgeSetupHandlers.ts +45 -10
- package/src/runtime/bridgeSignal.test.ts +7 -7
- package/src/runtime/bridgeSupervisor.test.ts +42 -42
- package/src/runtime/serve.ts +45 -3
- package/src/steam/avatars.test.ts +13 -13
- package/src/steam/steamApp.test.ts +8 -8
- package/src/steam/steamcmd.test.ts +44 -13
- package/src/steam/steamcmd.ts +27 -4
- package/src/steam/steamcmdNarrator.test.ts +4 -4
- package/src/steam/workshop.test.ts +5 -5
- package/src/utils/changedSettingKeys.test.ts +65 -0
- package/src/utils/changedSettingKeys.ts +6 -0
- package/src/utils/describeExecFailure.test.ts +34 -0
- package/src/utils/describeExecFailure.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/sameSettingValue.test.ts +35 -0
- package/src/utils/sameSettingValue.ts +40 -0
- package/src/utils/token.test.ts +7 -7
- package/src/wine/protonCommand.test.ts +5 -5
- package/src/wine/wineCommand.test.ts +6 -6
- package/src/wine/winePrefix.test.ts +6 -6
- package/src/wire/activity.test.ts +95 -0
- package/src/wire/activity.ts +41 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/panel.test.ts +91 -25
- package/src/wire/panel.ts +11 -0
|
@@ -1,12 +1,30 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
617
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|