@serverkgg/bridge 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/package.json +3 -2
  2. package/rules/package.md +60 -0
  3. package/src/blog/articleFrontmatter.test.ts +10 -10
  4. package/src/blog/compile.test.ts +9 -9
  5. package/src/bot/botDeploy.test.ts +16 -16
  6. package/src/bot/botEnvironment.test.ts +8 -8
  7. package/src/bot/botIdentity.test.ts +12 -12
  8. package/src/bot/botPanel.test.ts +41 -11
  9. package/src/bot/botPanel.ts +13 -5
  10. package/src/bot/botState.test.ts +3 -3
  11. package/src/bot/botTerminal.test.ts +5 -5
  12. package/src/bot/botVariables.test.ts +8 -8
  13. package/src/bot/detect/botToken.test.ts +6 -6
  14. package/src/bot/detect/detect.test.ts +21 -21
  15. package/src/bot/detect/detectTable.test.ts +5 -5
  16. package/src/bot/detect/envExample.test.ts +2 -2
  17. package/src/bot/detect/envScan.test.ts +4 -4
  18. package/src/bot/detect/inferBotPlatform.test.ts +4 -4
  19. package/src/bot/detect/keys.test.ts +3 -3
  20. package/src/bot/detect/sensitiveKey.test.ts +5 -5
  21. package/src/bot/detect/serverkJson.test.ts +5 -5
  22. package/src/catalogs/curseforge.test.ts +35 -35
  23. package/src/catalogs/thunderstore.test.ts +9 -9
  24. package/src/cli/detectTable.test.ts +3 -3
  25. package/src/cli/link.test.ts +11 -11
  26. package/src/cli/main.ts +8 -0
  27. package/src/cli/rules.test.ts +198 -0
  28. package/src/cli/rules.ts +188 -0
  29. package/src/cli/validate.test.ts +123 -29
  30. package/src/cli/validate.ts +37 -1
  31. package/src/codecs/ini.test.ts +13 -13
  32. package/src/codecs/javaProperties.test.ts +11 -11
  33. package/src/codecs/sourceCfg.test.ts +14 -14
  34. package/src/codecs/xmlProperties.test.ts +10 -10
  35. package/src/context/bridgeContext.ts +6 -1
  36. package/src/core/bridgeAction.ts +2 -0
  37. package/src/core/bridgeActivity.ts +9 -0
  38. package/src/core/bridgeAvailability.ts +10 -0
  39. package/src/core/bridgePort.test.ts +6 -6
  40. package/src/core/bridgeUserError.test.ts +13 -3
  41. package/src/core/bridgeUserError.ts +6 -2
  42. package/src/core/bridgeValue.test.ts +5 -5
  43. package/src/core/describeDownloadFailure.test.ts +58 -0
  44. package/src/core/describeDownloadFailure.ts +69 -0
  45. package/src/core/index.ts +3 -0
  46. package/src/guides/markdown.test.ts +31 -31
  47. package/src/install/installStamp.test.ts +9 -9
  48. package/src/manifest/extension.ts +36 -0
  49. package/src/manifest/index.ts +1 -0
  50. package/src/manifest/manifest.test.ts +208 -90
  51. package/src/manifest/manifest.ts +34 -0
  52. package/src/modules/bridge.ts +8 -1
  53. package/src/modules/bridgeModule.ts +13 -1
  54. package/src/net/loopback.test.ts +10 -10
  55. package/src/panel/bridgeDetail.ts +2 -1
  56. package/src/presence/rosterSync.test.ts +7 -7
  57. package/src/probes/a2s.test.ts +5 -5
  58. package/src/probes/minecraftPing.test.ts +17 -17
  59. package/src/probes/raknetPing.test.ts +13 -13
  60. package/src/protocol/bridgeActivity.ts +54 -0
  61. package/src/protocol/bridgeEvents.ts +30 -0
  62. package/src/protocol/bridgeFrame.ts +10 -1
  63. package/src/protocol/index.ts +1 -0
  64. package/src/rcon/rconAccess.test.ts +20 -20
  65. package/src/rcon/rconAccess.ts +13 -13
  66. package/src/rcon/rconClient.test.ts +49 -49
  67. package/src/rcon/rconFrame.test.ts +14 -14
  68. package/src/rcon/rconIds.test.ts +4 -4
  69. package/src/rcon/rconWriter.test.ts +6 -6
  70. package/src/runtime/bridgeActions.ts +19 -2
  71. package/src/runtime/bridgeActivity.test.ts +318 -0
  72. package/src/runtime/bridgeActivity.ts +268 -0
  73. package/src/runtime/bridgeBackupLease.test.ts +9 -9
  74. package/src/runtime/bridgeConfigReader.test.ts +5 -5
  75. package/src/runtime/bridgeConsoleHandler.test.ts +8 -8
  76. package/src/runtime/bridgeContext.test.ts +11 -5
  77. package/src/runtime/bridgeContext.ts +8 -1
  78. package/src/runtime/bridgeDriver.test.ts +114 -93
  79. package/src/runtime/bridgeDriver.ts +10 -7
  80. package/src/runtime/bridgeEvents.test.ts +15 -15
  81. package/src/runtime/bridgeFiles.test.ts +54 -54
  82. package/src/runtime/bridgeInstall.test.ts +10 -10
  83. package/src/runtime/bridgeLiveConfig.test.ts +6 -6
  84. package/src/runtime/bridgeLog.ts +10 -0
  85. package/src/runtime/bridgePending.test.ts +6 -6
  86. package/src/runtime/bridgeRedaction.test.ts +16 -16
  87. package/src/runtime/bridgeRefusalError.ts +13 -0
  88. package/src/runtime/bridgeRegistry.test.ts +1077 -34
  89. package/src/runtime/bridgeRegistry.ts +464 -65
  90. package/src/runtime/bridgeScopedContext.ts +55 -0
  91. package/src/runtime/bridgeServer.test.ts +424 -24
  92. package/src/runtime/bridgeServer.ts +125 -35
  93. package/src/runtime/bridgeSetup.test.ts +27 -27
  94. package/src/runtime/bridgeSetup.ts +1 -1
  95. package/src/runtime/bridgeSetupHandlers.test.ts +80 -17
  96. package/src/runtime/bridgeSetupHandlers.ts +45 -10
  97. package/src/runtime/bridgeSignal.test.ts +7 -7
  98. package/src/runtime/bridgeSupervisor.test.ts +42 -42
  99. package/src/runtime/serve.ts +45 -3
  100. package/src/steam/avatars.test.ts +13 -13
  101. package/src/steam/steamApp.test.ts +8 -8
  102. package/src/steam/steamcmd.test.ts +44 -13
  103. package/src/steam/steamcmd.ts +27 -4
  104. package/src/steam/steamcmdNarrator.test.ts +4 -4
  105. package/src/steam/workshop.test.ts +5 -5
  106. package/src/utils/changedSettingKeys.test.ts +65 -0
  107. package/src/utils/changedSettingKeys.ts +6 -0
  108. package/src/utils/describeExecFailure.test.ts +34 -0
  109. package/src/utils/describeExecFailure.ts +9 -0
  110. package/src/utils/index.ts +3 -0
  111. package/src/utils/sameSettingValue.test.ts +35 -0
  112. package/src/utils/sameSettingValue.ts +40 -0
  113. package/src/utils/token.test.ts +7 -7
  114. package/src/wine/protonCommand.test.ts +5 -5
  115. package/src/wine/wineCommand.test.ts +6 -6
  116. package/src/wine/winePrefix.test.ts +6 -6
  117. package/src/wire/activity.test.ts +95 -0
  118. package/src/wire/activity.ts +41 -0
  119. package/src/wire/index.ts +1 -0
  120. package/src/wire/panel.test.ts +91 -25
  121. package/src/wire/panel.ts +11 -0
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
1
+ import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
@@ -16,7 +16,8 @@ import {
16
16
  type BridgeTab,
17
17
  type BridgeTableSection,
18
18
  } from "../panel";
19
- import { placementReportLines, runValidate, setupReportLines } from "./validate";
19
+ import { RULES_FILE, readShippedRules, writeRules } from "./rules";
20
+ import { availabilityReportLines, placementReportLines, runValidate, setupReportLines } from "./validate";
20
21
 
21
22
  const text = (value: string) => ({
22
23
  ar: value,
@@ -54,7 +55,7 @@ describe("validate", () => {
54
55
  });
55
56
  });
56
57
 
57
- test("refuses a file: bridge dependency", async () => {
58
+ it("refuses a file: bridge dependency", async () => {
58
59
  writePackage({
59
60
  "@serverkgg/bridge": "file:../serverk/modules/bridge",
60
61
  });
@@ -64,7 +65,7 @@ describe("validate", () => {
64
65
  );
65
66
  });
66
67
 
67
- test("refuses a link: bridge dependency", async () => {
68
+ it("refuses a link: bridge dependency", async () => {
68
69
  writePackage({
69
70
  "@serverkgg/bridge": "link:@serverkgg/bridge",
70
71
  });
@@ -72,7 +73,7 @@ describe("validate", () => {
72
73
  await expect(validate()).rejects.toThrow('depends on @serverkgg/bridge via "link:@serverkgg/bridge"');
73
74
  });
74
75
 
75
- test("refuses a file: bridge devDependency", async () => {
76
+ it("refuses a file: bridge devDependency", async () => {
76
77
  writeFileSync(
77
78
  join(root, "package.json"),
78
79
  JSON.stringify({
@@ -86,7 +87,7 @@ describe("validate", () => {
86
87
  await expect(validate()).rejects.toThrow("pin a published version");
87
88
  });
88
89
 
89
- test("accepts a published bridge dependency and moves on to the manifest", async () => {
90
+ it("accepts a published bridge dependency and moves on to the manifest", async () => {
90
91
  writePackage({
91
92
  "@serverkgg/bridge": "0.0.3",
92
93
  });
@@ -94,7 +95,7 @@ describe("validate", () => {
94
95
  await expect(validate()).rejects.toThrow("missing serverk.yml");
95
96
  });
96
97
 
97
- test("ignores a package without a bridge dependency", async () => {
98
+ it("ignores a package without a bridge dependency", async () => {
98
99
  writePackage({
99
100
  zod: "4.4.3",
100
101
  });
@@ -102,19 +103,19 @@ describe("validate", () => {
102
103
  await expect(validate()).rejects.toThrow("missing serverk.yml");
103
104
  });
104
105
 
105
- test("ignores a package.json that is absent", async () => {
106
+ it("ignores a package.json that is absent", async () => {
106
107
  await expect(validate()).rejects.toThrow("missing serverk.yml");
107
108
  });
108
109
  });
109
110
 
110
111
  describe("the setup report", () => {
111
- test("says none for a driver that declares no setup", () => {
112
+ it("says none for a driver that declares no setup", () => {
112
113
  expect(setupReportLines(undefined)).toEqual([
113
114
  " setup: none",
114
115
  ]);
115
116
  });
116
117
 
117
- test("names every step, its kind, whether it is required, and where it lands", () => {
118
+ it("names every step, its kind, whether it is required, and where it lands", () => {
118
119
  const setup: BridgeSetupModule = {
119
120
  kind: BridgeKind.Setup,
120
121
  steps: [
@@ -138,8 +139,8 @@ describe("the setup report", () => {
138
139
  },
139
140
  {
140
141
  kind: BridgeSetupStepKind.Open,
141
- id: "txadmin",
142
- title: text("txAdmin"),
142
+ id: "dashboard",
143
+ title: text("Dashboard"),
143
144
  required: false,
144
145
  target: {
145
146
  tab: GuideOpenTab.Panel,
@@ -165,12 +166,12 @@ describe("the setup report", () => {
165
166
  " setup: 4 step(s)",
166
167
  " license — form, required → license/license [LICENSE_KEY]",
167
168
  " verify — driver, required → driver (needs the game running)",
168
- ' txadmin — open, optional → panel tab "license"',
169
+ ' dashboard — open, optional → panel tab "license"',
169
170
  " invite — open, optional → access",
170
171
  ]);
171
172
  });
172
173
 
173
- test("leaves a driver step that needs nothing running with no target", () => {
174
+ it("leaves a driver step that needs nothing running with no target", () => {
174
175
  expect(
175
176
  setupReportLines({
176
177
  kind: BridgeKind.Setup,
@@ -192,7 +193,7 @@ describe("the setup report", () => {
192
193
  ]);
193
194
  });
194
195
 
195
- test("reads an omitted required as required, the way the platform does", () => {
196
+ it("reads an omitted required as required, the way the platform does", () => {
196
197
  expect(
197
198
  setupReportLines({
198
199
  kind: BridgeKind.Setup,
@@ -259,7 +260,7 @@ describe("the placement report", () => {
259
260
  },
260
261
  });
261
262
 
262
- test("says nothing about a roster table that declares where it belongs", () => {
263
+ it("says nothing about a roster table that declares where it belongs", () => {
263
264
  expect(
264
265
  placementReportLines(
265
266
  driverWithTabs([
@@ -277,7 +278,7 @@ describe("the placement report", () => {
277
278
  ]);
278
279
  });
279
280
 
280
- test("warns that the platform absorbs an unplaced players collection by convention", () => {
281
+ it("warns that the platform absorbs an unplaced players collection by convention", () => {
281
282
  expect(
282
283
  placementReportLines(
283
284
  driverWithTabs([
@@ -295,7 +296,7 @@ describe("the placement report", () => {
295
296
  ]);
296
297
  });
297
298
 
298
- test("warns about a tab that takes the name of a platform sidebar entry", () => {
299
+ it("warns about a tab that takes the name of a platform sidebar entry", () => {
299
300
  expect(
300
301
  placementReportLines(
301
302
  driverWithTabs([
@@ -313,7 +314,7 @@ describe("the placement report", () => {
313
314
  ]);
314
315
  });
315
316
 
316
- test("leaves a players tab alone while it is the roster tab", () => {
317
+ it("leaves a players tab alone while it is the roster tab", () => {
317
318
  expect(
318
319
  placementReportLines(
319
320
  driverWithTabs([
@@ -331,7 +332,7 @@ describe("the placement report", () => {
331
332
  ]);
332
333
  });
333
334
 
334
- test("warns about a players tab that holds no roster", () => {
335
+ it("warns about a players tab that holds no roster", () => {
335
336
  expect(
336
337
  placementReportLines(
337
338
  driverWithTabs([
@@ -349,7 +350,7 @@ describe("the placement report", () => {
349
350
  ]);
350
351
  });
351
352
 
352
- test("says nothing about a tab that declares no sections at all", () => {
353
+ it("says nothing about a tab that declares no sections at all", () => {
353
354
  expect(
354
355
  placementReportLines(
355
356
  driverWithTabs([
@@ -363,7 +364,7 @@ describe("the placement report", () => {
363
364
  ).toEqual([]);
364
365
  });
365
366
 
366
- test("still warns about an empty players tab, which holds no roster", () => {
367
+ it("still warns about an empty players tab, which holds no roster", () => {
367
368
  expect(
368
369
  placementReportLines(
369
370
  driverWithTabs([
@@ -379,7 +380,7 @@ describe("the placement report", () => {
379
380
  ]);
380
381
  });
381
382
 
382
- test("says nothing about a tab that keeps a leftover section", () => {
383
+ it("says nothing about a tab that keeps a leftover section", () => {
383
384
  expect(
384
385
  placementReportLines(
385
386
  driverWithTabs([
@@ -493,6 +494,7 @@ describe("validating a bot package", () => {
493
494
  writeFileSync(join(root, "assets/logo.webp"), webp(256, 256));
494
495
  writeFileSync(join(root, "assets/banner.webp"), webp(1280, 720));
495
496
  writeFileSync(join(root, "src/driver.ts"), BOT_DRIVER);
497
+ writeRules(root, readShippedRules());
496
498
  };
497
499
 
498
500
  const validate = () => {
@@ -521,7 +523,7 @@ describe("validating a bot package", () => {
521
523
  });
522
524
  });
523
525
 
524
- test("validates a bot with no ports and says which kind it read", async () => {
526
+ it("validates a bot with no ports and says which kind it read", async () => {
525
527
  writeBot(botManifest());
526
528
 
527
529
  await validate();
@@ -535,7 +537,27 @@ describe("validating a bot package", () => {
535
537
  expect(lines.some((line) => line.includes("warning"))).toBe(false);
536
538
  });
537
539
 
538
- test("says when a bot turns the environment vault on", async () => {
540
+ it("warns about a missing rule file and still validates", async () => {
541
+ writeBot(botManifest());
542
+ rmSync(join(root, RULES_FILE));
543
+
544
+ await validate();
545
+
546
+ expect(lines).toContain("bot-node is valid");
547
+ expect(lines).toContain(` warning: ${RULES_FILE} is missing — run "serverk-bridge rules"`);
548
+ });
549
+
550
+ it("warns about a stale rule file and still validates", async () => {
551
+ writeBot(botManifest());
552
+ writeFileSync(join(root, RULES_FILE), "# edited by hand\n");
553
+
554
+ await validate();
555
+
556
+ expect(lines).toContain("bot-node is valid");
557
+ expect(lines).toContain(` warning: ${RULES_FILE} is out of date — run "serverk-bridge rules"`);
558
+ });
559
+
560
+ it("says when a bot turns the environment vault on", async () => {
539
561
  writeBot(
540
562
  botManifest({
541
563
  environment: {
@@ -549,7 +571,7 @@ describe("validating a bot package", () => {
549
571
  expect(lines).toContain(" environment: vault on");
550
572
  });
551
573
 
552
- test("fails a bot whose detection table is missing", async () => {
574
+ it("fails a bot whose detection table is missing", async () => {
553
575
  writeBot(botManifest(), null);
554
576
 
555
577
  await expect(validate()).rejects.toThrow(
@@ -557,7 +579,7 @@ describe("validating a bot package", () => {
557
579
  );
558
580
  });
559
581
 
560
- test("fails a bot whose detection table is stale", async () => {
582
+ it("fails a bot whose detection table is stale", async () => {
561
583
  writeBot(botManifest(), "{}\n");
562
584
 
563
585
  await expect(validate()).rejects.toThrow(
@@ -565,7 +587,7 @@ describe("validating a bot package", () => {
565
587
  );
566
588
  });
567
589
 
568
- test("warns about a port on a bot but still validates it", async () => {
590
+ it("warns about a port on a bot but still validates it", async () => {
569
591
  writeBot(
570
592
  botManifest({
571
593
  container: {
@@ -585,7 +607,7 @@ describe("validating a bot package", () => {
585
607
  );
586
608
  });
587
609
 
588
- test("fails a bot that declares platform secrets", async () => {
610
+ it("fails a bot that declares platform secrets", async () => {
589
611
  writeBot(
590
612
  botManifest({
591
613
  secrets: [
@@ -602,3 +624,75 @@ describe("validating a bot package", () => {
602
624
  );
603
625
  });
604
626
  });
627
+
628
+ describe("availability report", () => {
629
+ const driverWithModules = (modules: BridgeDriver["modules"]): BridgeDriver => ({
630
+ install: {
631
+ kind: BridgeKind.Install,
632
+ run: async () => {
633
+ return;
634
+ },
635
+ },
636
+ lifecycle: {
637
+ kind: BridgeKind.Lifecycle,
638
+ command: async () => [
639
+ "run",
640
+ ],
641
+ },
642
+ modules,
643
+ });
644
+
645
+ it("warns about a protected module that cannot say when its action is unavailable", () => {
646
+ expect(
647
+ availabilityReportLines(
648
+ driverWithModules({
649
+ worlds: {
650
+ kind: BridgeKind.Collection,
651
+ protectedActions: [
652
+ "delete",
653
+ ],
654
+ list: async () => [],
655
+ actions: {
656
+ delete: async () => {
657
+ return;
658
+ },
659
+ },
660
+ },
661
+ }),
662
+ ),
663
+ ).toEqual([
664
+ ' warning: module "worlds" protects delete but declares no `unavailable` — the panel offers the action everywhere and the platform stops the game before the driver can refuse it',
665
+ ]);
666
+ });
667
+
668
+ it("says nothing when the protected module declares the hook or protects nothing", () => {
669
+ expect(
670
+ availabilityReportLines(
671
+ driverWithModules({
672
+ worlds: {
673
+ kind: BridgeKind.Collection,
674
+ protectedActions: [
675
+ "delete",
676
+ ],
677
+ list: async () => [],
678
+ actions: {
679
+ delete: async () => {
680
+ return;
681
+ },
682
+ },
683
+ unavailable: async () => ({}),
684
+ },
685
+ players: {
686
+ kind: BridgeKind.Collection,
687
+ list: async () => [],
688
+ actions: {
689
+ kick: async () => {
690
+ return;
691
+ },
692
+ },
693
+ },
694
+ }),
695
+ ),
696
+ ).toEqual([]);
697
+ });
698
+ });
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
5
5
  import { z } from "zod";
6
6
  import { type ArticlePack, compileArticles } from "../blog";
7
7
  import { DETECT_TABLE_FILE, renderDetectTable } from "../bot/detect";
8
- import { PortProtocol } from "../core";
8
+ import { BridgeKind, PortProtocol } from "../core";
9
9
  import { GuideOpenTab, type GuideOpenTarget, type GuidePack, readGuidePack, readGuidePackage } from "../guides";
10
10
  import {
11
11
  declaredFiles,
@@ -31,6 +31,7 @@ import {
31
31
  } from "../protocol";
32
32
  import { validateDriver, validatePortActivation } from "../runtime/bridgeDriver";
33
33
  import { directorySource } from "./directorySource";
34
+ import { rulesReportLines } from "./rules";
34
35
 
35
36
  const HELP = `serverk-bridge validate [options]
36
37
 
@@ -279,6 +280,39 @@ const reportPlacement = (driver: BridgeDriver) => {
279
280
  }
280
281
  };
281
282
 
283
+ export const availabilityReportLines = (driver: BridgeDriver): string[] => {
284
+ const lines: string[] = [];
285
+
286
+ for (const [id, module] of Object.entries(driver.modules ?? {})) {
287
+ if (
288
+ module.kind !== BridgeKind.Actions
289
+ && module.kind !== BridgeKind.Catalog
290
+ && module.kind !== BridgeKind.Collection
291
+ && module.kind !== BridgeKind.Settings
292
+ ) {
293
+ continue;
294
+ }
295
+
296
+ const protectedActions = module.protectedActions ?? [];
297
+
298
+ if (protectedActions.length === 0 || module.unavailable) {
299
+ continue;
300
+ }
301
+
302
+ lines.push(
303
+ ` warning: module "${id}" protects ${protectedActions.join(", ")} but declares no \`unavailable\` — the panel offers the action everywhere and the platform stops the game before the driver can refuse it`,
304
+ );
305
+ }
306
+
307
+ return lines;
308
+ };
309
+
310
+ const reportAvailability = (driver: BridgeDriver) => {
311
+ for (const line of availabilityReportLines(driver)) {
312
+ console.log(line);
313
+ }
314
+ };
315
+
282
316
  const openTargetOf = (target: GuideOpenTarget) => {
283
317
  if (target.tab === GuideOpenTab.Panel) {
284
318
  return `panel tab "${target.tabId}"`;
@@ -413,6 +447,7 @@ const validateRunnablePackage = async (
413
447
  ...botPortLines(manifest),
414
448
  ...botEnvironmentLines(manifest),
415
449
  ...detectLines,
450
+ ...rulesReportLines(dir),
416
451
  ]) {
417
452
  console.log(line);
418
453
  }
@@ -432,6 +467,7 @@ const validateRunnablePackage = async (
432
467
  reportEvents(driver);
433
468
  reportActionTargets(driver);
434
469
  reportPlacement(driver);
470
+ reportAvailability(driver);
435
471
  };
436
472
 
437
473
  export const runValidate = async (args: string[]) => {
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import { mergeIni, parseIni } from "./ini";
3
3
 
4
4
  const SAMPLE = [
@@ -14,38 +14,38 @@ const SAMPLE = [
14
14
  ].join("\n");
15
15
 
16
16
  describe("parseIni", () => {
17
- test("reads the keys of one section", () => {
17
+ it("reads the keys of one section", () => {
18
18
  expect(parseIni(SAMPLE, "Server")).toEqual({
19
19
  name: "serverk",
20
20
  maxPlayers: "10",
21
21
  });
22
22
  });
23
23
 
24
- test("reads another section without leaking keys", () => {
24
+ it("reads another section without leaking keys", () => {
25
25
  expect(parseIni(SAMPLE, "Admin")).toEqual({
26
26
  password: "secret",
27
27
  });
28
28
  });
29
29
 
30
- test("reads the keys before the first header when no section is asked for", () => {
30
+ it("reads the keys before the first header when no section is asked for", () => {
31
31
  expect(parseIni("welcome=hello\n[Server]\nname=x\n")).toEqual({
32
32
  welcome: "hello",
33
33
  });
34
34
  });
35
35
 
36
- test("keeps the last of a repeated key", () => {
36
+ it("keeps the last of a repeated key", () => {
37
37
  expect(parseIni("[S]\nmod=a\nmod=b\n", "S")).toEqual({
38
38
  mod: "b",
39
39
  });
40
40
  });
41
41
 
42
- test("returns nothing for an unknown section", () => {
42
+ it("returns nothing for an unknown section", () => {
43
43
  expect(parseIni(SAMPLE, "Nothing")).toEqual({});
44
44
  });
45
45
  });
46
46
 
47
47
  describe("mergeIni", () => {
48
- test("rewrites a value in place and keeps comments and order", () => {
48
+ it("rewrites a value in place and keeps comments and order", () => {
49
49
  expect(
50
50
  mergeIni(
51
51
  SAMPLE,
@@ -69,7 +69,7 @@ describe("mergeIni", () => {
69
69
  );
70
70
  });
71
71
 
72
- test("appends an unknown key at the end of its section", () => {
72
+ it("appends an unknown key at the end of its section", () => {
73
73
  expect(
74
74
  mergeIni(
75
75
  SAMPLE,
@@ -93,7 +93,7 @@ describe("mergeIni", () => {
93
93
  );
94
94
  });
95
95
 
96
- test("creates a section that does not exist yet", () => {
96
+ it("creates a section that does not exist yet", () => {
97
97
  expect(
98
98
  mergeIni(
99
99
  "[Server]\nname=x\n",
@@ -105,7 +105,7 @@ describe("mergeIni", () => {
105
105
  ).toBe("[Server]\nname=x\n\n[Game]\nlevel=3\n");
106
106
  });
107
107
 
108
- test("drops repeated occurrences of a key it rewrites", () => {
108
+ it("drops repeated occurrences of a key it rewrites", () => {
109
109
  expect(
110
110
  mergeIni(
111
111
  "[S]\nmod=a\nmod=b\nother=1\n",
@@ -117,7 +117,7 @@ describe("mergeIni", () => {
117
117
  ).toBe("[S]\nmod=c\nother=1\n");
118
118
  });
119
119
 
120
- test("writes into an empty file", () => {
120
+ it("writes into an empty file", () => {
121
121
  expect(
122
122
  mergeIni(
123
123
  "",
@@ -129,7 +129,7 @@ describe("mergeIni", () => {
129
129
  ).toBe("[Server]\nname=x\n");
130
130
  });
131
131
 
132
- test("writes preamble keys when no section is asked for", () => {
132
+ it("writes preamble keys when no section is asked for", () => {
133
133
  expect(
134
134
  mergeIni("welcome=hello\n[Server]\nname=x\n", {
135
135
  welcome: "salam",
@@ -138,7 +138,7 @@ describe("mergeIni", () => {
138
138
  ).toBe("welcome=salam\nextra=1\n[Server]\nname=x\n");
139
139
  });
140
140
 
141
- test("leaves other sections untouched", () => {
141
+ it("leaves other sections untouched", () => {
142
142
  expect(
143
143
  mergeIni(
144
144
  "[A]\nkey=1\n[B]\nkey=2\n",
@@ -1,33 +1,33 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import { escapeJavaProperty, mergeJavaProperties, parseJavaProperties, unescapeJavaProperty } from "./javaProperties";
3
3
 
4
4
  const ARABIC = "سيرفرك";
5
5
 
6
6
  describe("parseJavaProperties", () => {
7
- test("decodes the unicode escapes the java server writes", () => {
7
+ it("decodes the unicode escapes the java server writes", () => {
8
8
  expect(parseJavaProperties("motd=\\u0633\\u064A\\u0631\\u0641\\u0631\\u0643\n")).toEqual({
9
9
  motd: ARABIC,
10
10
  });
11
11
  });
12
12
 
13
- test("decodes backslash escapes in values", () => {
13
+ it("decodes backslash escapes in values", () => {
14
14
  expect(parseJavaProperties("resource-pack=https\\://example.com/pack.zip\nmotd=a\\tb\\\\c\n")).toEqual({
15
15
  "resource-pack": "https://example.com/pack.zip",
16
16
  motd: "a\tb\\c",
17
17
  });
18
18
  });
19
19
 
20
- test("repairs a value the server stored as latin-1 bytes of utf-8", () => {
20
+ it("repairs a value the server stored as latin-1 bytes of utf-8", () => {
21
21
  expect(parseJavaProperties("motd=ddss \\u00D9\\u008A\\u00D8\\u00AA\n")).toEqual({
22
22
  motd: "ddss يت",
23
23
  });
24
24
  });
25
25
 
26
- test("keeps plain latin-1 text that is not utf-8", () => {
26
+ it("keeps plain latin-1 text that is not utf-8", () => {
27
27
  expect(unescapeJavaProperty("caf\\u00E9")).toBe("café");
28
28
  });
29
29
 
30
- test("keeps comments and plain values", () => {
30
+ it("keeps comments and plain values", () => {
31
31
  expect(parseJavaProperties("#Minecraft server properties\nmax-players=20\n")).toEqual({
32
32
  "max-players": "20",
33
33
  });
@@ -35,7 +35,7 @@ describe("parseJavaProperties", () => {
35
35
  });
36
36
 
37
37
  describe("mergeJavaProperties", () => {
38
- test("writes non ascii text as unicode escapes", () => {
38
+ it("writes non ascii text as unicode escapes", () => {
39
39
  expect(
40
40
  mergeJavaProperties("motd=A Minecraft Server\nmax-players=20\n", {
41
41
  motd: ARABIC,
@@ -43,21 +43,21 @@ describe("mergeJavaProperties", () => {
43
43
  ).toBe("motd=\\u0633\\u064A\\u0631\\u0641\\u0631\\u0643\nmax-players=20\n");
44
44
  });
45
45
 
46
- test("escapes the characters java treats as separators", () => {
46
+ it("escapes the characters java treats as separators", () => {
47
47
  expect(escapeJavaProperty("https://a.b#c=d!e")).toBe("https\\://a.b\\#c\\=d\\!e");
48
48
  });
49
49
 
50
- test("escapes a leading space and control characters", () => {
50
+ it("escapes a leading space and control characters", () => {
51
51
  expect(escapeJavaProperty(" a\tb\\c")).toBe("\\ a\\tb\\\\c");
52
52
  });
53
53
 
54
- test("round trips astral characters", () => {
54
+ it("round trips astral characters", () => {
55
55
  const value = "hi 🎮";
56
56
 
57
57
  expect(unescapeJavaProperty(escapeJavaProperty(value))).toBe(value);
58
58
  });
59
59
 
60
- test("appends a key the file does not have yet", () => {
60
+ it("appends a key the file does not have yet", () => {
61
61
  expect(
62
62
  mergeJavaProperties("", {
63
63
  motd: "hello",
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import {
3
3
  decodeSourceCfgValue,
4
4
  encodeSourceCfgValue,
@@ -8,21 +8,21 @@ import {
8
8
  } from "./sourceCfg";
9
9
 
10
10
  describe("readSourceCfgEntry", () => {
11
- test("splits a key from its value", () => {
11
+ it("splits a key from its value", () => {
12
12
  expect(readSourceCfgEntry('hostname "serverk"')).toEqual({
13
13
  key: "hostname",
14
14
  value: '"serverk"',
15
15
  });
16
16
  });
17
17
 
18
- test("accepts a key with no value", () => {
18
+ it("accepts a key with no value", () => {
19
19
  expect(readSourceCfgEntry("sv_lan")).toEqual({
20
20
  key: "sv_lan",
21
21
  value: "",
22
22
  });
23
23
  });
24
24
 
25
- test("ignores comments and blank lines", () => {
25
+ it("ignores comments and blank lines", () => {
26
26
  expect(readSourceCfgEntry("// a comment")).toBeNull();
27
27
  expect(readSourceCfgEntry("# a comment")).toBeNull();
28
28
  expect(readSourceCfgEntry(" ")).toBeNull();
@@ -31,32 +31,32 @@ describe("readSourceCfgEntry", () => {
31
31
  });
32
32
 
33
33
  describe("decodeSourceCfgValue", () => {
34
- test("unquotes a string and unescapes it", () => {
34
+ it("unquotes a string and unescapes it", () => {
35
35
  expect(decodeSourceCfgValue('"a \\"b\\" c"')).toBe('a "b" c');
36
36
  });
37
37
 
38
- test("reads booleans and numbers", () => {
38
+ it("reads booleans and numbers", () => {
39
39
  expect(decodeSourceCfgValue("true")).toBe(true);
40
40
  expect(decodeSourceCfgValue("false")).toBe(false);
41
41
  expect(decodeSourceCfgValue("42")).toBe(42);
42
42
  expect(decodeSourceCfgValue("-1.5")).toBe(-1.5);
43
43
  });
44
44
 
45
- test("leaves a bare word alone", () => {
45
+ it("leaves a bare word alone", () => {
46
46
  expect(decodeSourceCfgValue("de_dust2")).toBe("de_dust2");
47
47
  });
48
48
  });
49
49
 
50
50
  describe("encodeSourceCfgValue", () => {
51
- test("quotes and escapes text", () => {
51
+ it("quotes and escapes text", () => {
52
52
  expect(encodeSourceCfgValue('a "b"')).toBe('"a \\"b\\""');
53
53
  });
54
54
 
55
- test("flattens newlines", () => {
55
+ it("flattens newlines", () => {
56
56
  expect(encodeSourceCfgValue("a\nb")).toBe('"a b"');
57
57
  });
58
58
 
59
- test("writes booleans and numbers bare", () => {
59
+ it("writes booleans and numbers bare", () => {
60
60
  expect(encodeSourceCfgValue(true)).toBe("true");
61
61
  expect(encodeSourceCfgValue(7)).toBe("7");
62
62
  expect(encodeSourceCfgValue(null)).toBe('""');
@@ -64,7 +64,7 @@ describe("encodeSourceCfgValue", () => {
64
64
  });
65
65
 
66
66
  describe("parseSourceCfg", () => {
67
- test("reads a source cfg file", () => {
67
+ it("reads a source cfg file", () => {
68
68
  expect(parseSourceCfg('// header\nhostname "serverk"\nsv_maxplayers 24\nsv_cheats false\n')).toEqual({
69
69
  hostname: "serverk",
70
70
  sv_maxplayers: 24,
@@ -74,7 +74,7 @@ describe("parseSourceCfg", () => {
74
74
  });
75
75
 
76
76
  describe("mergeSourceCfg", () => {
77
- test("rewrites a value and keeps the comments", () => {
77
+ it("rewrites a value and keeps the comments", () => {
78
78
  expect(
79
79
  mergeSourceCfg('// header\nhostname "old"\nsv_maxplayers 24\n', {
80
80
  hostname: "new",
@@ -82,7 +82,7 @@ describe("mergeSourceCfg", () => {
82
82
  ).toBe('// header\nhostname "new"\nsv_maxplayers 24\n');
83
83
  });
84
84
 
85
- test("appends an unknown key", () => {
85
+ it("appends an unknown key", () => {
86
86
  expect(
87
87
  mergeSourceCfg('hostname "x"\n', {
88
88
  rcon_password: "abc",
@@ -90,7 +90,7 @@ describe("mergeSourceCfg", () => {
90
90
  ).toBe('hostname "x"\nrcon_password "abc"\n');
91
91
  });
92
92
 
93
- test("collapses a repeated key it rewrites", () => {
93
+ it("collapses a repeated key it rewrites", () => {
94
94
  expect(
95
95
  mergeSourceCfg("map de_dust2\nmap de_nuke\n", {
96
96
  map: "de_inferno",