@serverkgg/bridge 0.0.21 → 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.
Files changed (119) hide show
  1. package/package.json +3 -1
  2. package/src/bot/botDeploy.test.ts +363 -0
  3. package/src/bot/botDeploy.ts +136 -0
  4. package/src/bot/botEnvironment.test.ts +189 -0
  5. package/src/bot/botEnvironment.ts +141 -0
  6. package/src/bot/botIdentity.test.ts +251 -0
  7. package/src/bot/botIdentity.ts +140 -0
  8. package/src/bot/botPanel.test.ts +246 -0
  9. package/src/bot/botPanel.ts +129 -0
  10. package/src/bot/botState.test.ts +45 -0
  11. package/src/bot/botState.ts +54 -0
  12. package/src/bot/botTerminal.test.ts +79 -0
  13. package/src/bot/botTerminal.ts +62 -0
  14. package/src/bot/botVariables.test.ts +193 -0
  15. package/src/bot/botVariables.ts +73 -0
  16. package/src/bot/detect/botFramework.ts +23 -0
  17. package/src/bot/detect/botFrameworks.ts +198 -0
  18. package/src/bot/detect/botInspectWarning.ts +15 -0
  19. package/src/bot/detect/botPackageManager.ts +6 -0
  20. package/src/bot/detect/botPlatform.ts +10 -0
  21. package/src/bot/detect/botRuntime.ts +15 -0
  22. package/src/bot/detect/botToken.test.ts +71 -0
  23. package/src/bot/detect/botToken.ts +126 -0
  24. package/src/bot/detect/detect.test.ts +543 -0
  25. package/src/bot/detect/detect.ts +842 -0
  26. package/src/bot/detect/detectResult.ts +40 -0
  27. package/src/bot/detect/detectTable.test.ts +72 -0
  28. package/src/bot/detect/detectTable.ts +166 -0
  29. package/src/bot/detect/envExample.test.ts +29 -0
  30. package/src/bot/detect/envExample.ts +48 -0
  31. package/src/bot/detect/envKey.ts +7 -0
  32. package/src/bot/detect/envScan.test.ts +39 -0
  33. package/src/bot/detect/envScan.ts +87 -0
  34. package/src/bot/detect/index.ts +17 -0
  35. package/src/bot/detect/inferBotPlatform.test.ts +60 -0
  36. package/src/bot/detect/inferBotPlatform.ts +28 -0
  37. package/src/bot/detect/keys.test.ts +28 -0
  38. package/src/bot/detect/keys.ts +64 -0
  39. package/src/bot/detect/sensitiveKey.test.ts +91 -0
  40. package/src/bot/detect/sensitiveKey.ts +44 -0
  41. package/src/bot/detect/serverkJson.test.ts +88 -0
  42. package/src/bot/detect/serverkJson.ts +174 -0
  43. package/src/bot/index.ts +8 -0
  44. package/src/cli/detectTable.test.ts +66 -0
  45. package/src/cli/detectTable.ts +43 -0
  46. package/src/cli/main.ts +12 -4
  47. package/src/cli/renderedFile.ts +35 -0
  48. package/src/cli/schema.ts +7 -26
  49. package/src/cli/validate.test.ts +283 -3
  50. package/src/cli/validate.ts +105 -7
  51. package/src/context/bridgeContext.ts +17 -1
  52. package/src/core/bridgeAction.ts +2 -0
  53. package/src/core/bridgeActivity.ts +9 -0
  54. package/src/core/bridgeAvailability.ts +10 -0
  55. package/src/core/bridgePort.test.ts +45 -0
  56. package/src/core/bridgePort.ts +26 -0
  57. package/src/core/bridgeUserError.test.ts +10 -0
  58. package/src/core/bridgeUserError.ts +6 -2
  59. package/src/core/describeDownloadFailure.test.ts +58 -0
  60. package/src/core/describeDownloadFailure.ts +69 -0
  61. package/src/core/index.ts +3 -0
  62. package/src/guides/compile.ts +2 -2
  63. package/src/manifest/index.ts +1 -0
  64. package/src/manifest/kind.ts +5 -0
  65. package/src/manifest/manifest.test.ts +630 -3
  66. package/src/manifest/manifest.ts +391 -87
  67. package/src/manifest/runtime.ts +2 -2
  68. package/src/manifest/source.ts +4 -4
  69. package/src/modules/bridge.ts +8 -1
  70. package/src/modules/bridgeLifecycleSubject.ts +8 -0
  71. package/src/modules/bridgeModule.ts +19 -1
  72. package/src/modules/bridgeStopSignal.ts +13 -0
  73. package/src/modules/index.ts +2 -0
  74. package/src/panel/bridgeDetail.ts +4 -1
  75. package/src/protocol/bridgeActivity.ts +54 -0
  76. package/src/protocol/bridgeConfig.ts +2 -0
  77. package/src/protocol/bridgeEvents.ts +30 -0
  78. package/src/protocol/bridgeFrame.ts +10 -1
  79. package/src/protocol/index.ts +1 -0
  80. package/src/runtime/bridgeActions.ts +19 -2
  81. package/src/runtime/bridgeActivity.test.ts +318 -0
  82. package/src/runtime/bridgeActivity.ts +268 -0
  83. package/src/runtime/bridgeConfigReader.test.ts +80 -0
  84. package/src/runtime/bridgeConfigReader.ts +20 -0
  85. package/src/runtime/bridgeContext.test.ts +247 -0
  86. package/src/runtime/bridgeContext.ts +43 -10
  87. package/src/runtime/bridgeDriver.test.ts +153 -1
  88. package/src/runtime/bridgeDriver.ts +64 -8
  89. package/src/runtime/bridgeFiles.test.ts +62 -0
  90. package/src/runtime/bridgeFiles.ts +12 -2
  91. package/src/runtime/bridgeLiveConfig.ts +18 -0
  92. package/src/runtime/bridgeLog.ts +10 -0
  93. package/src/runtime/bridgeRedaction.test.ts +410 -0
  94. package/src/runtime/bridgeRedaction.ts +214 -0
  95. package/src/runtime/bridgeRefusalError.ts +13 -0
  96. package/src/runtime/bridgeRegistry.test.ts +1056 -10
  97. package/src/runtime/bridgeRegistry.ts +464 -50
  98. package/src/runtime/bridgeScopedContext.ts +55 -0
  99. package/src/runtime/bridgeServer.test.ts +534 -4
  100. package/src/runtime/bridgeServer.ts +145 -38
  101. package/src/runtime/bridgeSetupHandlers.test.ts +156 -5
  102. package/src/runtime/bridgeSetupHandlers.ts +59 -14
  103. package/src/runtime/bridgeSupervisor.test.ts +670 -2
  104. package/src/runtime/bridgeSupervisor.ts +219 -67
  105. package/src/runtime/serve.ts +87 -11
  106. package/src/steam/steamcmd.test.ts +32 -1
  107. package/src/steam/steamcmd.ts +27 -4
  108. package/src/utils/changedSettingKeys.test.ts +65 -0
  109. package/src/utils/changedSettingKeys.ts +6 -0
  110. package/src/utils/describeExecFailure.test.ts +34 -0
  111. package/src/utils/describeExecFailure.ts +9 -0
  112. package/src/utils/index.ts +3 -0
  113. package/src/utils/sameSettingValue.test.ts +35 -0
  114. package/src/utils/sameSettingValue.ts +40 -0
  115. package/src/wire/activity.test.ts +95 -0
  116. package/src/wire/activity.ts +41 -0
  117. package/src/wire/index.ts +1 -0
  118. package/src/wire/panel.test.ts +67 -1
  119. package/src/wire/panel.ts +11 -0
@@ -0,0 +1,268 @@
1
+ import type { BridgeActivityReporter } from "../context";
2
+ import { type BridgeActivity, type BridgeValue, type BridgeValues, isLocalizedText } from "../core";
3
+ import {
4
+ activityPayload,
5
+ BRIDGE_ACTIVITY_DETAIL_KEY_LENGTH_LIMIT,
6
+ BRIDGE_ACTIVITY_DETAIL_KEY_PATTERN,
7
+ BRIDGE_ACTIVITY_DETAILS_LIMIT,
8
+ BRIDGE_ACTIVITY_KEYS,
9
+ BRIDGE_ACTIVITY_LIMIT,
10
+ BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT,
11
+ BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT,
12
+ isBridgeEventName,
13
+ } from "../protocol";
14
+ import { type BridgeLogger, errorText } from "./bridgeLog";
15
+ import type { BridgeRedact } from "./bridgeRedaction";
16
+
17
+ export interface ActivityNormalizeOptions {
18
+ secretKeys: ReadonlySet<string>;
19
+ redact: BridgeRedact;
20
+ warn: BridgeLogger;
21
+ }
22
+
23
+ export interface ActivityReporterOptions extends ActivityNormalizeOptions {
24
+ onEmit: (event: string, payload: BridgeValues) => void;
25
+ }
26
+
27
+ export interface BridgeActivityAnswer<Result> {
28
+ result: Result;
29
+ activity: BridgeActivity[];
30
+ }
31
+
32
+ export interface BridgeActivityScope {
33
+ readonly reporter: BridgeActivityReporter;
34
+ readonly reported: number;
35
+ close(): BridgeActivity[];
36
+ drain(): void;
37
+ }
38
+
39
+ const RESERVED_KEYS: ReadonlySet<string> = new Set(Object.values(BRIDGE_ACTIVITY_KEYS));
40
+
41
+ const cut = (value: string, limit: number, reason: string, report: (reason: string) => void) => {
42
+ if (value.length <= limit) {
43
+ return value;
44
+ }
45
+
46
+ report(`long-${reason}`);
47
+
48
+ return value.slice(0, limit);
49
+ };
50
+
51
+ const isScalar = (value: unknown): value is BridgeValue => {
52
+ return (
53
+ value === null
54
+ || typeof value === "boolean"
55
+ || typeof value === "string"
56
+ || (typeof value === "number" && Number.isFinite(value))
57
+ );
58
+ };
59
+
60
+ const normalizeDetails = (
61
+ details: unknown,
62
+ options: ActivityNormalizeOptions,
63
+ report: (reason: string) => void,
64
+ ): BridgeValues | null => {
65
+ if (details === undefined || details === null) {
66
+ return null;
67
+ }
68
+
69
+ if (typeof details !== "object" || Array.isArray(details)) {
70
+ report("unusable-details");
71
+
72
+ return null;
73
+ }
74
+
75
+ const kept: BridgeValues = {};
76
+
77
+ let count = 0;
78
+
79
+ for (const [key, value] of Object.entries(details)) {
80
+ if (RESERVED_KEYS.has(key)) {
81
+ report("reserved-detail");
82
+
83
+ continue;
84
+ }
85
+
86
+ if (options.secretKeys.has(key)) {
87
+ report("secret-detail");
88
+
89
+ continue;
90
+ }
91
+
92
+ if (key.length > BRIDGE_ACTIVITY_DETAIL_KEY_LENGTH_LIMIT || !BRIDGE_ACTIVITY_DETAIL_KEY_PATTERN.test(key)) {
93
+ report("unusable-detail-key");
94
+
95
+ continue;
96
+ }
97
+
98
+ if (!isScalar(value)) {
99
+ report("unusable-detail-value");
100
+
101
+ continue;
102
+ }
103
+
104
+ if (count >= BRIDGE_ACTIVITY_DETAILS_LIMIT) {
105
+ report("extra-details");
106
+
107
+ break;
108
+ }
109
+
110
+ kept[key] =
111
+ typeof value === "string"
112
+ ? cut(options.redact(value), BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT, "detail", report)
113
+ : value;
114
+ count += 1;
115
+ }
116
+
117
+ return count > 0 ? kept : null;
118
+ };
119
+
120
+ export const normalizeActivity = (entry: BridgeActivity, options: ActivityNormalizeOptions): BridgeActivity | null => {
121
+ const name = String(entry.name);
122
+ const reported = new Set<string>();
123
+
124
+ const report = (reason: string) => {
125
+ if (reported.has(reason)) {
126
+ return;
127
+ }
128
+
129
+ reported.add(reason);
130
+
131
+ options.warn("activity entry normalized", {
132
+ name,
133
+ reason,
134
+ });
135
+ };
136
+
137
+ if (!isBridgeEventName(name)) {
138
+ report("unknown-name");
139
+
140
+ return null;
141
+ }
142
+
143
+ if (!isLocalizedText(entry.text)) {
144
+ report("unusable-text");
145
+
146
+ return null;
147
+ }
148
+
149
+ const ar = cut(options.redact(entry.text.ar).trim(), BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT, "text", report);
150
+ const en = cut(options.redact(entry.text.en).trim(), BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT, "text", report);
151
+
152
+ if (ar.length === 0 || en.length === 0) {
153
+ report("empty-text");
154
+
155
+ return null;
156
+ }
157
+
158
+ const subject =
159
+ typeof entry.subject === "string"
160
+ ? cut(options.redact(entry.subject).trim(), BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT, "subject", report)
161
+ : "";
162
+
163
+ if (entry.subject !== undefined && subject.length === 0) {
164
+ report("empty-subject");
165
+ }
166
+
167
+ const details = normalizeDetails(entry.details, options, report);
168
+
169
+ return {
170
+ name: entry.name,
171
+ text: {
172
+ ar,
173
+ en,
174
+ },
175
+ ...(subject.length > 0
176
+ ? {
177
+ subject,
178
+ }
179
+ : {}),
180
+ ...(details
181
+ ? {
182
+ details,
183
+ }
184
+ : {}),
185
+ };
186
+ };
187
+
188
+ const safely = (options: ActivityNormalizeOptions, run: () => void) => {
189
+ try {
190
+ run();
191
+ } catch (error) {
192
+ options.warn("activity report failed", {
193
+ error: errorText(error),
194
+ });
195
+ }
196
+ };
197
+
198
+ export const createActivityReporter = (options: ActivityReporterOptions): BridgeActivityReporter => {
199
+ return {
200
+ report(entry) {
201
+ safely(options, () => {
202
+ const normalized = normalizeActivity(entry, options);
203
+
204
+ if (normalized) {
205
+ options.onEmit(normalized.name, activityPayload(normalized));
206
+ }
207
+ });
208
+ },
209
+ };
210
+ };
211
+
212
+ export const createActivityScope = (
213
+ fallback: BridgeActivityReporter,
214
+ options: ActivityNormalizeOptions,
215
+ ): BridgeActivityScope => {
216
+ const entries: BridgeActivity[] = [];
217
+
218
+ let open = true;
219
+
220
+ return {
221
+ reporter: {
222
+ report(entry) {
223
+ safely(options, () => {
224
+ if (!open) {
225
+ fallback.report(entry);
226
+
227
+ return;
228
+ }
229
+
230
+ const normalized = normalizeActivity(entry, options);
231
+
232
+ if (!normalized) {
233
+ return;
234
+ }
235
+
236
+ if (entries.length >= BRIDGE_ACTIVITY_LIMIT) {
237
+ options.warn("activity entry dropped", {
238
+ name: normalized.name,
239
+ reason: "extra-entries",
240
+ });
241
+
242
+ return;
243
+ }
244
+
245
+ entries.push(normalized);
246
+ });
247
+ },
248
+ },
249
+
250
+ get reported() {
251
+ return entries.length;
252
+ },
253
+
254
+ close() {
255
+ open = false;
256
+
257
+ return entries.splice(0);
258
+ },
259
+
260
+ drain() {
261
+ open = false;
262
+
263
+ for (const entry of entries.splice(0)) {
264
+ fallback.report(entry);
265
+ }
266
+ },
267
+ };
268
+ };
@@ -0,0 +1,80 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BridgeConfigError, parseConfig } from "./bridgeConfigReader";
3
+
4
+ const written = (extra: Record<string, unknown> = {}) => {
5
+ return JSON.stringify({
6
+ code: "abcdef",
7
+ token: "token",
8
+ cwd: "/home/game",
9
+ run: "/run/serverk",
10
+ ports: {
11
+ game: 19_132,
12
+ media: 20_000,
13
+ },
14
+ memoryMb: 2048,
15
+ cpuPercent: 100,
16
+ gameUid: 1000,
17
+ gameGid: 1000,
18
+ variables: {},
19
+ secrets: {},
20
+ ...extra,
21
+ });
22
+ };
23
+
24
+ describe("parseConfig", () => {
25
+ test("reads a file an older agent wrote, with no public address and no port counts", () => {
26
+ const config = parseConfig(written());
27
+
28
+ expect(config.publicAddress).toBeUndefined();
29
+ expect(config.portCounts).toBeUndefined();
30
+ expect(config.ports).toEqual({
31
+ game: 19_132,
32
+ media: 20_000,
33
+ });
34
+ });
35
+
36
+ test("reads the public address and the port counts when the agent wrote them", () => {
37
+ const config = parseConfig(
38
+ written({
39
+ publicAddress: "203.0.113.7",
40
+ portCounts: {
41
+ game: 1,
42
+ media: 32,
43
+ },
44
+ }),
45
+ );
46
+
47
+ expect(config.publicAddress).toBe("203.0.113.7");
48
+ expect(config.portCounts).toEqual({
49
+ game: 1,
50
+ media: 32,
51
+ });
52
+ });
53
+
54
+ test("ignores a malformed public address and drops non-numeric counts", () => {
55
+ const config = parseConfig(
56
+ written({
57
+ publicAddress: 7,
58
+ portCounts: {
59
+ media: "32",
60
+ game: 1,
61
+ },
62
+ }),
63
+ );
64
+
65
+ expect(config.publicAddress).toBeUndefined();
66
+ expect(config.portCounts).toEqual({
67
+ game: 1,
68
+ });
69
+ });
70
+
71
+ test("still refuses a game that would run as root", () => {
72
+ expect(() =>
73
+ parseConfig(
74
+ written({
75
+ gameUid: 0,
76
+ }),
77
+ ),
78
+ ).toThrow(BridgeConfigError);
79
+ });
80
+ });
@@ -70,6 +70,24 @@ const stringRecord = (source: Record<string, unknown>, key: string) => {
70
70
  return out;
71
71
  };
72
72
 
73
+ const optionalString = (source: Record<string, unknown>, key: string) => {
74
+ const value = source[key];
75
+
76
+ if (typeof value !== "string" || value.length === 0) {
77
+ return;
78
+ }
79
+
80
+ return value;
81
+ };
82
+
83
+ const optionalNumberRecord = (source: Record<string, unknown>, key: string) => {
84
+ if (source[key] === undefined) {
85
+ return;
86
+ }
87
+
88
+ return numberRecord(source, key);
89
+ };
90
+
73
91
  export const configPath = () => {
74
92
  return `${BRIDGE_RUN_MOUNT}/${BRIDGE_CONFIG_NAME}`;
75
93
  };
@@ -97,6 +115,8 @@ export const parseConfig = (text: string): BridgeConfig => {
97
115
  cwd: requireString(source, "cwd"),
98
116
  run: requireString(source, "run"),
99
117
  ports: numberRecord(source, "ports"),
118
+ portCounts: optionalNumberRecord(source, "portCounts"),
119
+ publicAddress: optionalString(source, "publicAddress"),
100
120
  memoryMb: requireNumber(source, "memoryMb"),
101
121
  cpuPercent: requireNumber(source, "cpuPercent"),
102
122
  gameUid: requireNumber(source, "gameUid"),
@@ -0,0 +1,247 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { BridgeConfig } from "../protocol";
6
+ import { createContext } from "./bridgeContext";
7
+ import { createBridgeLiveConfig } from "./bridgeLiveConfig";
8
+ import { log } from "./bridgeLog";
9
+ import { createSupervisor } from "./bridgeSupervisor";
10
+
11
+ describe("the context's variable maps", () => {
12
+ let directory = "";
13
+ let path = "";
14
+
15
+ const configWith = (variables: Record<string, string>, secrets: Record<string, string>): BridgeConfig => ({
16
+ code: "abcdef",
17
+ token: "token",
18
+ cwd: directory,
19
+ run: join(directory, "run"),
20
+ ports: {},
21
+ memoryMb: 512,
22
+ cpuPercent: 100,
23
+ gameUid: 1000,
24
+ gameGid: 1000,
25
+ variables,
26
+ secrets,
27
+ });
28
+
29
+ beforeEach(async () => {
30
+ directory = await mkdtemp(join(tmpdir(), "bridge-context-"));
31
+ path = join(directory, "config.json");
32
+ });
33
+
34
+ afterEach(async () => {
35
+ await rm(directory, {
36
+ force: true,
37
+ recursive: true,
38
+ });
39
+ });
40
+
41
+ test("hand a driver every variable and secret the agent wrote, and follow a rewrite", async () => {
42
+ const boot = configWith(
43
+ {
44
+ MY_VAR: "1",
45
+ },
46
+ {
47
+ DISCORD_TOKEN: "sealed",
48
+ },
49
+ );
50
+
51
+ await writeFile(path, JSON.stringify(boot));
52
+
53
+ const live = createBridgeLiveConfig(path, boot, 60_000);
54
+ const supervisor = createSupervisor({
55
+ cwd: directory,
56
+ uid: 1000,
57
+ gid: 1000,
58
+ stopTimeoutSeconds: 1,
59
+ onLog: log,
60
+ resolveCommand: async () => [
61
+ "run",
62
+ ],
63
+ });
64
+ const context = createContext(
65
+ boot,
66
+ supervisor,
67
+ {
68
+ onLog: log,
69
+ onEmit() {
70
+ return;
71
+ },
72
+ onSetup() {
73
+ return;
74
+ },
75
+ onActivity() {
76
+ return;
77
+ },
78
+ async onRestart() {
79
+ return;
80
+ },
81
+ },
82
+ live,
83
+ );
84
+
85
+ expect(context.variables()).toEqual({
86
+ MY_VAR: "1",
87
+ });
88
+ expect(context.secrets()).toEqual({
89
+ DISCORD_TOKEN: "sealed",
90
+ });
91
+
92
+ await writeFile(
93
+ path,
94
+ JSON.stringify(
95
+ configWith(
96
+ {
97
+ MY_VAR: "2",
98
+ LOG_LEVEL: "debug",
99
+ },
100
+ {},
101
+ ),
102
+ ),
103
+ );
104
+
105
+ live.reload();
106
+
107
+ expect(context.variables()).toEqual({
108
+ MY_VAR: "2",
109
+ LOG_LEVEL: "debug",
110
+ });
111
+ expect(context.secrets()).toEqual({});
112
+ expect(context.variable("LOG_LEVEL")).toBe("debug");
113
+ });
114
+ });
115
+
116
+ describe("the context's ports and address", () => {
117
+ let directory = "";
118
+ let path = "";
119
+
120
+ const configWith = (extra: Partial<BridgeConfig>): BridgeConfig => ({
121
+ code: "abcdef",
122
+ token: "token",
123
+ cwd: directory,
124
+ run: join(directory, "run"),
125
+ ports: {
126
+ game: 19_140,
127
+ media: 20_064,
128
+ },
129
+ memoryMb: 512,
130
+ cpuPercent: 100,
131
+ gameUid: 1000,
132
+ gameGid: 1000,
133
+ variables: {},
134
+ secrets: {},
135
+ ...extra,
136
+ });
137
+
138
+ const contextOf = (boot: BridgeConfig) => {
139
+ const live = createBridgeLiveConfig(path, boot, 60_000);
140
+ const supervisor = createSupervisor({
141
+ cwd: directory,
142
+ uid: 1000,
143
+ gid: 1000,
144
+ stopTimeoutSeconds: 1,
145
+ onLog: log,
146
+ resolveCommand: async () => [
147
+ "run",
148
+ ],
149
+ });
150
+
151
+ return {
152
+ live,
153
+ context: createContext(
154
+ boot,
155
+ supervisor,
156
+ {
157
+ onLog: log,
158
+ onEmit() {
159
+ return;
160
+ },
161
+ onSetup() {
162
+ return;
163
+ },
164
+ onActivity() {
165
+ return;
166
+ },
167
+ async onRestart() {
168
+ return;
169
+ },
170
+ },
171
+ live,
172
+ ),
173
+ };
174
+ };
175
+
176
+ beforeEach(async () => {
177
+ directory = await mkdtemp(join(tmpdir(), "bridge-context-ports-"));
178
+ path = join(directory, "config.json");
179
+ });
180
+
181
+ afterEach(async () => {
182
+ await rm(directory, {
183
+ force: true,
184
+ recursive: true,
185
+ });
186
+ });
187
+
188
+ test("hand a driver the block the agent published and its public address", async () => {
189
+ const boot = configWith({
190
+ portCounts: {
191
+ game: 1,
192
+ media: 32,
193
+ },
194
+ publicAddress: "203.0.113.7",
195
+ });
196
+
197
+ await writeFile(path, JSON.stringify(boot));
198
+
199
+ const { context } = contextOf(boot);
200
+
201
+ expect(context.portBlock("media")).toEqual({
202
+ first: 20_064,
203
+ count: 32,
204
+ });
205
+ expect(context.portBlock("game")).toEqual({
206
+ first: 19_140,
207
+ count: 1,
208
+ });
209
+ expect(context.server.publicAddress).toBe("203.0.113.7");
210
+ });
211
+
212
+ test("treat every port as a single one and the address as unknown for a file an older agent wrote", async () => {
213
+ const boot = configWith({});
214
+
215
+ await writeFile(path, JSON.stringify(boot));
216
+
217
+ const { context } = contextOf(boot);
218
+
219
+ expect(context.portBlock("media")).toEqual({
220
+ first: 20_064,
221
+ count: 1,
222
+ });
223
+ expect(context.server.publicAddress).toBeNull();
224
+ expect(() => context.portBlock("query")).toThrow('this game declares no port named "query"');
225
+ });
226
+
227
+ test("follow a rewrite of the public address", async () => {
228
+ const boot = configWith({});
229
+
230
+ await writeFile(path, JSON.stringify(boot));
231
+
232
+ const { context, live } = contextOf(boot);
233
+
234
+ await writeFile(
235
+ path,
236
+ JSON.stringify(
237
+ configWith({
238
+ publicAddress: "198.51.100.4",
239
+ }),
240
+ ),
241
+ );
242
+
243
+ live.reload();
244
+
245
+ expect(context.server.publicAddress).toBe("198.51.100.4");
246
+ });
247
+ });