@serverkgg/bridge 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/bot/botPanel.test.ts +30 -0
- package/src/bot/botPanel.ts +9 -1
- package/src/cli/validate.test.ts +73 -1
- package/src/cli/validate.ts +35 -1
- 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/bridgeUserError.test.ts +10 -0
- package/src/core/bridgeUserError.ts +6 -2
- package/src/core/describeDownloadFailure.test.ts +58 -0
- package/src/core/describeDownloadFailure.ts +69 -0
- package/src/core/index.ts +3 -0
- package/src/modules/bridge.ts +8 -1
- package/src/modules/bridgeModule.ts +13 -1
- package/src/panel/bridgeDetail.ts +2 -1
- 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/runtime/bridgeActions.ts +19 -2
- package/src/runtime/bridgeActivity.test.ts +318 -0
- package/src/runtime/bridgeActivity.ts +268 -0
- package/src/runtime/bridgeContext.test.ts +6 -0
- package/src/runtime/bridgeContext.ts +8 -1
- package/src/runtime/bridgeDriver.test.ts +21 -0
- package/src/runtime/bridgeDriver.ts +10 -7
- package/src/runtime/bridgeLog.ts +10 -0
- package/src/runtime/bridgeRefusalError.ts +13 -0
- package/src/runtime/bridgeRegistry.test.ts +1053 -10
- package/src/runtime/bridgeRegistry.ts +464 -65
- package/src/runtime/bridgeScopedContext.ts +55 -0
- package/src/runtime/bridgeServer.test.ts +404 -4
- package/src/runtime/bridgeServer.ts +125 -35
- package/src/runtime/bridgeSetupHandlers.test.ts +69 -6
- package/src/runtime/bridgeSetupHandlers.ts +45 -10
- package/src/runtime/serve.ts +45 -3
- package/src/steam/steamcmd.test.ts +32 -1
- package/src/steam/steamcmd.ts +27 -4
- 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/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 +67 -1
- package/src/wire/panel.ts +11 -0
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import { describe, expect, test } 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,12 +97,22 @@ 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", () => {
|
|
@@ -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
|
{
|
|
@@ -555,6 +596,7 @@ describe("detail modules", () => {
|
|
|
555
596
|
actions: [
|
|
556
597
|
BridgeAction.Read,
|
|
557
598
|
"update",
|
|
599
|
+
BridgeAction.Availability,
|
|
558
600
|
],
|
|
559
601
|
},
|
|
560
602
|
]);
|
|
@@ -582,7 +624,7 @@ 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
|
});
|
|
@@ -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",
|
|
@@ -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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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 is not 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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
test("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
|
+
});
|