@serverkgg/bridge 0.1.13 → 0.1.15

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 (35) hide show
  1. package/package.json +1 -1
  2. package/rules/package.md +3 -0
  3. package/src/catalogs/curseforge.test.ts +46 -0
  4. package/src/catalogs/curseforge.ts +20 -0
  5. package/src/context/bridgeContext.ts +1 -0
  6. package/src/manifest/extension.ts +1 -0
  7. package/src/manifest/manifest.test.ts +18 -0
  8. package/src/modules/bridge.ts +13 -2
  9. package/src/modules/bridgeModule.ts +20 -4
  10. package/src/panel/bridgePanel.ts +2 -0
  11. package/src/protocol/bridgeCatalog.ts +7 -0
  12. package/src/protocol/bridgeCatalogSource.ts +9 -0
  13. package/src/protocol/bridgeConfig.ts +1 -0
  14. package/src/protocol/bridgeFrame.ts +1 -0
  15. package/src/protocol/bridgeLimits.ts +6 -0
  16. package/src/protocol/bridgeManifest.ts +2 -0
  17. package/src/protocol/index.ts +1 -0
  18. package/src/runtime/bridgeActions.ts +9 -2
  19. package/src/runtime/bridgeConfigReader.test.ts +31 -0
  20. package/src/runtime/bridgeConfigReader.ts +1 -0
  21. package/src/runtime/bridgeContext.test.ts +53 -0
  22. package/src/runtime/bridgeContext.ts +3 -0
  23. package/src/runtime/bridgeDriver.test.ts +347 -1
  24. package/src/runtime/bridgeDriver.ts +130 -18
  25. package/src/runtime/bridgeLiveConfig.test.ts +137 -1
  26. package/src/runtime/bridgeLiveConfig.ts +46 -2
  27. package/src/runtime/bridgeRegistry.test.ts +220 -0
  28. package/src/runtime/bridgeRegistry.ts +27 -1
  29. package/src/runtime/bridgeSample.test.ts +63 -0
  30. package/src/runtime/bridgeSample.ts +22 -0
  31. package/src/runtime/serve.ts +5 -12
  32. package/src/steam/workshop.test.ts +19 -1
  33. package/src/steam/workshop.ts +12 -0
  34. package/src/wire/panel.test.ts +177 -0
  35. package/src/wire/panel.ts +16 -0
@@ -2,8 +2,11 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
2
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { BridgeKind } from "../core";
6
+ import type { BridgeDriver } from "../modules";
7
+ import { BridgeControl, BridgeFormTarget, BridgeLayout } from "../panel";
5
8
  import type { BridgeConfig } from "../protocol";
6
- import { createBridgeLiveConfig } from "./bridgeLiveConfig";
9
+ import { createBridgeLiveConfig, variableDefaultsOf } from "./bridgeLiveConfig";
7
10
 
8
11
  const configWith = (secrets: Record<string, string>, variables: Record<string, string> = {}): BridgeConfig => ({
9
12
  code: "wsvv8fakxm",
@@ -146,4 +149,137 @@ describe("the bridge live config reader", () => {
146
149
  expect(live.memoryMb()).toBe(4096);
147
150
  expect(live.cpuPercent()).toBe(200);
148
151
  });
152
+
153
+ it("answers a variable the config does not carry with the default the driver declares", async () => {
154
+ const config = configWith({});
155
+
156
+ await write(config);
157
+
158
+ const live = createBridgeLiveConfig(path, config, 0, {
159
+ WINDOWS_STEAM: "true",
160
+ });
161
+
162
+ expect(live.variable("WINDOWS_STEAM")).toBe("true");
163
+ expect(live.variables()).toEqual({
164
+ WINDOWS_STEAM: "true",
165
+ VERSION: "1.21",
166
+ });
167
+ });
168
+
169
+ it("answers the value the owner set over the default, and the default again once the value is gone", async () => {
170
+ const config = configWith(
171
+ {},
172
+ {
173
+ WINDOWS_STEAM: "false",
174
+ },
175
+ );
176
+
177
+ await write(config);
178
+
179
+ const live = createBridgeLiveConfig(path, config, 0, {
180
+ WINDOWS_STEAM: "true",
181
+ });
182
+
183
+ expect(live.variable("WINDOWS_STEAM")).toBe("false");
184
+
185
+ await write(configWith({}));
186
+
187
+ expect(live.variable("WINDOWS_STEAM")).toBe("true");
188
+ });
189
+
190
+ it("answers the default for a variable the owner cleared, as the form shows it", async () => {
191
+ const config = configWith(
192
+ {},
193
+ {
194
+ SERVER_NAME: "",
195
+ },
196
+ );
197
+
198
+ await write(config);
199
+
200
+ const live = createBridgeLiveConfig(path, config, 0, {
201
+ SERVER_NAME: "Serverk",
202
+ });
203
+
204
+ expect(live.variable("SERVER_NAME")).toBe("Serverk");
205
+ });
206
+ });
207
+
208
+ describe("variableDefaultsOf", () => {
209
+ it("collects the default of every form field that declares one", () => {
210
+ const driver: BridgeDriver = {
211
+ install: {
212
+ kind: BridgeKind.Install,
213
+ run: async () => {
214
+ return;
215
+ },
216
+ },
217
+ lifecycle: {
218
+ kind: BridgeKind.Lifecycle,
219
+ command: async () => [
220
+ "run",
221
+ ],
222
+ },
223
+ panel: {
224
+ tabs: [
225
+ {
226
+ id: "server",
227
+ title: {
228
+ ar: "السيرفر",
229
+ en: "Server",
230
+ },
231
+ sections: [
232
+ {
233
+ layout: BridgeLayout.Form,
234
+ id: "steam",
235
+ target: BridgeFormTarget.Variables,
236
+ fields: [
237
+ {
238
+ key: "WINDOWS_STEAM",
239
+ control: BridgeControl.Boolean,
240
+ label: {
241
+ ar: "تفعيل Steam",
242
+ en: "Use Steam",
243
+ },
244
+ default: "true",
245
+ },
246
+ {
247
+ key: "SERVER_NAME",
248
+ control: BridgeControl.Text,
249
+ label: {
250
+ ar: "الاسم",
251
+ en: "Name",
252
+ },
253
+ },
254
+ ],
255
+ },
256
+ ],
257
+ },
258
+ ],
259
+ },
260
+ };
261
+
262
+ expect(variableDefaultsOf(driver)).toEqual({
263
+ WINDOWS_STEAM: "true",
264
+ });
265
+ });
266
+
267
+ it("answers no defaults for a driver without a panel", () => {
268
+ expect(
269
+ variableDefaultsOf({
270
+ install: {
271
+ kind: BridgeKind.Install,
272
+ run: async () => {
273
+ return;
274
+ },
275
+ },
276
+ lifecycle: {
277
+ kind: BridgeKind.Lifecycle,
278
+ command: async () => [
279
+ "run",
280
+ ],
281
+ },
282
+ }),
283
+ ).toEqual({});
284
+ });
149
285
  });
@@ -1,4 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import type { BridgeDriver } from "../modules";
3
+ import { BridgeLayout } from "../panel";
2
4
  import type { BridgeConfig } from "../protocol";
3
5
  import { parseConfig } from "./bridgeConfigReader";
4
6
 
@@ -20,15 +22,54 @@ export interface BridgeLiveConfig {
20
22
  memoryMb: () => number;
21
23
  cpuPercent: () => number;
22
24
  publicAddress: () => string | null;
25
+ playerAddress: () => string | null;
23
26
  reload: () => void;
24
27
  }
25
28
 
29
+ export const variableDefaultsOf = (driver: BridgeDriver): Record<string, string> => {
30
+ const defaults: Record<string, string> = {};
31
+
32
+ for (const tab of driver.panel?.tabs ?? []) {
33
+ for (const section of tab.sections) {
34
+ if (section.layout !== BridgeLayout.Form) {
35
+ continue;
36
+ }
37
+
38
+ for (const field of section.fields) {
39
+ if (field.default !== undefined) {
40
+ defaults[field.key] = field.default;
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ return defaults;
47
+ };
48
+
26
49
  export const createBridgeLiveConfig = (
27
50
  path: string,
28
51
  config: BridgeConfig,
29
52
  refreshMs: number = BRIDGE_CONFIG_REFRESH_MS,
53
+ defaults: Readonly<Record<string, string>> = {},
30
54
  ): BridgeLiveConfig => {
31
- let current = config;
55
+ const withDefaults = (read: BridgeConfig): BridgeConfig => {
56
+ const variables = {
57
+ ...read.variables,
58
+ };
59
+
60
+ for (const [key, value] of Object.entries(defaults)) {
61
+ if (!variables[key]) {
62
+ variables[key] = value;
63
+ }
64
+ }
65
+
66
+ return {
67
+ ...read,
68
+ variables,
69
+ };
70
+ };
71
+
72
+ let current = withDefaults(config);
32
73
 
33
74
  let checkedAt: number | null = null;
34
75
 
@@ -42,7 +83,7 @@ export const createBridgeLiveConfig = (
42
83
  checkedAt = stamp;
43
84
 
44
85
  try {
45
- current = parseConfig(readFileSync(path, "utf8"));
86
+ current = withDefaults(parseConfig(readFileSync(path, "utf8")));
46
87
  } catch {
47
88
  return current;
48
89
  }
@@ -72,6 +113,9 @@ export const createBridgeLiveConfig = (
72
113
  publicAddress() {
73
114
  return refresh().publicAddress ?? null;
74
115
  },
116
+ playerAddress() {
117
+ return refresh().playerAddress ?? null;
118
+ },
75
119
  reload() {
76
120
  checkedAt = null;
77
121
 
@@ -14,6 +14,9 @@ import { BridgeControl, BridgeFormTarget, BridgeLayout } from "../panel";
14
14
  import {
15
15
  BRIDGE_ACTIVITY_KEYS,
16
16
  BRIDGE_TEXT_LENGTH_LIMIT,
17
+ type BridgeCatalogEntry,
18
+ type BridgeCatalogPage,
19
+ BridgeCatalogSourceKind,
17
20
  type BridgeConfig,
18
21
  BridgeConfirm,
19
22
  BridgeEventName,
@@ -742,6 +745,223 @@ describe("detail modules", () => {
742
745
  });
743
746
  });
744
747
 
748
+ describe("catalog modules", () => {
749
+ const entry = (id: string): BridgeCatalogEntry => ({
750
+ id,
751
+ provider: null,
752
+ path: `Mods/${id}`,
753
+ title: id,
754
+ version: null,
755
+ sizeBytes: 0,
756
+ enabled: true,
757
+ gameVersion: null,
758
+ stale: false,
759
+ pageUrl: null,
760
+ icon: null,
761
+ });
762
+
763
+ const emptyPage = async (): Promise<BridgeCatalogPage> => ({
764
+ hits: [],
765
+ total: 0,
766
+ providers: [],
767
+ categories: [],
768
+ sorts: [],
769
+ });
770
+
771
+ const WORKSHOP = {
772
+ kind: BridgeCatalogSourceKind.Workshop,
773
+ appId: 1_623_730,
774
+ provided: [
775
+ "UE4SS",
776
+ "PalSchema",
777
+ ],
778
+ };
779
+
780
+ const driver = (added: string[] = []): BridgeDriver => ({
781
+ ...baseDriver(),
782
+ modules: {
783
+ mods: {
784
+ kind: BridgeKind.Catalog,
785
+ source: WORKSHOP,
786
+ installed: async () => [],
787
+ install: async (_context, id) => entry(id),
788
+ add: async (_context, input) => {
789
+ added.push(input);
790
+
791
+ return entry("upload:BetterMap");
792
+ },
793
+ remove: async () => {
794
+ return;
795
+ },
796
+ toggle: async () => {
797
+ return;
798
+ },
799
+ },
800
+ addons: {
801
+ kind: BridgeKind.Catalog,
802
+ search: emptyPage,
803
+ releases: async () => [],
804
+ preview: async () => ({
805
+ confirm: BridgeConfirm.Normal,
806
+ lines: [],
807
+ }),
808
+ installed: async () => [],
809
+ install: async (_context, id) => entry(id),
810
+ remove: async () => {
811
+ return;
812
+ },
813
+ },
814
+ },
815
+ });
816
+
817
+ it("advertises search and preview for a sourced catalog, never releases, and carries its source", () => {
818
+ expect(registryFor(driver()).manifest().modules).toEqual([
819
+ {
820
+ id: "mods",
821
+ kind: BridgeKind.Catalog,
822
+ requiresRunning: false,
823
+ refreshSeconds: null,
824
+ pageSize: null,
825
+ dependsOn: [],
826
+ actions: [
827
+ BridgeAction.Preview,
828
+ BridgeAction.Install,
829
+ BridgeAction.Installed,
830
+ BridgeAction.Remove,
831
+ BridgeAction.Search,
832
+ BridgeAction.Add,
833
+ BridgeAction.Toggle,
834
+ ],
835
+ source: WORKSHOP,
836
+ },
837
+ {
838
+ id: "addons",
839
+ kind: BridgeKind.Catalog,
840
+ requiresRunning: false,
841
+ refreshSeconds: null,
842
+ pageSize: null,
843
+ dependsOn: [],
844
+ actions: [
845
+ BridgeAction.Releases,
846
+ BridgeAction.Preview,
847
+ BridgeAction.Install,
848
+ BridgeAction.Installed,
849
+ BridgeAction.Remove,
850
+ BridgeAction.Search,
851
+ ],
852
+ },
853
+ ]);
854
+ });
855
+
856
+ it("hands add the staged input and answers the entry it installed", async () => {
857
+ const added: string[] = [];
858
+ const { result } = await registryFor(driver(added)).invoke("mods", BridgeAction.Add, {
859
+ input: ".serverk-staging/mods-upload/BetterMap",
860
+ });
861
+
862
+ expect(added).toEqual([
863
+ ".serverk-staging/mods-upload/BetterMap",
864
+ ]);
865
+ expect(result).toEqual(entry("upload:BetterMap"));
866
+ });
867
+
868
+ it("refuses an add with no staged input before the driver is entered", async () => {
869
+ const added: string[] = [];
870
+
871
+ await expect(registryFor(driver(added)).invoke("mods", BridgeAction.Add, {})).rejects.toThrow(
872
+ '"input" must be a non-empty string',
873
+ );
874
+ expect(added).toEqual([]);
875
+ });
876
+
877
+ it("refuses an add on a catalog that defines none", async () => {
878
+ await expect(
879
+ registryFor(driver()).invoke("addons", BridgeAction.Add, {
880
+ input: ".serverk-staging/mods-upload/BetterMap",
881
+ }),
882
+ ).rejects.toThrow('module "addons" does not support "add"');
883
+ });
884
+
885
+ it("refuses search, preview and releases on a sourced catalog, which the platform answers", async () => {
886
+ const registry = registryFor(driver());
887
+
888
+ for (const action of [
889
+ BridgeAction.Search,
890
+ BridgeAction.Preview,
891
+ BridgeAction.Releases,
892
+ ]) {
893
+ const refused = registry.invoke("mods", action, {
894
+ id: "3762839811",
895
+ });
896
+
897
+ await expect(refused).rejects.toBeInstanceOf(BridgeRefusalError);
898
+ await expect(refused).rejects.toThrow(
899
+ `module "mods" takes its items from the workshop source, so the platform answers "${action}"`,
900
+ );
901
+ }
902
+ });
903
+
904
+ it("hands back the note an entry carries, unchanged through the wire's JSON", async () => {
905
+ const noted: BridgeCatalogEntry = {
906
+ ...entry("3762839811"),
907
+ note: {
908
+ ar: "يشتغل بعد إعادة التشغيل",
909
+ en: "Runs after a restart",
910
+ },
911
+ };
912
+
913
+ const registry = registryFor({
914
+ ...baseDriver(),
915
+ modules: {
916
+ mods: {
917
+ kind: BridgeKind.Catalog,
918
+ source: WORKSHOP,
919
+ installed: async () => [
920
+ noted,
921
+ ],
922
+ install: async () => noted,
923
+ remove: async () => {
924
+ return;
925
+ },
926
+ },
927
+ },
928
+ });
929
+
930
+ const { result: installed } = await registry.invoke("mods", BridgeAction.Installed);
931
+ const { result: install } = await registry.invoke("mods", BridgeAction.Install, {
932
+ id: "3762839811",
933
+ });
934
+
935
+ expect(JSON.parse(JSON.stringify(installed))).toEqual([
936
+ noted,
937
+ ]);
938
+ expect(JSON.parse(JSON.stringify(install))).toEqual(noted);
939
+ });
940
+
941
+ it("still answers search, releases and preview on a catalog that answers its own search", async () => {
942
+ const registry = registryFor(driver());
943
+
944
+ expect((await registry.invoke("addons", BridgeAction.Search)).result).toEqual(await emptyPage());
945
+ expect(
946
+ (
947
+ await registry.invoke("addons", BridgeAction.Releases, {
948
+ id: "sodium",
949
+ })
950
+ ).result,
951
+ ).toEqual([]);
952
+ expect(
953
+ (
954
+ await registry.invoke("addons", BridgeAction.Preview, {
955
+ id: "sodium",
956
+ })
957
+ ).result,
958
+ ).toEqual({
959
+ confirm: BridgeConfirm.Normal,
960
+ lines: [],
961
+ });
962
+ });
963
+ });
964
+
745
965
  describe("the setup singleton", () => {
746
966
  it("rides the manifest as its own block and never joins the panel modules", () => {
747
967
  const steps = [
@@ -97,6 +97,16 @@ const pageSizeOf = (module: BridgePanelModule) => {
97
97
  return module.kind === BridgeKind.Catalog ? (module.pageSize ?? null) : null;
98
98
  };
99
99
 
100
+ const sourceOf = (module: BridgePanelModule) => {
101
+ return module.kind === BridgeKind.Catalog ? (module.source ?? null) : null;
102
+ };
103
+
104
+ const SOURCED_CATALOG_ACTIONS: readonly string[] = [
105
+ BridgeAction.Preview,
106
+ BridgeAction.Releases,
107
+ BridgeAction.Search,
108
+ ];
109
+
100
110
  const terminalManifestOf = (module: BridgeTerminalModule | undefined): BridgeTerminalManifest | null => {
101
111
  if (!module) {
102
112
  return null;
@@ -750,13 +760,19 @@ export const createRegistry = (
750
760
  }
751
761
 
752
762
  case BridgeKind.Catalog: {
763
+ if (module.source && SOURCED_CATALOG_ACTIONS.includes(action)) {
764
+ throw new BridgeRefusalError(
765
+ `module "${name}" takes its items from the ${module.source.kind} source, so the platform answers "${action}"`,
766
+ );
767
+ }
768
+
753
769
  if (action === BridgeAction.Releases && module.releases) {
754
770
  return await module.releases(context, asString(args, "id"));
755
771
  }
756
772
  if (action === BridgeAction.Preview && module.preview) {
757
773
  return await module.preview(context, asString(args, "id"), asOptional(args, "releaseId") ?? undefined);
758
774
  }
759
- if (action === BridgeAction.Search) {
775
+ if (action === BridgeAction.Search && module.search) {
760
776
  return await module.search(context, {
761
777
  query: asText(args, "query"),
762
778
  page: asPage(args),
@@ -774,6 +790,10 @@ export const createRegistry = (
774
790
  return await module.install(context, asString(args, "id"), asOptional(args, "releaseId") ?? undefined);
775
791
  }
776
792
 
793
+ if (action === BridgeAction.Add && module.add) {
794
+ return await module.add(context, asString(args, "input"));
795
+ }
796
+
777
797
  if (action === BridgeAction.Remove) {
778
798
  const id = asString(args, "id");
779
799
 
@@ -803,6 +823,7 @@ export const createRegistry = (
803
823
  manifest(): BridgeManifest {
804
824
  const entries: BridgeModuleManifest[] = Object.entries(modules).map(([id, module]) => {
805
825
  const protectedActions = protectedActionsOf(module);
826
+ const source = sourceOf(module);
806
827
  return {
807
828
  id,
808
829
  kind: module.kind,
@@ -816,6 +837,11 @@ export const createRegistry = (
816
837
  protectedActions,
817
838
  }
818
839
  : {}),
840
+ ...(source
841
+ ? {
842
+ source,
843
+ }
844
+ : {}),
819
845
  };
820
846
  });
821
847
 
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { idleSample, sampleResultOf } from "./bridgeSample";
3
+
4
+ describe("sampleResultOf", () => {
5
+ it("serves no tick for an older driver that reports only its players", () => {
6
+ expect(
7
+ sampleResultOf({
8
+ online: 3,
9
+ max: 16,
10
+ }),
11
+ ).toEqual({
12
+ online: 3,
13
+ max: 16,
14
+ tick: null,
15
+ });
16
+ });
17
+
18
+ it("carries the milliseconds per tick a driver measured", () => {
19
+ expect(
20
+ sampleResultOf({
21
+ online: 3,
22
+ max: 16,
23
+ tick: 21.4,
24
+ }).tick,
25
+ ).toBe(21.4);
26
+ });
27
+
28
+ it("keeps a tick a driver could not measure as null", () => {
29
+ expect(
30
+ sampleResultOf({
31
+ online: null,
32
+ max: null,
33
+ tick: null,
34
+ }).tick,
35
+ ).toBeNull();
36
+ });
37
+
38
+ for (const tick of [
39
+ -1,
40
+ Number.NaN,
41
+ Number.POSITIVE_INFINITY,
42
+ ]) {
43
+ it(`drops a tick of ${tick}, which no game measures`, () => {
44
+ expect(
45
+ sampleResultOf({
46
+ online: 3,
47
+ max: 16,
48
+ tick,
49
+ }).tick,
50
+ ).toBeNull();
51
+ });
52
+ }
53
+ });
54
+
55
+ describe("idleSample", () => {
56
+ it("reports no players and no tick while the game is not running", () => {
57
+ expect(idleSample()).toEqual({
58
+ online: null,
59
+ max: null,
60
+ tick: null,
61
+ });
62
+ });
63
+ });
@@ -0,0 +1,22 @@
1
+ import type { BridgeSample } from "../modules";
2
+ import type { BridgeSampleResult } from "../protocol";
3
+
4
+ export const idleSample = (): BridgeSampleResult => {
5
+ return {
6
+ online: null,
7
+ max: null,
8
+ tick: null,
9
+ };
10
+ };
11
+
12
+ const tickOf = (tick: number | null | undefined): number | null => {
13
+ return typeof tick === "number" && Number.isFinite(tick) && tick >= 0 ? tick : null;
14
+ };
15
+
16
+ export const sampleResultOf = (sample: BridgeSample): BridgeSampleResult => {
17
+ return {
18
+ online: sample.online,
19
+ max: sample.max,
20
+ tick: tickOf(sample.tick),
21
+ };
22
+ };
@@ -18,7 +18,7 @@ import { createContext } from "./bridgeContext";
18
18
  import { validateDriver } from "./bridgeDriver";
19
19
  import { createEventRing } from "./bridgeEvents";
20
20
  import { decideInstall, recountHold } from "./bridgeInstall";
21
- import { createBridgeLiveConfig } from "./bridgeLiveConfig";
21
+ import { BRIDGE_CONFIG_REFRESH_MS, createBridgeLiveConfig, variableDefaultsOf } from "./bridgeLiveConfig";
22
22
  import { errorDetail, errorText, print, log as stdoutLog } from "./bridgeLog";
23
23
  import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
24
24
  import {
@@ -29,6 +29,7 @@ import {
29
29
  secretFieldKeys,
30
30
  } from "./bridgeRedaction";
31
31
  import { createRegistry } from "./bridgeRegistry";
32
+ import { idleSample, sampleResultOf } from "./bridgeSample";
32
33
  import { createServer } from "./bridgeServer";
33
34
  import { setupSummaryOf } from "./bridgeSetup";
34
35
  import { createSetupHandlers } from "./bridgeSetupHandlers";
@@ -90,7 +91,7 @@ export const serve = async (driver: BridgeDriver) => {
90
91
 
91
92
  const desired = await readDesired(statePath(config));
92
93
 
93
- const live = createBridgeLiveConfig(configPath(), config);
94
+ const live = createBridgeLiveConfig(configPath(), config, BRIDGE_CONFIG_REFRESH_MS, variableDefaultsOf(driver));
94
95
 
95
96
  const redact = createBridgeRedaction(secretFieldKeys(driver), live);
96
97
 
@@ -266,10 +267,7 @@ export const serve = async (driver: BridgeDriver) => {
266
267
  }
267
268
 
268
269
  if (!supervisor.running) {
269
- return {
270
- online: null,
271
- max: null,
272
- };
270
+ return idleSample();
273
271
  }
274
272
 
275
273
  const stamp = performance.now();
@@ -278,12 +276,7 @@ export const serve = async (driver: BridgeDriver) => {
278
276
  return sampled.value;
279
277
  }
280
278
 
281
- const result = await query.sample(context);
282
-
283
- const value: BridgeSampleResult = {
284
- online: result.online,
285
- max: result.max,
286
- };
279
+ const value = sampleResultOf(await query.sample(context));
287
280
 
288
281
  sampled = {
289
282
  value,
@@ -1,5 +1,11 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import { workshopContentPath, workshopDownloadArgs, workshopFormBody } from "./workshop";
2
+ import {
3
+ workshopArchivePath,
4
+ workshopContentPath,
5
+ workshopDownloadArgs,
6
+ workshopFormBody,
7
+ workshopItemPageUrl,
8
+ } from "./workshop";
3
9
 
4
10
  describe("workshopFormBody", () => {
5
11
  it("writes the count and the indexed ids", () => {
@@ -43,3 +49,15 @@ describe("workshopContentPath", () => {
43
49
  expect(workshopContentPath("440900", "12345")).toBe("steamapps/workshop/content/440900/12345");
44
50
  });
45
51
  });
52
+
53
+ describe("workshopArchivePath", () => {
54
+ it("names the archive the platform pushes for one item", () => {
55
+ expect(workshopArchivePath(1_623_730, "3762839811")).toBe(".serverk/snapshots/workshop-1623730-3762839811.zip");
56
+ });
57
+ });
58
+
59
+ describe("workshopItemPageUrl", () => {
60
+ it("links the item's steam community page", () => {
61
+ expect(workshopItemPageUrl("3762839811")).toBe("https://steamcommunity.com/sharedfiles/filedetails/?id=3762839811");
62
+ });
63
+ });