@serverkgg/bridge 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/rules/package.md +60 -0
- package/src/blog/articleFrontmatter.test.ts +10 -10
- package/src/blog/compile.test.ts +9 -9
- package/src/bot/botDeploy.test.ts +16 -16
- package/src/bot/botEnvironment.test.ts +8 -8
- package/src/bot/botIdentity.test.ts +12 -12
- package/src/bot/botPanel.test.ts +13 -13
- package/src/bot/botPanel.ts +4 -4
- package/src/bot/botState.test.ts +3 -3
- package/src/bot/botTerminal.test.ts +5 -5
- package/src/bot/botVariables.test.ts +8 -8
- package/src/bot/detect/botToken.test.ts +6 -6
- package/src/bot/detect/detect.test.ts +21 -21
- package/src/bot/detect/detectTable.test.ts +5 -5
- package/src/bot/detect/envExample.test.ts +2 -2
- package/src/bot/detect/envScan.test.ts +4 -4
- package/src/bot/detect/inferBotPlatform.test.ts +4 -4
- package/src/bot/detect/keys.test.ts +3 -3
- package/src/bot/detect/sensitiveKey.test.ts +5 -5
- package/src/bot/detect/serverkJson.test.ts +5 -5
- package/src/catalogs/curseforge.test.ts +35 -35
- package/src/catalogs/thunderstore.test.ts +9 -9
- package/src/cli/detectTable.test.ts +3 -3
- package/src/cli/link.test.ts +11 -11
- package/src/cli/main.ts +8 -0
- package/src/cli/rules.test.ts +198 -0
- package/src/cli/rules.ts +188 -0
- package/src/cli/validate.test.ts +52 -30
- package/src/cli/validate.ts +2 -0
- package/src/codecs/ini.test.ts +13 -13
- package/src/codecs/javaProperties.test.ts +11 -11
- package/src/codecs/sourceCfg.test.ts +14 -14
- package/src/codecs/xmlProperties.test.ts +10 -10
- package/src/core/bridgePort.test.ts +6 -6
- package/src/core/bridgeUserError.test.ts +4 -4
- package/src/core/bridgeValue.test.ts +5 -5
- package/src/core/describeDownloadFailure.test.ts +12 -12
- package/src/core/describeDownloadFailure.ts +10 -10
- package/src/guides/markdown.test.ts +31 -31
- package/src/install/installStamp.test.ts +9 -9
- package/src/manifest/extension.ts +36 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +208 -90
- package/src/manifest/manifest.ts +34 -0
- package/src/net/loopback.test.ts +10 -10
- package/src/presence/rosterSync.test.ts +7 -7
- package/src/probes/a2s.test.ts +5 -5
- package/src/probes/minecraftPing.test.ts +17 -17
- package/src/probes/raknetPing.test.ts +13 -13
- package/src/rcon/rconAccess.test.ts +20 -20
- package/src/rcon/rconAccess.ts +13 -13
- package/src/rcon/rconClient.test.ts +49 -49
- package/src/rcon/rconFrame.test.ts +14 -14
- package/src/rcon/rconIds.test.ts +4 -4
- package/src/rcon/rconWriter.test.ts +6 -6
- package/src/runtime/bridgeActivity.test.ts +15 -15
- package/src/runtime/bridgeBackupLease.test.ts +9 -9
- package/src/runtime/bridgeConfigReader.test.ts +5 -5
- package/src/runtime/bridgeConsoleHandler.test.ts +8 -8
- package/src/runtime/bridgeContext.test.ts +5 -5
- package/src/runtime/bridgeDriver.test.ts +94 -94
- package/src/runtime/bridgeEvents.test.ts +15 -15
- package/src/runtime/bridgeFiles.test.ts +54 -54
- package/src/runtime/bridgeInstall.test.ts +10 -10
- package/src/runtime/bridgeLiveConfig.test.ts +6 -6
- package/src/runtime/bridgePending.test.ts +6 -6
- package/src/runtime/bridgeRedaction.test.ts +16 -16
- package/src/runtime/bridgeRegistry.test.ts +52 -52
- package/src/runtime/bridgeRegistry.ts +2 -2
- package/src/runtime/bridgeServer.test.ts +31 -31
- package/src/runtime/bridgeSetup.test.ts +27 -27
- package/src/runtime/bridgeSetup.ts +1 -1
- package/src/runtime/bridgeSetupHandlers.test.ts +13 -13
- package/src/runtime/bridgeSignal.test.ts +7 -7
- package/src/runtime/bridgeSupervisor.test.ts +42 -42
- package/src/runtime/serve.ts +4 -4
- package/src/steam/avatars.test.ts +13 -13
- package/src/steam/steamApp.test.ts +8 -8
- package/src/steam/steamcmd.test.ts +17 -17
- package/src/steam/steamcmd.ts +4 -4
- package/src/steam/steamcmdNarrator.test.ts +4 -4
- package/src/steam/workshop.test.ts +5 -5
- package/src/utils/changedSettingKeys.test.ts +5 -5
- package/src/utils/describeExecFailure.test.ts +4 -4
- package/src/utils/describeExecFailure.ts +1 -1
- package/src/utils/sameSettingValue.test.ts +7 -7
- package/src/utils/token.test.ts +7 -7
- package/src/wine/protonCommand.test.ts +5 -5
- package/src/wine/wineCommand.test.ts +6 -6
- package/src/wine/winePrefix.test.ts +6 -6
- package/src/wire/activity.test.ts +8 -8
- package/src/wire/panel.test.ts +28 -28
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect,
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { BridgeRuntime } from "../core";
|
|
4
4
|
import { BridgeLifecycleSubject, BridgeStopSignal } from "../modules";
|
|
@@ -230,7 +230,7 @@ const wrapperArgv = () => {
|
|
|
230
230
|
};
|
|
231
231
|
|
|
232
232
|
describe("createSupervisor", () => {
|
|
233
|
-
|
|
233
|
+
it("runs the game outside the bridge's own process group", async () => {
|
|
234
234
|
const { supervisor, pid } = await startWrapper(WRAPPER, 1);
|
|
235
235
|
|
|
236
236
|
try {
|
|
@@ -247,7 +247,7 @@ describe("createSupervisor", () => {
|
|
|
247
247
|
expect(await waitGone(pid, 5000)).toBe(true);
|
|
248
248
|
}, 20_000);
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
it("takes the wrapper's child down with it when the stop times out", async () => {
|
|
251
251
|
const { supervisor, pid } = await startWrapper(WRAPPER, 1);
|
|
252
252
|
|
|
253
253
|
await supervisor.stop();
|
|
@@ -256,7 +256,7 @@ describe("createSupervisor", () => {
|
|
|
256
256
|
expect(await waitGone(pid, 5000), "the game was orphaned when the wrapper was signalled").toBe(true);
|
|
257
257
|
}, 20_000);
|
|
258
258
|
|
|
259
|
-
|
|
259
|
+
it("kills the wrapper's child through the escalation when the wrapper ignores SIGTERM", async () => {
|
|
260
260
|
const { supervisor, pid } = await startWrapper(STUBBORN_WRAPPER, 1);
|
|
261
261
|
|
|
262
262
|
await supervisor.stop();
|
|
@@ -265,7 +265,7 @@ describe("createSupervisor", () => {
|
|
|
265
265
|
expect(await waitGone(pid, 5000), "the game outlived the SIGKILL escalation").toBe(true);
|
|
266
266
|
}, 30_000);
|
|
267
267
|
|
|
268
|
-
|
|
268
|
+
it("does not restart a game that exited before it ever reached its ready line", async () => {
|
|
269
269
|
const { supervisor, records } = crashSupervisor(() => BOOT_CRASH, NEVER_READY);
|
|
270
270
|
|
|
271
271
|
await supervisor.start();
|
|
@@ -289,7 +289,7 @@ describe("createSupervisor", () => {
|
|
|
289
289
|
expect(supervisor.restarts).toBe(0);
|
|
290
290
|
}, 30_000);
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
it("restarts a game that crashed after it reached its ready line", async () => {
|
|
293
293
|
const { supervisor, records } = crashSupervisor(() => RUNNING_CRASH, READY);
|
|
294
294
|
|
|
295
295
|
await supervisor.start();
|
|
@@ -307,7 +307,7 @@ describe("createSupervisor", () => {
|
|
|
307
307
|
await supervisor.stop();
|
|
308
308
|
}, 30_000);
|
|
309
309
|
|
|
310
|
-
|
|
310
|
+
it("flags a game killed with SIGKILL as out of memory", async () => {
|
|
311
311
|
const { supervisor, records } = crashSupervisor(() => OOM_CRASH, NEVER_READY);
|
|
312
312
|
|
|
313
313
|
await supervisor.start();
|
|
@@ -323,7 +323,7 @@ describe("createSupervisor", () => {
|
|
|
323
323
|
await supervisor.stop();
|
|
324
324
|
}, 30_000);
|
|
325
325
|
|
|
326
|
-
|
|
326
|
+
it("trips the breaker on the third running crash inside the window", async () => {
|
|
327
327
|
const { supervisor, records } = crashSupervisor(() => RUNNING_CRASH, READY);
|
|
328
328
|
|
|
329
329
|
await supervisor.start();
|
|
@@ -346,7 +346,7 @@ describe("createSupervisor", () => {
|
|
|
346
346
|
await supervisor.stop();
|
|
347
347
|
}, 40_000);
|
|
348
348
|
|
|
349
|
-
|
|
349
|
+
it("launches again when start is asked after a booting crash", async () => {
|
|
350
350
|
let script = BOOT_CRASH;
|
|
351
351
|
|
|
352
352
|
const { supervisor, records } = crashSupervisor(() => script, NEVER_READY);
|
|
@@ -367,7 +367,7 @@ describe("createSupervisor", () => {
|
|
|
367
367
|
expect(supervisor.runtime).toBe(BridgeRuntime.Stopped);
|
|
368
368
|
}, 30_000);
|
|
369
369
|
|
|
370
|
-
|
|
370
|
+
it("carries the last line the game printed into the crash record", async () => {
|
|
371
371
|
const { supervisor, records } = crashSupervisor(() => LAST_LINE_CRASH, NEVER_READY);
|
|
372
372
|
|
|
373
373
|
await supervisor.start();
|
|
@@ -376,7 +376,7 @@ describe("createSupervisor", () => {
|
|
|
376
376
|
expect(records.at(0)?.lastLine).toBe(LAST_LINE);
|
|
377
377
|
}, 30_000);
|
|
378
378
|
|
|
379
|
-
|
|
379
|
+
it("carries the last line even when the game exits with no pause after printing it", async () => {
|
|
380
380
|
const { supervisor, records } = crashSupervisor(() => TIGHT_LAST_LINE_CRASH, NEVER_READY);
|
|
381
381
|
|
|
382
382
|
await supervisor.start();
|
|
@@ -386,7 +386,7 @@ describe("createSupervisor", () => {
|
|
|
386
386
|
expect(records.at(0)?.exitCode).toBe(3);
|
|
387
387
|
}, 30_000);
|
|
388
388
|
|
|
389
|
-
|
|
389
|
+
it("leaves no dead child behind when the game exits the instant it launches", async () => {
|
|
390
390
|
let adopted = 0;
|
|
391
391
|
|
|
392
392
|
for (let attempt = 0; attempt < 40 && adopted < 20; attempt += 1) {
|
|
@@ -421,7 +421,7 @@ describe("createSupervisor", () => {
|
|
|
421
421
|
expect(adopted, "the game never survived long enough to be adopted as the child").toBeGreaterThan(0);
|
|
422
422
|
}, 30_000);
|
|
423
423
|
|
|
424
|
-
|
|
424
|
+
it("treats a run that outlived the stable window as running even when the ready line never came", async () => {
|
|
425
425
|
const { supervisor, records, stables } = timedSupervisor(() => LATE_CRASH, NEVER_READY, 300);
|
|
426
426
|
|
|
427
427
|
await supervisor.start();
|
|
@@ -438,7 +438,7 @@ describe("createSupervisor", () => {
|
|
|
438
438
|
await supervisor.stop();
|
|
439
439
|
}, 30_000);
|
|
440
440
|
|
|
441
|
-
|
|
441
|
+
it("keeps a slow boot starting past the stable window when the driver declares a ready timeout", async () => {
|
|
442
442
|
const { supervisor, records, stables } = timedSupervisor(() => LATE_CRASH, NEVER_READY, 300, 30);
|
|
443
443
|
|
|
444
444
|
await supervisor.start();
|
|
@@ -457,7 +457,7 @@ describe("createSupervisor", () => {
|
|
|
457
457
|
expect(stables()).toBe(0);
|
|
458
458
|
}, 30_000);
|
|
459
459
|
|
|
460
|
-
|
|
460
|
+
it("fails the boot when the declared ready timeout passes, never flipping it to running", async () => {
|
|
461
461
|
const { supervisor, records, stables } = timedSupervisor(() => "exec sleep 300", NEVER_READY, 60_000, 1);
|
|
462
462
|
|
|
463
463
|
await supervisor.start();
|
|
@@ -476,7 +476,7 @@ describe("createSupervisor", () => {
|
|
|
476
476
|
expect(stables()).toBe(0);
|
|
477
477
|
}, 30_000);
|
|
478
478
|
|
|
479
|
-
|
|
479
|
+
it("still treats a game with no declared ready timeout as running once the default wait passes", async () => {
|
|
480
480
|
const { supervisor, stables } = timedSupervisor(() => "exec sleep 300", NEVER_READY, 60_000);
|
|
481
481
|
|
|
482
482
|
await supervisor.start();
|
|
@@ -488,7 +488,7 @@ describe("createSupervisor", () => {
|
|
|
488
488
|
await supervisor.stop();
|
|
489
489
|
}, 30_000);
|
|
490
490
|
|
|
491
|
-
|
|
491
|
+
it("opens a new episode when start is asked after the breaker tripped", async () => {
|
|
492
492
|
let script = RUNNING_CRASH;
|
|
493
493
|
|
|
494
494
|
const { supervisor, records } = crashSupervisor(() => script, READY);
|
|
@@ -508,7 +508,7 @@ describe("createSupervisor", () => {
|
|
|
508
508
|
await supervisor.stop();
|
|
509
509
|
}, 40_000);
|
|
510
510
|
|
|
511
|
-
|
|
511
|
+
it("spawns one game when two starts race inside the launch window", async () => {
|
|
512
512
|
const { supervisor, pids, launches, overlapped } = racedSupervisor(200, wrapperArgv);
|
|
513
513
|
|
|
514
514
|
await Promise.all([
|
|
@@ -529,7 +529,7 @@ describe("createSupervisor", () => {
|
|
|
529
529
|
expect(await waitGone(pids.at(0) ?? 0, 5000)).toBe(true);
|
|
530
530
|
}, 30_000);
|
|
531
531
|
|
|
532
|
-
|
|
532
|
+
it("never spawns beside the game a stop is taking down", async () => {
|
|
533
533
|
const { supervisor, pids, launches, overlapped } = racedSupervisor(0, wrapperArgv);
|
|
534
534
|
|
|
535
535
|
await supervisor.start();
|
|
@@ -555,7 +555,7 @@ describe("createSupervisor", () => {
|
|
|
555
555
|
expect(await waitGone(pids.at(1) ?? 0, 5000)).toBe(true);
|
|
556
556
|
}, 30_000);
|
|
557
557
|
|
|
558
|
-
|
|
558
|
+
it("starts again after a launch that threw before the game was spawned", async () => {
|
|
559
559
|
let resolvable = false;
|
|
560
560
|
|
|
561
561
|
const { supervisor, pids } = racedSupervisor(0, () => {
|
|
@@ -581,7 +581,7 @@ describe("createSupervisor", () => {
|
|
|
581
581
|
expect(await waitGone(pids.at(0) ?? 0, 5000)).toBe(true);
|
|
582
582
|
}, 30_000);
|
|
583
583
|
|
|
584
|
-
|
|
584
|
+
it("stops a game that reports a failed boot instead of waiting for a ready line that never comes", async () => {
|
|
585
585
|
const records: BridgeCrashRecord[] = [];
|
|
586
586
|
const pids: number[] = [];
|
|
587
587
|
|
|
@@ -628,7 +628,7 @@ describe("createSupervisor", () => {
|
|
|
628
628
|
expect(await waitGone(pids.at(0) ?? 0, 5000)).toBe(true);
|
|
629
629
|
}, 30_000);
|
|
630
630
|
|
|
631
|
-
|
|
631
|
+
it("ignores the failed pattern once the game has been running long enough to count as stable", async () => {
|
|
632
632
|
const records: BridgeCrashRecord[] = [];
|
|
633
633
|
|
|
634
634
|
const supervisor = createSupervisor({
|
|
@@ -713,7 +713,7 @@ const restartSupervisor = (script: () => string, resolveEnvironment?: () => Prom
|
|
|
713
713
|
};
|
|
714
714
|
|
|
715
715
|
describe("the resolved environment", () => {
|
|
716
|
-
|
|
716
|
+
it("reaches the game beside what the bridge inherited", async () => {
|
|
717
717
|
const { supervisor } = restartSupervisor(
|
|
718
718
|
() => 'echo "probe=$SERVERK_PROBE"; echo "path=$PATH"; echo ready; exec sleep 300',
|
|
719
719
|
async () => ({
|
|
@@ -731,7 +731,7 @@ describe("the resolved environment", () => {
|
|
|
731
731
|
await supervisor.stop();
|
|
732
732
|
}, 20_000);
|
|
733
733
|
|
|
734
|
-
|
|
734
|
+
it("leaves the inherited environment alone when the driver resolves none", async () => {
|
|
735
735
|
const { supervisor } = restartSupervisor(() => 'echo "path=$PATH"; echo ready; exec sleep 300');
|
|
736
736
|
const path = supervisor.watch(/^path=(?<value>.*)$/, 10_000);
|
|
737
737
|
|
|
@@ -745,7 +745,7 @@ describe("the resolved environment", () => {
|
|
|
745
745
|
await supervisor.stop();
|
|
746
746
|
}, 20_000);
|
|
747
747
|
|
|
748
|
-
|
|
748
|
+
it("refuses a key that is not a variable name as a start failure", async () => {
|
|
749
749
|
const { supervisor } = restartSupervisor(
|
|
750
750
|
() => READY_THEN_WAIT,
|
|
751
751
|
async () => ({
|
|
@@ -757,7 +757,7 @@ describe("the resolved environment", () => {
|
|
|
757
757
|
expect(supervisor.running).toBe(false);
|
|
758
758
|
});
|
|
759
759
|
|
|
760
|
-
|
|
760
|
+
it("refuses a value carrying a NUL byte without echoing it", async () => {
|
|
761
761
|
const { supervisor } = restartSupervisor(
|
|
762
762
|
() => READY_THEN_WAIT,
|
|
763
763
|
async () => ({
|
|
@@ -776,7 +776,7 @@ describe("the resolved environment", () => {
|
|
|
776
776
|
});
|
|
777
777
|
|
|
778
778
|
describe("restart", () => {
|
|
779
|
-
|
|
779
|
+
it("replaces the process without ever reporting it stopped", async () => {
|
|
780
780
|
const { supervisor, records, pids, stopped } = restartSupervisor(() => READY_THEN_WAIT);
|
|
781
781
|
|
|
782
782
|
await supervisor.start();
|
|
@@ -819,7 +819,7 @@ describe("restart", () => {
|
|
|
819
819
|
expect(stopped()).toBe(1);
|
|
820
820
|
}, 30_000);
|
|
821
821
|
|
|
822
|
-
|
|
822
|
+
it("counts a quick exit after a restart as a boot crash, not a running one", async () => {
|
|
823
823
|
let script = READY_THEN_WAIT;
|
|
824
824
|
|
|
825
825
|
const { supervisor, records, pids } = restartSupervisor(() => script);
|
|
@@ -848,7 +848,7 @@ describe("restart", () => {
|
|
|
848
848
|
expect(supervisor.restarts).toBe(0);
|
|
849
849
|
}, 30_000);
|
|
850
850
|
|
|
851
|
-
|
|
851
|
+
it("refuses to restart a game that is meant to be stopped", async () => {
|
|
852
852
|
const { supervisor } = restartSupervisor(() => READY_THEN_WAIT);
|
|
853
853
|
|
|
854
854
|
await expect(supervisor.restart()).rejects.toThrow("the game is not meant to be running");
|
|
@@ -907,7 +907,7 @@ const runningWith = async (supervisor: BridgeSupervisor, pids: number[], count:
|
|
|
907
907
|
};
|
|
908
908
|
|
|
909
909
|
describe("the stop signal", () => {
|
|
910
|
-
|
|
910
|
+
it("stops a game that exits on SIGTERM well inside the timeout", async () => {
|
|
911
911
|
const { supervisor, pids } = signalSupervisor(SIGNAL_EXITS, 10, BridgeStopSignal.Term);
|
|
912
912
|
|
|
913
913
|
await supervisor.start();
|
|
@@ -921,7 +921,7 @@ describe("the stop signal", () => {
|
|
|
921
921
|
expect(await waitGone(pids[0] ?? 0, 5000)).toBe(true);
|
|
922
922
|
}, 30_000);
|
|
923
923
|
|
|
924
|
-
|
|
924
|
+
it("stops a game that exits on SIGINT when the driver asks for it", async () => {
|
|
925
925
|
const { supervisor, pids } = signalSupervisor(SIGNAL_EXITS, 10, BridgeStopSignal.Interrupt);
|
|
926
926
|
|
|
927
927
|
await supervisor.start();
|
|
@@ -934,7 +934,7 @@ describe("the stop signal", () => {
|
|
|
934
934
|
expect(await waitGone(pids[0] ?? 0, 5000)).toBe(true);
|
|
935
935
|
}, 30_000);
|
|
936
936
|
|
|
937
|
-
|
|
937
|
+
it("still kills a game that ignores the signal once the timeout passes", async () => {
|
|
938
938
|
const { supervisor, pids } = signalSupervisor(SIGNAL_IGNORED, 1, BridgeStopSignal.Term);
|
|
939
939
|
|
|
940
940
|
await supervisor.start();
|
|
@@ -949,7 +949,7 @@ describe("the stop signal", () => {
|
|
|
949
949
|
expect(await waitGone(pids[0] ?? 0, 5000), "the ignoring child outlived the kill").toBe(true);
|
|
950
950
|
}, 30_000);
|
|
951
951
|
|
|
952
|
-
|
|
952
|
+
it("keeps waiting the timeout before SIGTERM when the driver declares no signal", async () => {
|
|
953
953
|
const { supervisor, pids } = signalSupervisor(SIGNAL_EXITS, 2);
|
|
954
954
|
|
|
955
955
|
await supervisor.start();
|
|
@@ -963,7 +963,7 @@ describe("the stop signal", () => {
|
|
|
963
963
|
expect(await waitGone(pids[0] ?? 0, 5000)).toBe(true);
|
|
964
964
|
}, 30_000);
|
|
965
965
|
|
|
966
|
-
|
|
966
|
+
it("restarts through the same signal-first path", async () => {
|
|
967
967
|
const { supervisor, pids } = signalSupervisor(SIGNAL_EXITS, 10, BridgeStopSignal.Term);
|
|
968
968
|
|
|
969
969
|
await supervisor.start();
|
|
@@ -1056,7 +1056,7 @@ describe("secret redaction", () => {
|
|
|
1056
1056
|
restore();
|
|
1057
1057
|
});
|
|
1058
1058
|
|
|
1059
|
-
|
|
1059
|
+
it("hides a secret value from stdout, stderr, the stored lines and the crash last line", async () => {
|
|
1060
1060
|
const redact = createBridgeRedaction(
|
|
1061
1061
|
[
|
|
1062
1062
|
"BOT_TOKEN",
|
|
@@ -1081,7 +1081,7 @@ describe("secret redaction", () => {
|
|
|
1081
1081
|
expect(written.join("")).toContain(REDACTED);
|
|
1082
1082
|
}, 30_000);
|
|
1083
1083
|
|
|
1084
|
-
|
|
1084
|
+
it("hides a platform secret from the manifest that the game prints", async () => {
|
|
1085
1085
|
const redact = createBridgeRedaction(
|
|
1086
1086
|
[],
|
|
1087
1087
|
variablesSource(
|
|
@@ -1105,7 +1105,7 @@ describe("secret redaction", () => {
|
|
|
1105
1105
|
expect(records.at(0)?.lastLine).toBe(`fatal: bad token ${REDACTED}`);
|
|
1106
1106
|
}, 30_000);
|
|
1107
1107
|
|
|
1108
|
-
|
|
1108
|
+
it("leaves a value shorter than eight characters in the lines", async () => {
|
|
1109
1109
|
const redact = createBridgeRedaction(
|
|
1110
1110
|
[
|
|
1111
1111
|
"RCON_PASSWORD",
|
|
@@ -1122,7 +1122,7 @@ describe("secret redaction", () => {
|
|
|
1122
1122
|
expect(records.at(0)?.lastLine).toBe(`fatal: bad token ${LEAKED}`);
|
|
1123
1123
|
}, 30_000);
|
|
1124
1124
|
|
|
1125
|
-
|
|
1125
|
+
it("hides a value saved after the start from the lines that follow", async () => {
|
|
1126
1126
|
const { source, save } = variablesSource({});
|
|
1127
1127
|
const redact = createBridgeRedaction(
|
|
1128
1128
|
[
|
|
@@ -1152,7 +1152,7 @@ describe("secret redaction", () => {
|
|
|
1152
1152
|
await supervisor.stop();
|
|
1153
1153
|
}, 30_000);
|
|
1154
1154
|
|
|
1155
|
-
|
|
1155
|
+
it("runs a driver with no secret fields and no secrets without touching a line", async () => {
|
|
1156
1156
|
const redact = createBridgeRedaction([], variablesSource({}).source);
|
|
1157
1157
|
|
|
1158
1158
|
const { supervisor, records } = redactingSupervisor(LEAK_AND_CRASH, redact);
|
|
@@ -1220,7 +1220,7 @@ const subjectSupervisor = (script: string, subject?: BridgeLifecycleSubject) =>
|
|
|
1220
1220
|
};
|
|
1221
1221
|
|
|
1222
1222
|
describe("the lifecycle subject", () => {
|
|
1223
|
-
|
|
1223
|
+
it("names the game in the lifecycle lines when the driver declares no subject", async () => {
|
|
1224
1224
|
const { supervisor, messages } = subjectSupervisor(READY_THEN_WAIT);
|
|
1225
1225
|
|
|
1226
1226
|
await supervisor.start();
|
|
@@ -1236,7 +1236,7 @@ describe("the lifecycle subject", () => {
|
|
|
1236
1236
|
]);
|
|
1237
1237
|
}, 20_000);
|
|
1238
1238
|
|
|
1239
|
-
|
|
1239
|
+
it("names the bot in every lifecycle line of a bot, the crash loop included", async () => {
|
|
1240
1240
|
const { supervisor, messages, records } = subjectSupervisor(RUNNING_CRASH, BridgeLifecycleSubject.Bot);
|
|
1241
1241
|
|
|
1242
1242
|
await supervisor.start();
|
|
@@ -1252,7 +1252,7 @@ describe("the lifecycle subject", () => {
|
|
|
1252
1252
|
expect(messages.filter((message) => message.includes("game"))).toEqual([]);
|
|
1253
1253
|
}, 40_000);
|
|
1254
1254
|
|
|
1255
|
-
|
|
1255
|
+
it("names the bot when it refuses an environment it cannot pass on", async () => {
|
|
1256
1256
|
const { logger } = recordingLogger();
|
|
1257
1257
|
|
|
1258
1258
|
const supervisor = createSupervisor({
|
package/src/runtime/serve.ts
CHANGED
|
@@ -41,12 +41,12 @@ const DEFAULT_SAMPLE_REFRESH_SECONDS = 30;
|
|
|
41
41
|
|
|
42
42
|
const INSTALL_FAILED_TEXT: Record<BridgeLifecycleSubject, LocalizedText> = {
|
|
43
43
|
[BridgeLifecycleSubject.Game]: {
|
|
44
|
-
ar: "
|
|
45
|
-
en: "We
|
|
44
|
+
ar: "تعذّر تثبيت اللعبة، جرب مرة ثانية.",
|
|
45
|
+
en: "We couldn't install the game. Try again.",
|
|
46
46
|
},
|
|
47
47
|
[BridgeLifecycleSubject.Bot]: {
|
|
48
|
-
ar: "
|
|
49
|
-
en: "We
|
|
48
|
+
ar: "تعذّر تثبيت كود بوتك، جرب مرة ثانية.",
|
|
49
|
+
en: "We couldn't install your bot's code. Try again.",
|
|
50
50
|
},
|
|
51
51
|
};
|
|
52
52
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, describe, expect,
|
|
1
|
+
import { afterEach, describe, expect, it, setSystemTime } from "bun:test";
|
|
2
2
|
import type { BridgeContext } from "../context";
|
|
3
3
|
import type { BridgeValues } from "../core";
|
|
4
4
|
import { createSteamAvatars, STEAM_AVATAR_CACHE_TTL_MS, steamAvatarHashOf } from "./avatars";
|
|
@@ -79,22 +79,22 @@ afterEach(() => {
|
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
describe("reading the avatar hash out of a steam profile picture", () => {
|
|
82
|
-
|
|
82
|
+
it("takes the hash out of the full-size url steam answers with", () => {
|
|
83
83
|
expect(steamAvatarHashOf(`https://avatars.steamstatic.com/${ALICE_HASH}_full.jpg`)).toBe(ALICE_HASH);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
it("takes the hash out of a url that carries no size suffix", () => {
|
|
87
87
|
expect(steamAvatarHashOf(`https://avatars.steamstatic.com/${BOB_HASH}.jpg`)).toBe(BOB_HASH);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
it("reports no hash for a url that is not a steam avatar", () => {
|
|
91
91
|
expect(steamAvatarHashOf("https://example.com/avatar.png")).toBeNull();
|
|
92
92
|
expect(steamAvatarHashOf("")).toBeNull();
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
describe("keeping the steam avatars a roster shows", () => {
|
|
97
|
-
|
|
97
|
+
it("asks steam for a hash it has never seen, and answers with it", async () => {
|
|
98
98
|
const steam = fakeSteam({
|
|
99
99
|
answer: () => [
|
|
100
100
|
summaryOf(ALICE, ALICE_HASH),
|
|
@@ -123,7 +123,7 @@ describe("keeping the steam avatars a roster shows", () => {
|
|
|
123
123
|
]);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
it("answers the second time without asking steam again", async () => {
|
|
127
127
|
const steam = fakeSteam({
|
|
128
128
|
answer: () => [
|
|
129
129
|
summaryOf(ALICE, ALICE_HASH),
|
|
@@ -147,7 +147,7 @@ describe("keeping the steam avatars a roster shows", () => {
|
|
|
147
147
|
expect(steam.calls.length).toBe(1);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
it("asks only for the ids it is missing, and still answers for the cached one", async () => {
|
|
151
151
|
const steam = fakeSteam({
|
|
152
152
|
answer: (steamIds) => {
|
|
153
153
|
return steamIds.map((steamId) => {
|
|
@@ -175,7 +175,7 @@ describe("keeping the steam avatars a roster shows", () => {
|
|
|
175
175
|
expect(hashes.get(BOB)).toBe(BOB_HASH);
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
it("asks steam again once the cached hash is six hours old", async () => {
|
|
179
179
|
setSystemTime(new Date(0));
|
|
180
180
|
|
|
181
181
|
const steam = fakeSteam({
|
|
@@ -211,7 +211,7 @@ describe("keeping the steam avatars a roster shows", () => {
|
|
|
211
211
|
expect(steam.calls.length).toBe(2);
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
-
|
|
214
|
+
it("never asks steam about a player the game does not map to a steam id", async () => {
|
|
215
215
|
const steam = fakeSteam({});
|
|
216
216
|
const avatars = createSteamAvatars({
|
|
217
217
|
steamIdOf: identity,
|
|
@@ -225,7 +225,7 @@ describe("keeping the steam avatars a roster shows", () => {
|
|
|
225
225
|
expect(steam.calls).toEqual([]);
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
it("answers with nothing while the platform has no steam key", async () => {
|
|
229
229
|
const steam = fakeSteam({
|
|
230
230
|
key: null,
|
|
231
231
|
});
|
|
@@ -242,7 +242,7 @@ describe("keeping the steam avatars a roster shows", () => {
|
|
|
242
242
|
expect(steam.warnings).toEqual([]);
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
it("answers with nothing and warns once when steam refuses", async () => {
|
|
246
246
|
const steam = fakeSteam({
|
|
247
247
|
answer: () => {
|
|
248
248
|
throw new Error("429 too many requests");
|
|
@@ -266,7 +266,7 @@ describe("keeping the steam avatars a roster shows", () => {
|
|
|
266
266
|
expect(steam.warnings.length).toBe(1);
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
it("never writes the api key into the warning it logs", async () => {
|
|
270
270
|
const steam = fakeSteam({
|
|
271
271
|
key: "top-secret-key",
|
|
272
272
|
answer: () => {
|
|
@@ -284,7 +284,7 @@ describe("keeping the steam avatars a roster shows", () => {
|
|
|
284
284
|
expect(JSON.stringify(steam.warnings)).not.toContain("top-secret-key");
|
|
285
285
|
});
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
it("keeps the roster moving when steam sends a profile with no usable picture", async () => {
|
|
288
288
|
const steam = fakeSteam({
|
|
289
289
|
answer: () => [
|
|
290
290
|
{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { BridgeContext } from "../context";
|
|
3
3
|
import { appManifestPath, buildIdOf, installedBuildId, missingGameRoots } from "./steamApp";
|
|
4
4
|
|
|
@@ -24,32 +24,32 @@ const testContext = (files: Record<string, string>) => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
describe("appManifestPath", () => {
|
|
27
|
-
|
|
27
|
+
it("points at the steamapps manifest", () => {
|
|
28
28
|
expect(appManifestPath("2394010")).toBe("steamapps/appmanifest_2394010.acf");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
it("accepts another steamapps directory", () => {
|
|
32
32
|
expect(appManifestPath("90", ".steam/steamapps")).toBe(".steam/steamapps/appmanifest_90.acf");
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
describe("buildIdOf", () => {
|
|
37
|
-
|
|
37
|
+
it("reads the build id out of an acf manifest", () => {
|
|
38
38
|
expect(buildIdOf(MANIFEST)).toBe("19582821");
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
it("returns null when there is no build id", () => {
|
|
42
42
|
expect(buildIdOf('"AppState"\n{\n}\n')).toBeNull();
|
|
43
43
|
expect(buildIdOf("")).toBeNull();
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
describe("installedBuildId", () => {
|
|
48
|
-
|
|
48
|
+
it("returns null when the app was never installed", async () => {
|
|
49
49
|
expect(await installedBuildId(testContext({}), "2394010")).toBeNull();
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
it("reads the installed build id", async () => {
|
|
53
53
|
const context = testContext({
|
|
54
54
|
"steamapps/appmanifest_2394010.acf": MANIFEST,
|
|
55
55
|
});
|
|
@@ -59,7 +59,7 @@ describe("installedBuildId", () => {
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
describe("missingGameRoots", () => {
|
|
62
|
-
|
|
62
|
+
it("names only the paths that are absent", async () => {
|
|
63
63
|
const context = testContext({
|
|
64
64
|
"PalServer.sh": "",
|
|
65
65
|
Engine: "",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { steamcmdArgv, steamcmdFailureText, steamcmdScript, steamcmdUpdateArgs } from "./steamcmd";
|
|
3
3
|
import { SteamcmdLoginKind, steamcmdLoginArgs } from "./steamcmdLogin";
|
|
4
4
|
import { SteamcmdPlatform } from "./steamcmdPlatform";
|
|
@@ -9,13 +9,13 @@ const OPTIONS = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
describe("steamcmdLoginArgs", () => {
|
|
12
|
-
|
|
12
|
+
it("logs in anonymously by default", () => {
|
|
13
13
|
expect(steamcmdLoginArgs()).toEqual([
|
|
14
14
|
"anonymous",
|
|
15
15
|
]);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
it("passes the account credentials through", () => {
|
|
19
19
|
expect(
|
|
20
20
|
steamcmdLoginArgs({
|
|
21
21
|
kind: SteamcmdLoginKind.Account,
|
|
@@ -30,14 +30,14 @@ describe("steamcmdLoginArgs", () => {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
describe("steamcmdScript", () => {
|
|
33
|
-
|
|
33
|
+
it("lives inside the copied steamcmd directory", () => {
|
|
34
34
|
expect(steamcmdScript()).toBe(".steamcmd/steamcmd.sh");
|
|
35
35
|
expect(steamcmdScript(".sc")).toBe(".sc/steamcmd.sh");
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
describe("steamcmdArgv", () => {
|
|
40
|
-
|
|
40
|
+
it("forces the install directory and logs in before the command", () => {
|
|
41
41
|
expect(
|
|
42
42
|
steamcmdArgv(OPTIONS, "/home/container", [
|
|
43
43
|
"+app_update",
|
|
@@ -55,7 +55,7 @@ describe("steamcmdArgv", () => {
|
|
|
55
55
|
]);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
it("forces the windows platform before the login", () => {
|
|
59
59
|
const argv = steamcmdArgv(
|
|
60
60
|
{
|
|
61
61
|
...OPTIONS,
|
|
@@ -70,7 +70,7 @@ describe("steamcmdArgv", () => {
|
|
|
70
70
|
expect(argv.indexOf("+@sSteamCmdForcePlatformType")).toBeLessThan(argv.indexOf("+login"));
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
it("leaves the platform alone on linux", () => {
|
|
74
74
|
expect(
|
|
75
75
|
steamcmdArgv(
|
|
76
76
|
{
|
|
@@ -85,7 +85,7 @@ describe("steamcmdArgv", () => {
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
describe("steamcmdUpdateArgs", () => {
|
|
88
|
-
|
|
88
|
+
it("updates without validating by default", () => {
|
|
89
89
|
expect(
|
|
90
90
|
steamcmdUpdateArgs("258550", {
|
|
91
91
|
validate: false,
|
|
@@ -96,7 +96,7 @@ describe("steamcmdUpdateArgs", () => {
|
|
|
96
96
|
]);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
it("adds validate last of the update flags", () => {
|
|
100
100
|
expect(
|
|
101
101
|
steamcmdUpdateArgs("258550", {
|
|
102
102
|
validate: true,
|
|
@@ -108,7 +108,7 @@ describe("steamcmdUpdateArgs", () => {
|
|
|
108
108
|
]);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
it("passes a branch and its password", () => {
|
|
112
112
|
expect(
|
|
113
113
|
steamcmdUpdateArgs("90", {
|
|
114
114
|
validate: true,
|
|
@@ -126,7 +126,7 @@ describe("steamcmdUpdateArgs", () => {
|
|
|
126
126
|
]);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
it("ignores a branch password without a branch", () => {
|
|
130
130
|
expect(
|
|
131
131
|
steamcmdUpdateArgs("90", {
|
|
132
132
|
validate: false,
|
|
@@ -138,7 +138,7 @@ describe("steamcmdUpdateArgs", () => {
|
|
|
138
138
|
]);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
it("appends the extra arguments", () => {
|
|
142
142
|
expect(
|
|
143
143
|
steamcmdUpdateArgs("376030", {
|
|
144
144
|
validate: false,
|
|
@@ -157,7 +157,7 @@ describe("steamcmdUpdateArgs", () => {
|
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
describe("steamcmdFailureText", () => {
|
|
160
|
-
|
|
160
|
+
it("says the download account does not own the app when steam answers no subscription", () => {
|
|
161
161
|
expect(
|
|
162
162
|
steamcmdFailureText(
|
|
163
163
|
{
|
|
@@ -167,10 +167,10 @@ describe("steamcmdFailureText", () => {
|
|
|
167
167
|
},
|
|
168
168
|
"assetto",
|
|
169
169
|
).en,
|
|
170
|
-
).toBe("Steam refused to download assetto because the download account
|
|
170
|
+
).toBe("Steam refused to download assetto because the download account doesn't own it. Contact support.");
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
it("says the download failed for any other exit", () => {
|
|
174
174
|
expect(
|
|
175
175
|
steamcmdFailureText(
|
|
176
176
|
{
|
|
@@ -181,8 +181,8 @@ describe("steamcmdFailureText", () => {
|
|
|
181
181
|
"palworld",
|
|
182
182
|
),
|
|
183
183
|
).toEqual({
|
|
184
|
-
ar: "
|
|
185
|
-
en: "We
|
|
184
|
+
ar: "تعذّر تنزيل palworld من Steam، جرب مرة ثانية.",
|
|
185
|
+
en: "We couldn't download palworld from Steam. Try again.",
|
|
186
186
|
});
|
|
187
187
|
});
|
|
188
188
|
});
|