@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,126 @@
1
+ import { BotPlatform } from "./botPlatform";
2
+
3
+ export const BOT_TOKEN_KEYS: Readonly<Record<BotPlatform, readonly string[]>> = {
4
+ [BotPlatform.Discord]: [
5
+ "DISCORD_TOKEN",
6
+ "DISCORD_BOT_TOKEN",
7
+ "BOT_TOKEN",
8
+ "TOKEN",
9
+ ],
10
+ [BotPlatform.Telegram]: [
11
+ "TELEGRAM_TOKEN",
12
+ "TELEGRAM_BOT_TOKEN",
13
+ "BOT_TOKEN",
14
+ "TOKEN",
15
+ ],
16
+ };
17
+
18
+ const TELEGRAM_TOKEN = /^(?<id>\d{1,20}):[A-Za-z0-9_-]{20,128}$/;
19
+
20
+ const DISCORD_TOKEN = /^(?<id>[A-Za-z0-9_-]{16,40})\.[A-Za-z0-9_-]{4,16}\.[A-Za-z0-9_-]{20,128}$/;
21
+
22
+ const SNOWFLAKE = /^\d{17,20}$/;
23
+
24
+ const PLATFORM_KEY_HINTS: readonly [
25
+ RegExp,
26
+ BotPlatform,
27
+ ][] = [
28
+ [
29
+ /DISCORD/i,
30
+ BotPlatform.Discord,
31
+ ],
32
+ [
33
+ /TELEGRAM/i,
34
+ BotPlatform.Telegram,
35
+ ],
36
+ ];
37
+
38
+ const decodeBase64Url = (segment: string) => {
39
+ const standard = segment.replaceAll("-", "+").replaceAll("_", "/");
40
+ const padded = standard.padEnd(standard.length + ((4 - (standard.length % 4)) % 4), "=");
41
+
42
+ try {
43
+ return atob(padded);
44
+ } catch {
45
+ return null;
46
+ }
47
+ };
48
+
49
+ export const telegramBotId = (token: string): string | null => {
50
+ return TELEGRAM_TOKEN.exec(token.trim())?.groups?.id ?? null;
51
+ };
52
+
53
+ export const discordApplicationId = (token: string): string | null => {
54
+ const segment = DISCORD_TOKEN.exec(token.trim())?.groups?.id;
55
+
56
+ if (!segment) {
57
+ return null;
58
+ }
59
+
60
+ const decoded = decodeBase64Url(segment);
61
+
62
+ return decoded !== null && SNOWFLAKE.test(decoded) ? decoded : null;
63
+ };
64
+
65
+ export const platformOfToken = (value: string): BotPlatform | null => {
66
+ if (telegramBotId(value) !== null) {
67
+ return BotPlatform.Telegram;
68
+ }
69
+
70
+ if (discordApplicationId(value) !== null) {
71
+ return BotPlatform.Discord;
72
+ }
73
+
74
+ return null;
75
+ };
76
+
77
+ export const platformOfKey = (key: string): BotPlatform | null => {
78
+ for (const [pattern, platform] of PLATFORM_KEY_HINTS) {
79
+ if (pattern.test(key)) {
80
+ return platform;
81
+ }
82
+ }
83
+
84
+ return null;
85
+ };
86
+
87
+ export interface BotEnvironmentMaps {
88
+ variables: Readonly<Record<string, string>>;
89
+ secrets: Readonly<Record<string, string>>;
90
+ }
91
+
92
+ export interface BotTokenMatch {
93
+ key: string;
94
+ value: string;
95
+ }
96
+
97
+ const entriesOf = (maps: BotEnvironmentMaps) => {
98
+ return [
99
+ ...Object.entries(maps.secrets),
100
+ ...Object.entries(maps.variables),
101
+ ];
102
+ };
103
+
104
+ export const findBotToken = (platform: BotPlatform, maps: BotEnvironmentMaps): BotTokenMatch | null => {
105
+ const entries = entriesOf(maps).filter(([, value]) => platformOfToken(value) === platform);
106
+
107
+ for (const name of BOT_TOKEN_KEYS[platform]) {
108
+ const named = entries.find(([key]) => key === name);
109
+
110
+ if (named) {
111
+ return {
112
+ key: named[0],
113
+ value: named[1].trim(),
114
+ };
115
+ }
116
+ }
117
+
118
+ const first = entries[0];
119
+
120
+ return first
121
+ ? {
122
+ key: first[0],
123
+ value: first[1].trim(),
124
+ }
125
+ : null;
126
+ };
@@ -0,0 +1,543 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BotFramework } from "./botFramework";
3
+ import { BotInspectWarning } from "./botInspectWarning";
4
+ import { BotPackageManager } from "./botPackageManager";
5
+ import { BotPlatform } from "./botPlatform";
6
+ import { BotRuntime } from "./botRuntime";
7
+ import {
8
+ compareCodePoints,
9
+ DETECT_SOURCE_FILE_LIMIT,
10
+ DETECT_SOURCE_MAX_BYTES,
11
+ detect,
12
+ detectWantedFiles,
13
+ parsePyprojectDependencies,
14
+ parseRequirements,
15
+ } from "./detect";
16
+ import type { DetectResult } from "./detectResult";
17
+
18
+ const project = (contents: Record<string, string>, extra: string[] = []) => {
19
+ return detect({
20
+ files: [
21
+ ...Object.keys(contents),
22
+ ...extra,
23
+ ].map((path) => ({
24
+ path,
25
+ })),
26
+ contents,
27
+ });
28
+ };
29
+
30
+ const DISCORD_NODE = {
31
+ "package.json": JSON.stringify({
32
+ name: "discord-bot",
33
+ type: "module",
34
+ main: "index.js",
35
+ scripts: {
36
+ start: "node index.js",
37
+ },
38
+ dependencies: {
39
+ "discord.js": "^14.27.0",
40
+ },
41
+ }),
42
+ "index.js":
43
+ 'const token = process.env.DISCORD_TOKEN;\nawait client.login(token);\nimport { Client } from "discord.js";',
44
+ ".env.example": "DISCORD_TOKEN=\n",
45
+ };
46
+
47
+ const TELEGRAM_PYTHON = {
48
+ "requirements.txt": "httpx==0.28.1\npython-telegram-bot==21.11.1\n",
49
+ "main.py":
50
+ 'import os\nfrom telegram.ext import Application\napp = Application.builder().token(os.environ["TELEGRAM_TOKEN"]).build()\n',
51
+ ".env.example": "TELEGRAM_TOKEN=\n",
52
+ };
53
+
54
+ describe("detect on the templates", () => {
55
+ test("reads the discord.js template end to end", () => {
56
+ expect(
57
+ project(DISCORD_NODE, [
58
+ "package-lock.json",
59
+ "README.md",
60
+ ]),
61
+ ).toEqual({
62
+ runtime: BotRuntime.Node,
63
+ package: "bot-node",
64
+ packageManager: BotPackageManager.Npm,
65
+ framework: BotFramework.DiscordJs,
66
+ platform: BotPlatform.Discord,
67
+ install: {
68
+ command: "npm ci",
69
+ source: "package-lock.json",
70
+ },
71
+ start: {
72
+ command: "node index.js",
73
+ source: "package.json",
74
+ },
75
+ env: [
76
+ {
77
+ key: "DISCORD_TOKEN",
78
+ sensitive: true,
79
+ source: ".env.example",
80
+ example: null,
81
+ },
82
+ ],
83
+ tokenKey: "DISCORD_TOKEN",
84
+ warnings: [],
85
+ } satisfies DetectResult);
86
+ });
87
+
88
+ test("reads the python-telegram-bot template end to end", () => {
89
+ expect(project(TELEGRAM_PYTHON)).toEqual({
90
+ runtime: BotRuntime.Python,
91
+ package: "bot-python",
92
+ packageManager: BotPackageManager.Pip,
93
+ framework: BotFramework.PythonTelegramBot,
94
+ platform: BotPlatform.Telegram,
95
+ install: {
96
+ command: "pip install -r requirements.txt",
97
+ source: "requirements.txt",
98
+ },
99
+ start: {
100
+ command: "python main.py",
101
+ source: "main.py",
102
+ },
103
+ env: [
104
+ {
105
+ key: "TELEGRAM_TOKEN",
106
+ sensitive: true,
107
+ source: ".env.example",
108
+ example: null,
109
+ },
110
+ ],
111
+ tokenKey: "TELEGRAM_TOKEN",
112
+ warnings: [],
113
+ } satisfies DetectResult);
114
+ });
115
+
116
+ test("reads the lockfile a pnpm or yarn project ships", () => {
117
+ expect(
118
+ project(DISCORD_NODE, [
119
+ "pnpm-lock.yaml",
120
+ ]).install,
121
+ ).toEqual({
122
+ command: "corepack pnpm install --frozen-lockfile",
123
+ source: "pnpm-lock.yaml",
124
+ });
125
+ expect(
126
+ project(DISCORD_NODE, [
127
+ "yarn.lock",
128
+ ]).packageManager,
129
+ ).toBe(BotPackageManager.Yarn);
130
+ });
131
+
132
+ test("installs a pyproject project with pip", () => {
133
+ const result = project({
134
+ "pyproject.toml": '[project]\nname = "bot"\ndependencies = [\n "discord.py>=2.4",\n]\n',
135
+ "bot.py": 'import discord\nimport os\nos.getenv("DISCORD_TOKEN")\n',
136
+ });
137
+
138
+ expect(result.install).toEqual({
139
+ command: "pip install .",
140
+ source: "pyproject.toml",
141
+ });
142
+ expect(result.framework).toBe(BotFramework.DiscordPy);
143
+ expect(result.start).toEqual({
144
+ command: "python bot.py",
145
+ source: "bot.py",
146
+ });
147
+ });
148
+ });
149
+
150
+ describe("detect start commands", () => {
151
+ test("follows the launcher's order: start script, main, known files, then the Procfile", () => {
152
+ expect(
153
+ project({
154
+ "package.json": JSON.stringify({
155
+ main: "src/app.js",
156
+ }),
157
+ "src/app.js": "",
158
+ "index.js": "",
159
+ }).start,
160
+ ).toEqual({
161
+ command: "node src/app.js",
162
+ source: "package.json",
163
+ });
164
+ expect(
165
+ project({
166
+ "package.json": "{}",
167
+ "src/bot.js": "",
168
+ }).start,
169
+ ).toEqual({
170
+ command: "node src/bot.js",
171
+ source: "src/bot.js",
172
+ });
173
+ expect(
174
+ project({
175
+ "package.json": "{}",
176
+ Procfile: "web: node server.js\nworker: node worker.js\n",
177
+ }).start,
178
+ ).toEqual({
179
+ command: "node worker.js",
180
+ source: "Procfile",
181
+ });
182
+ });
183
+
184
+ test("warns when nothing starts and when node would run TypeScript directly", () => {
185
+ expect(
186
+ project({
187
+ "package.json": "{}",
188
+ }).warnings,
189
+ ).toContain(BotInspectWarning.NoStart);
190
+ expect(
191
+ project({
192
+ "package.json": "{}",
193
+ "index.ts": "",
194
+ }).warnings,
195
+ ).toContain(BotInspectWarning.TypescriptEntry);
196
+ expect(
197
+ project({
198
+ "package.json": JSON.stringify({
199
+ scripts: {
200
+ start: "tsx src/index.ts",
201
+ },
202
+ }),
203
+ }).warnings,
204
+ ).not.toContain(BotInspectWarning.TypescriptEntry);
205
+ });
206
+ });
207
+
208
+ describe("detect warnings", () => {
209
+ test("flags a missing lockfile, loose requirements and a missing manifest", () => {
210
+ expect(
211
+ project({
212
+ "package.json": "{}",
213
+ "index.js": "",
214
+ }).warnings,
215
+ ).toEqual([
216
+ BotInspectWarning.NoLockfile,
217
+ ]);
218
+ expect(
219
+ project({
220
+ "requirements.txt": "discord.py\naiohttp>=3\n",
221
+ "main.py": "",
222
+ }).warnings,
223
+ ).toEqual([
224
+ BotInspectWarning.UnpinnedRequirements,
225
+ ]);
226
+ expect(
227
+ project({
228
+ "main.py": "import discord",
229
+ }).warnings,
230
+ ).toEqual([
231
+ BotInspectWarning.NoManifest,
232
+ ]);
233
+ expect(
234
+ project({
235
+ "README.md": "",
236
+ }).warnings,
237
+ ).toEqual([
238
+ BotInspectWarning.NoStart,
239
+ BotInspectWarning.NoManifest,
240
+ ]);
241
+ });
242
+
243
+ test("flags committed secrets and dependency folders but not the templates for them", () => {
244
+ expect(
245
+ project(DISCORD_NODE, [
246
+ "package-lock.json",
247
+ ".env",
248
+ "config/.env.production",
249
+ "node_modules/discord.js/package.json",
250
+ "node_modules/some/.env",
251
+ ".venv/pyvenv.cfg",
252
+ ".env.sample",
253
+ ]).warnings,
254
+ ).toEqual([
255
+ BotInspectWarning.EnvCommitted,
256
+ BotInspectWarning.NodeModulesCommitted,
257
+ BotInspectWarning.VenvCommitted,
258
+ ]);
259
+ });
260
+
261
+ test("flags two runtimes and picks the one whose framework it found", () => {
262
+ const result = project({
263
+ ...DISCORD_NODE,
264
+ "requirements.txt": "requests==2.32.0\n",
265
+ });
266
+
267
+ expect(result.runtime).toBe(BotRuntime.Node);
268
+ expect(result.warnings).toContain(BotInspectWarning.TwoRuntimes);
269
+ });
270
+
271
+ test("flags two platforms and leaves the choice to the customer", () => {
272
+ const result = project({
273
+ "package.json": JSON.stringify({
274
+ dependencies: {
275
+ "discord.js": "14",
276
+ grammy: "1",
277
+ },
278
+ }),
279
+ "index.js": "",
280
+ });
281
+
282
+ expect(result.platform).toBeNull();
283
+ expect(result.framework).toBeNull();
284
+ expect(result.warnings).toContain(BotInspectWarning.TwoPlatforms);
285
+ });
286
+
287
+ test("drops keys the platform owns and warns, but keeps quiet about keys the runtime gives anyway", () => {
288
+ const result = project({
289
+ ...DISCORD_NODE,
290
+ "index.js": "process.env.REPO_URL; process.env.HOME; process.env.SERVERK_BOT; process.env.GUILD_ID;",
291
+ });
292
+
293
+ expect(result.env.map((entry) => entry.key)).toEqual([
294
+ "DISCORD_TOKEN",
295
+ "GUILD_ID",
296
+ ]);
297
+ expect(result.warnings).toContain(BotInspectWarning.ReservedEnvKey);
298
+
299
+ const quiet = project({
300
+ ...DISCORD_NODE,
301
+ "index.js": "process.env.PATH; process.env.BOT_PLATFORM;",
302
+ });
303
+
304
+ expect(quiet.warnings).not.toContain(BotInspectWarning.ReservedEnvKey);
305
+ });
306
+ });
307
+
308
+ describe("detect with serverk.json", () => {
309
+ test("lets the repository's own file win and says when the code disagrees", () => {
310
+ const result = project({
311
+ ...DISCORD_NODE,
312
+ "serverk.json": JSON.stringify({
313
+ platform: "telegram",
314
+ start: "node dist/main.js",
315
+ env: [
316
+ {
317
+ key: "OWNER_ID",
318
+ sensitive: false,
319
+ example: "1234",
320
+ },
321
+ ],
322
+ }),
323
+ });
324
+
325
+ expect(result.platform).toBe(BotPlatform.Telegram);
326
+ expect(result.start).toEqual({
327
+ command: "node dist/main.js",
328
+ source: "serverk.json",
329
+ });
330
+ expect(result.env[0]).toEqual({
331
+ key: "OWNER_ID",
332
+ sensitive: false,
333
+ source: "serverk.json",
334
+ example: "1234",
335
+ });
336
+ expect(result.warnings).toContain(BotInspectWarning.ManifestDisagrees);
337
+ });
338
+
339
+ test("installs with the file's own install command over the lockfile's", () => {
340
+ const result = project({
341
+ ...DISCORD_NODE,
342
+ "package-lock.json": "{}",
343
+ "serverk.json": JSON.stringify({
344
+ install: "npm ci --omit=dev",
345
+ }),
346
+ });
347
+
348
+ expect(result.install).toEqual({
349
+ command: "npm ci --omit=dev",
350
+ source: "serverk.json",
351
+ });
352
+ expect(result.packageManager).toBe(BotPackageManager.Npm);
353
+ });
354
+
355
+ test("reports a broken file and falls back to the code", () => {
356
+ const result = project({
357
+ ...DISCORD_NODE,
358
+ "package-lock.json": "{}",
359
+ "serverk.json": '{"runtime": "go", "start": "node other.js", "install": "npm i"}',
360
+ });
361
+
362
+ expect(result.runtime).toBe(BotRuntime.Node);
363
+ expect(result.start?.command).toBe("node index.js");
364
+ expect(result.install?.command).toBe("npm ci");
365
+ expect(result.warnings).toContain(BotInspectWarning.ManifestInvalid);
366
+ });
367
+ });
368
+
369
+ describe("compareCodePoints", () => {
370
+ test("orders by code point, the way Python sorts strings, whatever the locale", () => {
371
+ expect(
372
+ [
373
+ "b.js",
374
+ "B.js",
375
+ "a.js",
376
+ "\u{1F916}.js",
377
+ "a.js",
378
+ ].sort(compareCodePoints),
379
+ ).toEqual([
380
+ "B.js",
381
+ "a.js",
382
+ "b.js",
383
+ "a.js",
384
+ "\u{1F916}.js",
385
+ ]);
386
+ });
387
+ });
388
+
389
+ describe("detect env suggestions", () => {
390
+ test("reads the example file first, then every way code reads the environment", () => {
391
+ const result = project({
392
+ "package.json": "{}",
393
+ "index.js": [
394
+ "const { GUILD_ID, OWNER: owner, ...rest } = process.env;",
395
+ 'process.env["PREFIX"];',
396
+ "process.env.OPENAI_API_KEY;",
397
+ ].join("\n"),
398
+ ".env.example": "# comment\nexport LOG_LEVEL=info # verbose\nAPI_SECRET='shh'\n",
399
+ });
400
+
401
+ expect(result.env).toEqual([
402
+ {
403
+ key: "LOG_LEVEL",
404
+ sensitive: false,
405
+ source: ".env.example",
406
+ example: "info",
407
+ },
408
+ {
409
+ key: "API_SECRET",
410
+ sensitive: true,
411
+ source: ".env.example",
412
+ example: null,
413
+ },
414
+ {
415
+ key: "GUILD_ID",
416
+ sensitive: false,
417
+ source: "index.js",
418
+ example: null,
419
+ },
420
+ {
421
+ key: "OWNER",
422
+ sensitive: false,
423
+ source: "index.js",
424
+ example: null,
425
+ },
426
+ {
427
+ key: "PREFIX",
428
+ sensitive: false,
429
+ source: "index.js",
430
+ example: null,
431
+ },
432
+ {
433
+ key: "OPENAI_API_KEY",
434
+ sensitive: true,
435
+ source: "index.js",
436
+ example: null,
437
+ },
438
+ ]);
439
+ });
440
+
441
+ test("names the token key the code reads for its platform", () => {
442
+ const result = project({
443
+ "requirements.txt": "discord.py==2.7.1\n",
444
+ "main.py": 'import os\nimport discord\nos.environ.get("MY_BOT_TOKEN")\nos.getenv("LOG_CHANNEL")\n',
445
+ });
446
+
447
+ expect(result.platform).toBe(BotPlatform.Discord);
448
+ expect(result.tokenKey).toBe("MY_BOT_TOKEN");
449
+ });
450
+ });
451
+
452
+ describe("detectWantedFiles", () => {
453
+ test("asks for the small files the kit reads and at most forty small sources", () => {
454
+ const files = [
455
+ {
456
+ path: "package.json",
457
+ size: 400,
458
+ },
459
+ {
460
+ path: "package-lock.json",
461
+ size: 90_000,
462
+ },
463
+ {
464
+ path: ".env.example",
465
+ size: 20,
466
+ },
467
+ {
468
+ path: "./src/index.js",
469
+ size: 100,
470
+ },
471
+ {
472
+ path: "index.js",
473
+ size: 100,
474
+ },
475
+ {
476
+ path: "huge.js",
477
+ size: DETECT_SOURCE_MAX_BYTES + 1,
478
+ },
479
+ {
480
+ path: "node_modules/discord.js/index.js",
481
+ size: 10,
482
+ },
483
+ ...Array.from(
484
+ {
485
+ length: 60,
486
+ },
487
+ (_, index) => ({
488
+ path: `lib/deep/file${index}.js`,
489
+ size: 10,
490
+ }),
491
+ ),
492
+ ];
493
+
494
+ const wanted = detectWantedFiles(files);
495
+
496
+ expect(wanted.slice(0, 4)).toEqual([
497
+ "package.json",
498
+ ".env.example",
499
+ "index.js",
500
+ "src/index.js",
501
+ ]);
502
+ expect(wanted).not.toContain("package-lock.json");
503
+ expect(wanted).not.toContain("huge.js");
504
+ expect(wanted).not.toContain("node_modules/discord.js/index.js");
505
+ expect(wanted.length).toBe(2 + DETECT_SOURCE_FILE_LIMIT);
506
+ });
507
+ });
508
+
509
+ describe("the dependency readers", () => {
510
+ test("read requirement names and whether each one names a version", () => {
511
+ expect(parseRequirements("Discord.py[voice]==2.7.1\n-r base.txt\n# note\naiohttp ; python_version>'3'\n")).toEqual([
512
+ {
513
+ name: "discord-py",
514
+ pinned: true,
515
+ },
516
+ {
517
+ name: "aiohttp",
518
+ pinned: false,
519
+ },
520
+ ]);
521
+ });
522
+
523
+ test("read project and poetry dependencies from pyproject.toml", () => {
524
+ expect(
525
+ parsePyprojectDependencies(
526
+ '[project]\ndependencies = ["hikari>=2", \'aiogram\']\n[tool.poetry.dependencies]\npython = "^3.12"\ntelethon = "*"\n',
527
+ ),
528
+ ).toEqual([
529
+ {
530
+ name: "hikari",
531
+ pinned: true,
532
+ },
533
+ {
534
+ name: "aiogram",
535
+ pinned: false,
536
+ },
537
+ {
538
+ name: "telethon",
539
+ pinned: false,
540
+ },
541
+ ]);
542
+ });
543
+ });