@serverkgg/bridge 0.1.10 → 0.1.12

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/bot/botCauses.test.ts +81 -0
  3. package/src/bot/botCauses.ts +5 -4
  4. package/src/bot/detect/botInspectWarning.ts +1 -0
  5. package/src/bot/detect/corpus/anonymousx1025-anonxmusic.json +3 -2
  6. package/src/bot/detect/corpus/bot-base-telegram-bot-template.json +3 -1
  7. package/src/bot/detect/corpus/nezdemkovski-telegram-bot-boilerplate.json +3 -1
  8. package/src/bot/detect/corpus/serverk-discord-node.json +3 -2
  9. package/src/bot/detect/corpus/serverk-discord-python.json +3 -2
  10. package/src/bot/detect/corpus/serverk-telegram-node.json +3 -2
  11. package/src/bot/detect/corpus/serverk-telegram-python.json +3 -2
  12. package/src/bot/detect/detect.test.ts +285 -0
  13. package/src/bot/detect/detect.ts +39 -14
  14. package/src/bot/detect/detectTable.ts +4 -0
  15. package/src/bot/detect/detectedVersion.test.ts +160 -0
  16. package/src/bot/detect/detectedVersion.ts +55 -0
  17. package/src/bot/detect/envExample.test.ts +4 -0
  18. package/src/bot/detect/envExample.ts +1 -0
  19. package/src/bot/detect/envScan.test.ts +110 -0
  20. package/src/bot/detect/envScan.ts +7 -3
  21. package/src/protocol/bridgeCrash.ts +5 -1
  22. package/src/protocol/bridgeEvents.ts +1 -0
  23. package/src/runtime/bridgeEvents.test.ts +52 -1
  24. package/src/runtime/bridgeLineSpacing.test.ts +132 -0
  25. package/src/runtime/bridgeLineSpacing.ts +49 -0
  26. package/src/runtime/bridgeSupervisor.ts +5 -3
  27. package/src/steam/steamcmdNarrator.test.ts +18 -5
  28. package/src/steam/steamcmdNarrator.ts +26 -2
  29. package/src/wine/protonCommand.test.ts +17 -1
  30. package/src/wine/protonCommand.ts +7 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Serverk game bridge and driver runtime, wire protocol, manifest schema, and maintainer CLI",
5
5
  "homepage": "https://serverk.gg",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -25,6 +25,26 @@ const CASES: readonly {
25
25
  line: "GrammyError: Call to 'getMe' failed! (401: Unauthorized)",
26
26
  cause: BridgeCrashCause.TokenInvalid,
27
27
  },
28
+ {
29
+ line: "TelegramError: 401: Unauthorized",
30
+ cause: BridgeCrashCause.TokenInvalid,
31
+ },
32
+ {
33
+ line: 'error: [polling_error] {"code":"ETELEGRAM","message":"ETELEGRAM: 401 Unauthorized"}',
34
+ cause: BridgeCrashCause.TokenInvalid,
35
+ },
36
+ {
37
+ line: "TelegramApiError: 401: Unauthorized",
38
+ cause: BridgeCrashCause.TokenInvalid,
39
+ },
40
+ {
41
+ line: "DiscordAPIError[0]: 401: Unauthorized",
42
+ cause: BridgeCrashCause.TokenInvalid,
43
+ },
44
+ {
45
+ line: "httpx.HTTPStatusError: Client error '401 Unauthorized' for url 'https://api.telegram.org/bot***/getMe'",
46
+ cause: BridgeCrashCause.TokenInvalid,
47
+ },
28
48
  {
29
49
  line: "telegram.error.InvalidToken: The token `123:abc` was rejected by the server.",
30
50
  cause: BridgeCrashCause.TokenInvalid,
@@ -101,6 +121,18 @@ const CASES: readonly {
101
121
  line: "discord.errors.PrivilegedIntentsRequired: Shard ID None is requesting privileged intents that have not been explicitly enabled in the developer portal.",
102
122
  cause: BridgeCrashCause.IntentsDisallowed,
103
123
  },
124
+ {
125
+ line: "Error: Disallowed intents specified",
126
+ cause: BridgeCrashCause.IntentsDisallowed,
127
+ },
128
+ {
129
+ line: "GatewayError: Disallowed intents specified. Make sure any privileged intents you're trying to access have been enabled in the developer portal.",
130
+ cause: BridgeCrashCause.IntentsDisallowed,
131
+ },
132
+ {
133
+ line: "hikari.errors.GatewayServerClosedConnectionError: Server closed connection with code 4014 (Disallowed intent(s))",
134
+ cause: BridgeCrashCause.IntentsDisallowed,
135
+ },
104
136
  {
105
137
  line: "telegram.error.Conflict: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running",
106
138
  cause: BridgeCrashCause.TokenInUse,
@@ -109,6 +141,22 @@ const CASES: readonly {
109
141
  line: "GrammyError: Call to 'getUpdates' failed! (409: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running)",
110
142
  cause: BridgeCrashCause.TokenInUse,
111
143
  },
144
+ {
145
+ line: "TelegramError: 409: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running",
146
+ cause: BridgeCrashCause.TokenInUse,
147
+ },
148
+ {
149
+ line: 'error: [polling_error] {"code":"ETELEGRAM","message":"ETELEGRAM: 409 Conflict: terminated by other getUpdates request; make sure that only one bot instance is running"}',
150
+ cause: BridgeCrashCause.TokenInUse,
151
+ },
152
+ {
153
+ line: "TelegramApiError: 409: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running",
154
+ cause: BridgeCrashCause.TokenInUse,
155
+ },
156
+ {
157
+ line: "TelegramError: 409: Conflict: terminated by setWebhook request",
158
+ cause: BridgeCrashCause.TokenInUse,
159
+ },
112
160
  {
113
161
  line: "GrammyError: Call to 'getUpdates' failed! (409: Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first)",
114
162
  cause: BridgeCrashCause.WebhookActive,
@@ -145,6 +193,10 @@ const CASES: readonly {
145
193
  line: "ERROR: [Errno 98] error while attempting to bind on address ('0.0.0.0', 8080): address already in use",
146
194
  cause: BridgeCrashCause.PortBusy,
147
195
  },
196
+ {
197
+ line: "OSError: [Errno 48] Address already in use",
198
+ cause: BridgeCrashCause.PortBusy,
199
+ },
148
200
  {
149
201
  line: "Error: Cannot find module 'discord.js'",
150
202
  cause: BridgeCrashCause.MissingDependency,
@@ -221,6 +273,14 @@ const CASES: readonly {
221
273
  line: "error code: 1015",
222
274
  cause: BridgeCrashCause.RateLimited,
223
275
  },
276
+ {
277
+ line: "Error 1015: You are being rate limited",
278
+ cause: BridgeCrashCause.RateLimited,
279
+ },
280
+ {
281
+ line: '"message": "You are being rate limited.",',
282
+ cause: BridgeCrashCause.RateLimited,
283
+ },
224
284
  {
225
285
  line: "FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory",
226
286
  cause: BridgeCrashCause.OutOfMemory,
@@ -401,6 +461,27 @@ describe("BOT_CRASH_EVIDENCE", () => {
401
461
  expect(ring.crash(recordOf())).toBe(BridgeCrashCause.Unknown);
402
462
  });
403
463
 
464
+ for (const line of [
465
+ "AxiosError: Request failed with status code 401",
466
+ "AxiosError: Request failed with status code 409",
467
+ "requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://api.spotify.com/v1/me",
468
+ "requests.exceptions.HTTPError: 409 Client Error: Conflict for url: https://api.github.com/repos/owner/repo/merges",
469
+ "aiohttp.client_exceptions.ClientResponseError: 401, message='Unauthorized', url='https://api.spotify.com/v1/me'",
470
+ "httpx.HTTPStatusError: Client error '401 Unauthorized' for url 'https://api.spotify.com/v1/me'",
471
+ "httpx.HTTPStatusError: Client error '409 Conflict' for url 'https://api.github.com/repos/owner/repo/merges'",
472
+ "urllib.error.HTTPError: HTTP Error 401: Unauthorized",
473
+ "urllib.error.HTTPError: HTTP Error 409: Conflict",
474
+ "openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Incorrect API key provided: sk-proj-****. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_api_key'}}",
475
+ ]) {
476
+ it(`leaves a refusal from an API other than the bot's platform unexplained from "${line}"`, () => {
477
+ const { ring, print } = botRing();
478
+
479
+ print(line);
480
+
481
+ expect(ring.crash(recordOf())).toBe(BridgeCrashCause.Unknown);
482
+ });
483
+ }
484
+
404
485
  it("calls exit 137 out of memory only when the record carries the flag", () => {
405
486
  const unflagged = botRing();
406
487
 
@@ -4,7 +4,7 @@ import { BridgeEventName } from "../protocol";
4
4
  export const BOT_CRASH_EVIDENCE: readonly BridgeEventPattern[] = [
5
5
  {
6
6
  match:
7
- /\[TokenInvalid\]|An invalid token was provided|LoginFailure: Improper token|\b401:? Unauthorized\b|\bInvalidToken\b|TelegramUnauthorizedError|\bACCESS_TOKEN_(?:INVALID|EXPIRED)\b/,
7
+ /\[TokenInvalid\]|An invalid token was provided|LoginFailure: Improper token|(?:Telegram|ETELEGRAM|Call to '|\/bot[^/\s]*\/|DiscordAPIError\[).*\b401:? Unauthorized\b|\b401:? Unauthorized\b.*(?:Telegram|ETELEGRAM|\/bot[^/\s]*\/)|\bInvalidToken\b|TelegramUnauthorizedError|\bACCESS_TOKEN_(?:INVALID|EXPIRED)\b/,
8
8
  emit: BridgeEventName.TokenInvalid,
9
9
  },
10
10
  {
@@ -19,7 +19,7 @@ export const BOT_CRASH_EVIDENCE: readonly BridgeEventPattern[] = [
19
19
  },
20
20
  {
21
21
  match:
22
- /Used disallowed intents|\[DisallowedIntents\]|Privileged intent provided is not enabled or whitelisted|PrivilegedIntentsRequired|close code:? 4014\b/,
22
+ /Used disallowed intents|\[DisallowedIntents\]|Privileged intent provided is not enabled or whitelisted|PrivilegedIntentsRequired|Disallowed intents specified|Server closed connection with code 4014\b|close code:? 4014\b/,
23
23
  emit: BridgeEventName.IntentsDisallowed,
24
24
  },
25
25
  {
@@ -27,7 +27,8 @@ export const BOT_CRASH_EVIDENCE: readonly BridgeEventPattern[] = [
27
27
  emit: BridgeEventName.WebhookActive,
28
28
  },
29
29
  {
30
- match: /terminated by other getUpdates request|\b409:? Conflict\b/,
30
+ match:
31
+ /terminated by other getUpdates request|(?:Telegram|ETELEGRAM|Call to '|\/bot[^/\s]*\/).*\b409:? Conflict\b|\b409:? Conflict\b.*(?:Telegram|ETELEGRAM|\/bot[^/\s]*\/)/,
31
32
  emit: BridgeEventName.TokenInUse,
32
33
  },
33
34
  {
@@ -71,6 +72,6 @@ export const BOT_CRASH_EVIDENCE: readonly BridgeEventPattern[] = [
71
72
  {
72
73
  match:
73
74
  /FATAL ERROR: (?:Reached heap limit|Ineffective mark-compacts near heap limit) Allocation failed - JavaScript heap out of memory|^MemoryError\b/,
74
- emit: BridgeEventName.ServerCrashed,
75
+ emit: BridgeEventName.OutOfMemory,
75
76
  },
76
77
  ];
@@ -15,4 +15,5 @@ export enum BotInspectWarning {
15
15
  ManifestDisagrees = "manifest-disagrees",
16
16
  ManifestInvalid = "manifest-invalid",
17
17
  ReservedEnvKey = "reserved-env-key",
18
+ WebPort = "web-port",
18
19
  }
@@ -27,7 +27,8 @@
27
27
  ".python-version": "3.13\n",
28
28
  "Procfile": "worker: bash start\n",
29
29
  "pyproject.toml": "[project]\nname = \"anonxmusic\"\nversion = \"3.0.3\"\ndescription = \"Telegram Group Calls Streaming bot with some useful features.\"\nreadme = \".github/README.md\"\nrequires-python = \">=3.10\"\n\ndependencies = [\n \"aiohttp~=3.14.3\",\n \"kurigram>=2.2.25\",\n \"ntgcalls==3.0.0rc1\",\n \"pillow~=12.3.0\",\n \"psutil~=7.2.2\",\n \"py-tgcalls==3.0.0rc1\",\n \"py-yt-search~=0.8.0\",\n \"pymongo>=4.17.0\",\n \"pytgcrypto>=1.2.12\",\n \"python-dotenv~=1.2.3\",\n \"yt-dlp[default]>=2026.8.19\",\n]\n",
30
- "requirements.txt": "aiohttp~=3.14.3\nkurigram>=2.2.25\nntgcalls==3.0.0rc1\npillow~=12.3.0\npsutil~=7.2.2\npy-tgcalls==3.0.0rc1\npy-yt-search~=0.8.0\npymongo>=4.17.0\npytgcrypto>=1.2.12\npython-dotenv~=1.2.3\nyt-dlp[default]>=2026.8.19\n"
30
+ "requirements.txt": "aiohttp~=3.14.3\nkurigram>=2.2.25\nntgcalls==3.0.0rc1\npillow~=12.3.0\npsutil~=7.2.2\npy-tgcalls==3.0.0rc1\npy-yt-search~=0.8.0\npymongo>=4.17.0\npytgcrypto>=1.2.12\npython-dotenv~=1.2.3\nyt-dlp[default]>=2026.8.19\n",
31
+ "sample.env": "# from my.telegram.org/apps\nAPI_ID=\nAPI_HASH=\n\n# from @BotFather on telegram\nBOT_TOKEN=\nLOGGER_ID=\n\n# mongo url from cloud.mongodb.com\nMONGO_URL=\n\nOWNER_ID=\n\n# pyrogram session from @StringFatherBot on telegram\nSESSION=\n"
31
32
  },
32
33
  "expected": {
33
34
  "runtime": "python",
@@ -39,7 +40,7 @@
39
40
  "start": "bash start",
40
41
  "nodeVersion": null,
41
42
  "pythonVersion": "3.13",
42
- "tokenKey": null,
43
+ "tokenKey": "BOT_TOKEN",
43
44
  "warnings": []
44
45
  }
45
46
  }
@@ -30,6 +30,8 @@
30
30
  "nodeVersion": "20",
31
31
  "pythonVersion": null,
32
32
  "tokenKey": "BOT_TOKEN",
33
- "warnings": []
33
+ "warnings": [
34
+ "web-port"
35
+ ]
34
36
  }
35
37
  }
@@ -35,6 +35,8 @@
35
35
  "nodeVersion": null,
36
36
  "pythonVersion": null,
37
37
  "tokenKey": "BOT_TOKEN",
38
- "warnings": []
38
+ "warnings": [
39
+ "web-port"
40
+ ]
39
41
  }
40
42
  }
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "source": {
3
- "url": "https://github.com/serverkgg/bot-node/tree/main/templates/discord-node",
3
+ "url": "https://github.com/serverkgg/bot-template-discord-node",
4
4
  "branch": "main",
5
- "commit": "3f9f6c033824857a9ffd849421fc5ecff31500d7",
5
+ "commit": "0c950227f6c2ab97b610080f2e9c9f60ff8c6069",
6
6
  "stars": null
7
7
  },
8
8
  "files": [
9
9
  ".env.example",
10
10
  ".gitignore",
11
+ "AGENTS.md",
11
12
  "LICENSE",
12
13
  "README.md",
13
14
  "index.js",
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "source": {
3
- "url": "https://github.com/serverkgg/bot-python/tree/main/templates/discord-python",
3
+ "url": "https://github.com/serverkgg/bot-template-discord-python",
4
4
  "branch": "main",
5
- "commit": "0a623a9dc114e31f0f2a5011e1f0471b2c047df3",
5
+ "commit": "e520ae1dfb07bb1e8a1eba73c5651a3ad3d39ad1",
6
6
  "stars": null
7
7
  },
8
8
  "files": [
9
9
  ".env.example",
10
10
  ".gitignore",
11
+ "AGENTS.md",
11
12
  "LICENSE",
12
13
  "README.md",
13
14
  "main.py",
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "source": {
3
- "url": "https://github.com/serverkgg/bot-node/tree/main/templates/telegram-node",
3
+ "url": "https://github.com/serverkgg/bot-template-telegram-node",
4
4
  "branch": "main",
5
- "commit": "3f9f6c033824857a9ffd849421fc5ecff31500d7",
5
+ "commit": "49a55385c7336551dbb50b6ebc99a4e18743418f",
6
6
  "stars": null
7
7
  },
8
8
  "files": [
9
9
  ".env.example",
10
10
  ".gitignore",
11
+ "AGENTS.md",
11
12
  "LICENSE",
12
13
  "README.md",
13
14
  "index.js",
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "source": {
3
- "url": "https://github.com/serverkgg/bot-python/tree/main/templates/telegram-python",
3
+ "url": "https://github.com/serverkgg/bot-template-telegram-python",
4
4
  "branch": "main",
5
- "commit": "0a623a9dc114e31f0f2a5011e1f0471b2c047df3",
5
+ "commit": "342e61c43f02f469c572f43cd3ed1e95ba85a130",
6
6
  "stars": null
7
7
  },
8
8
  "files": [
9
9
  ".env.example",
10
10
  ".gitignore",
11
+ "AGENTS.md",
11
12
  "LICENSE",
12
13
  "README.md",
13
14
  "main.py",
@@ -51,6 +51,16 @@ const TELEGRAM_PYTHON = {
51
51
  ".env.example": "TELEGRAM_TOKEN=\n",
52
52
  };
53
53
 
54
+ const NPM_INSTALLED_PACKAGE = `${JSON.stringify(
55
+ {
56
+ dependencies: {
57
+ "discord.js": "^14.27.0",
58
+ },
59
+ },
60
+ null,
61
+ 2,
62
+ )}\n`;
63
+
54
64
  describe("detect on the templates", () => {
55
65
  it("reads the discord.js template end to end", () => {
56
66
  expect(
@@ -371,6 +381,89 @@ describe("detect on the templates", () => {
371
381
  expect(project(TELEGRAM_PYTHON).pythonVersion).toBeNull();
372
382
  });
373
383
 
384
+ const NODE_VERSION_CASES: readonly {
385
+ first: string;
386
+ second: string;
387
+ files: Record<string, string>;
388
+ version: string;
389
+ }[] = [
390
+ {
391
+ first: "engines.node",
392
+ second: ".nvmrc",
393
+ files: {
394
+ "package.json": JSON.stringify({
395
+ ...JSON.parse(DISCORD_NODE["package.json"]),
396
+ engines: {
397
+ node: ">=18",
398
+ },
399
+ }),
400
+ ".nvmrc": "v20.11.1\n",
401
+ },
402
+ version: "18",
403
+ },
404
+ {
405
+ first: ".nvmrc",
406
+ second: ".node-version",
407
+ files: {
408
+ ".nvmrc": "v22.11.0\n",
409
+ ".node-version": "20.11.1\n",
410
+ },
411
+ version: "22",
412
+ },
413
+ {
414
+ first: ".node-version",
415
+ second: ".tool-versions",
416
+ files: {
417
+ ".node-version": "20.11.1\n",
418
+ ".tool-versions": "nodejs 22.3.0\n",
419
+ },
420
+ version: "20",
421
+ },
422
+ ];
423
+
424
+ for (const item of NODE_VERSION_CASES) {
425
+ it(`reads the Node version from ${item.first} before ${item.second}`, () => {
426
+ const result = project({
427
+ ...DISCORD_NODE,
428
+ ...item.files,
429
+ });
430
+
431
+ expect(result.nodeVersion).toBe(item.version);
432
+ });
433
+ }
434
+
435
+ it("reads the Python version from runtime.txt before the Pipfile's python_version", () => {
436
+ const result = project({
437
+ Pipfile:
438
+ '[[source]]\nurl = "https://pypi.org/simple"\nverify_ssl = true\nname = "pypi"\n\n[packages]\n"discord.py" = "*"\n\n[dev-packages]\n\n[requires]\npython_version = "3.10"\n',
439
+ "runtime.txt": "python-3.11.9\n",
440
+ "bot.py": "import discord\n",
441
+ });
442
+
443
+ expect(result.packageManager).toBe(BotPackageManager.Pipenv);
444
+ expect(result.pythonVersion).toBe("3.11");
445
+ });
446
+
447
+ it("installs with yarn when only engines.yarn names a manager and no lockfile ships", () => {
448
+ const result = project({
449
+ ...DISCORD_NODE,
450
+ "package.json": JSON.stringify({
451
+ ...JSON.parse(DISCORD_NODE["package.json"]),
452
+ engines: {
453
+ node: ">=18",
454
+ yarn: ">=1.22",
455
+ },
456
+ }),
457
+ });
458
+
459
+ expect(result.packageManager).toBe(BotPackageManager.Yarn);
460
+ expect(result.install).toEqual({
461
+ command: "corepack yarn install",
462
+ source: "package.json",
463
+ });
464
+ expect(result.warnings).toContain(BotInspectWarning.NoLockfile);
465
+ });
466
+
374
467
  it("installs from the Python lockfiles, honouring uv and warning about the rest", () => {
375
468
  const locked = (lockfile: string) => {
376
469
  return project(
@@ -484,6 +577,107 @@ describe("detect start commands", () => {
484
577
  });
485
578
  });
486
579
 
580
+ const NODE_START_CASES: readonly {
581
+ file: string;
582
+ beside: string[];
583
+ }[] = [
584
+ {
585
+ file: "index.js",
586
+ beside: [
587
+ "src/index.js",
588
+ "bot.js",
589
+ "src/bot.js",
590
+ "index.ts",
591
+ ],
592
+ },
593
+ {
594
+ file: "src/index.js",
595
+ beside: [
596
+ "bot.js",
597
+ "src/bot.js",
598
+ "index.ts",
599
+ ],
600
+ },
601
+ {
602
+ file: "bot.js",
603
+ beside: [
604
+ "src/bot.js",
605
+ "index.ts",
606
+ ],
607
+ },
608
+ {
609
+ file: "src/bot.js",
610
+ beside: [
611
+ "index.ts",
612
+ ],
613
+ },
614
+ {
615
+ file: "index.ts",
616
+ beside: [],
617
+ },
618
+ ];
619
+
620
+ for (const item of NODE_START_CASES) {
621
+ it(`starts ${item.file} when package.json names no start script and no main, ahead of [${item.beside.join(", ")}]`, () => {
622
+ const result = project(
623
+ {
624
+ "package.json": NPM_INSTALLED_PACKAGE,
625
+ [item.file]: "",
626
+ },
627
+ item.beside,
628
+ );
629
+
630
+ expect(result.start).toEqual({
631
+ command: `node ${item.file}`,
632
+ source: item.file,
633
+ });
634
+ expect(result.warnings).toEqual([
635
+ BotInspectWarning.NoLockfile,
636
+ ]);
637
+ });
638
+ }
639
+
640
+ const PYTHON_START_CASES: readonly {
641
+ file: string;
642
+ beside: string[];
643
+ }[] = [
644
+ {
645
+ file: "app.py",
646
+ beside: [
647
+ "src/main.py",
648
+ "src/bot.py",
649
+ ],
650
+ },
651
+ {
652
+ file: "src/main.py",
653
+ beside: [
654
+ "src/bot.py",
655
+ ],
656
+ },
657
+ {
658
+ file: "src/bot.py",
659
+ beside: [],
660
+ },
661
+ ];
662
+
663
+ for (const item of PYTHON_START_CASES) {
664
+ it(`starts ${item.file} when no main.py or bot.py exists, ahead of [${item.beside.join(", ")}]`, () => {
665
+ const result = project(
666
+ {
667
+ "requirements.txt": "python-telegram-bot==21.11.1\n",
668
+ [item.file]: "",
669
+ },
670
+ item.beside,
671
+ );
672
+
673
+ expect(result.start).toEqual({
674
+ command: `python ${item.file}`,
675
+ source: item.file,
676
+ });
677
+ expect(result.warnings).toEqual([]);
678
+ });
679
+ }
680
+
487
681
  it("starts the one Python package that has a __main__ module when no start file exists", () => {
488
682
  expect(
489
683
  project(
@@ -700,6 +894,68 @@ describe("detect warnings", () => {
700
894
  ]);
701
895
  expect(loud.warnings).toContain(BotInspectWarning.ReservedEnvKey);
702
896
  });
897
+
898
+ it("warns that a node bot reading PORT needs the web address, and keeps PORT among the variables", () => {
899
+ const result = project({
900
+ ...DISCORD_NODE,
901
+ "index.js": "const token = process.env.DISCORD_TOKEN;\napp.listen(process.env.PORT);\n",
902
+ });
903
+
904
+ expect(result.env.map((entry) => entry.key)).toEqual([
905
+ "DISCORD_TOKEN",
906
+ "PORT",
907
+ ]);
908
+ expect(result.warnings).toContain(BotInspectWarning.WebPort);
909
+ });
910
+
911
+ it("warns that a python bot whose example file lists a webhook needs the web address", () => {
912
+ const result = project({
913
+ ...TELEGRAM_PYTHON,
914
+ ".env.example": "TELEGRAM_TOKEN=\nWEBHOOK_URL=\n",
915
+ });
916
+
917
+ expect(result.env.map((entry) => entry.key)).toEqual([
918
+ "TELEGRAM_TOKEN",
919
+ "WEBHOOK_URL",
920
+ ]);
921
+ expect(result.warnings).toContain(BotInspectWarning.WebPort);
922
+ });
923
+
924
+ it("reads a webhook key on a discord bot as an outgoing log webhook, and on a telegram bot as one it receives", () => {
925
+ const discord = project({
926
+ ...DISCORD_NODE,
927
+ ".env.example": "DISCORD_TOKEN=\nLOG_WEBHOOK_URL=\n",
928
+ });
929
+
930
+ expect(discord.platform).toBe(BotPlatform.Discord);
931
+ expect(discord.env.map((entry) => entry.key)).toEqual([
932
+ "DISCORD_TOKEN",
933
+ "LOG_WEBHOOK_URL",
934
+ ]);
935
+ expect(discord.warnings).not.toContain(BotInspectWarning.WebPort);
936
+
937
+ const telegram = project({
938
+ ...TELEGRAM_PYTHON,
939
+ ".env.example": "TELEGRAM_TOKEN=\nLOG_WEBHOOK_URL=\n",
940
+ });
941
+
942
+ expect(telegram.platform).toBe(BotPlatform.Telegram);
943
+ expect(telegram.warnings).toContain(BotInspectWarning.WebPort);
944
+ });
945
+
946
+ it("keeps quiet about the web address for a bot that reads neither PORT nor a webhook", () => {
947
+ const result = project({
948
+ ...DISCORD_NODE,
949
+ "index.js": "const token = process.env.DISCORD_TOKEN;\nconst port = process.env.SERVER_PORT;\n",
950
+ });
951
+
952
+ expect(result.env.map((entry) => entry.key)).toEqual([
953
+ "DISCORD_TOKEN",
954
+ "SERVER_PORT",
955
+ ]);
956
+ expect(result.warnings).not.toContain(BotInspectWarning.WebPort);
957
+ expect(project(TELEGRAM_PYTHON).warnings).not.toContain(BotInspectWarning.WebPort);
958
+ });
703
959
  });
704
960
 
705
961
  describe("detect with serverk.json", () => {
@@ -733,6 +989,32 @@ describe("detect with serverk.json", () => {
733
989
  expect(result.warnings).toContain(BotInspectWarning.ManifestDisagrees);
734
990
  });
735
991
 
992
+ it("says when the file names a runtime the code is not written in", () => {
993
+ const disagrees = project({
994
+ ...TELEGRAM_PYTHON,
995
+ "serverk.json": JSON.stringify({
996
+ runtime: "node",
997
+ }),
998
+ });
999
+
1000
+ expect(disagrees.runtime).toBe(BotRuntime.Node);
1001
+ expect(disagrees.warnings).toEqual([
1002
+ BotInspectWarning.NoStart,
1003
+ BotInspectWarning.NoManifest,
1004
+ BotInspectWarning.ManifestDisagrees,
1005
+ ]);
1006
+
1007
+ const agrees = project({
1008
+ ...TELEGRAM_PYTHON,
1009
+ "serverk.json": JSON.stringify({
1010
+ runtime: "python",
1011
+ }),
1012
+ });
1013
+
1014
+ expect(agrees.runtime).toBe(BotRuntime.Python);
1015
+ expect(agrees.warnings).toEqual([]);
1016
+ });
1017
+
736
1018
  it("installs with the file's own install command over the lockfile's", () => {
737
1019
  const result = project({
738
1020
  ...DISCORD_NODE,
@@ -847,6 +1129,9 @@ describe("detect env suggestions", () => {
847
1129
  {
848
1130
  file: ".env.dist",
849
1131
  },
1132
+ {
1133
+ file: "sample.env",
1134
+ },
850
1135
  ];
851
1136
 
852
1137
  for (const item of CASES) {