@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
@@ -1,14 +1,21 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { z } from "zod";
3
- import { PortJoin, PortProtocol } from "../core";
3
+ import { PORT_BLOCK_MAX_COUNT, PortJoin, PortProtocol } from "../core";
4
4
  import { GameCategory } from "./category";
5
+ import { buildJsonSchema } from "./jsonSchema";
6
+ import { PackageKind } from "./kind";
5
7
  import {
8
+ botManifestSchema,
9
+ gameManifestSchema,
6
10
  guidePackageSchema,
11
+ isBotManifest,
12
+ isGameManifest,
7
13
  MANIFEST_SCHEMA_VERSION,
8
14
  type Manifest,
9
15
  manifestSchema,
10
16
  PORT_FOLLOW_MIN_OFFSET,
11
17
  packageSchema,
18
+ runnablePackageSchema,
12
19
  } from "./manifest";
13
20
  import { isServerkAvatarTemplate, PRESENCE_AVATAR_LENGTH_LIMIT } from "./presence";
14
21
  import {
@@ -22,6 +29,7 @@ import {
22
29
  SHM_MAX_MB,
23
30
  WINE_MIN_SHM_MB,
24
31
  } from "./runtime";
32
+ import { MANIFEST_FILE, type PackageSource, readPackage } from "./source";
25
33
 
26
34
  const text = (value: string) => ({
27
35
  ar: value,
@@ -610,7 +618,7 @@ describe("packageSchema", () => {
610
618
  test("routes on kind without a separate pre-parse", () => {
611
619
  const game = packageSchema.parse(base());
612
620
 
613
- expect(game.kind).toBe("GamePackage");
621
+ expect(game.kind).toBe(PackageKind.Game);
614
622
 
615
623
  const guides = packageSchema.parse({
616
624
  schemaVersion: MANIFEST_SCHEMA_VERSION,
@@ -631,7 +639,7 @@ describe("packageSchema", () => {
631
639
  ],
632
640
  });
633
641
 
634
- expect(guides.kind).toBe("GuidePackage");
642
+ expect(guides.kind).toBe(PackageKind.Guide);
635
643
  });
636
644
 
637
645
  test("guide packages get the same guide checks", () => {
@@ -1176,3 +1184,622 @@ describe("port mirroring", () => {
1176
1184
  ]);
1177
1185
  });
1178
1186
  });
1187
+
1188
+ describe("port pairs and blocks", () => {
1189
+ const pairPort = (key: string, port: number) => ({
1190
+ ...udpPort(key, port, port),
1191
+ protocol: PortProtocol.TcpUdp,
1192
+ });
1193
+
1194
+ const blockPort = (key: string, first: number, count: number) => ({
1195
+ key,
1196
+ containerPort: first,
1197
+ protocol: PortProtocol.Udp,
1198
+ mirror: true,
1199
+ count,
1200
+ hostRange: {
1201
+ default: first,
1202
+ min: first,
1203
+ max: first + 959,
1204
+ },
1205
+ });
1206
+
1207
+ test("defaults a port to a single binding", () => {
1208
+ const manifest = manifestSchema.parse(base());
1209
+
1210
+ expect(manifest.container.ports[0]?.count).toBe(1);
1211
+ });
1212
+
1213
+ test("accepts a tcp+udp join port beside a udp media block", () => {
1214
+ const manifest = manifestSchema.parse(
1215
+ withPorts([
1216
+ pairPort("game", 19_132),
1217
+ blockPort("media", 20_000, 32),
1218
+ ]),
1219
+ );
1220
+
1221
+ expect(
1222
+ manifest.container.ports.map((port) => [
1223
+ port.protocol,
1224
+ port.count,
1225
+ ]),
1226
+ ).toEqual([
1227
+ [
1228
+ PortProtocol.TcpUdp,
1229
+ 1,
1230
+ ],
1231
+ [
1232
+ PortProtocol.Udp,
1233
+ 32,
1234
+ ],
1235
+ ]);
1236
+ });
1237
+
1238
+ test("refuses a count above the block limit", () => {
1239
+ expect(
1240
+ failure(
1241
+ withPorts([
1242
+ blockPort("media", 20_000, PORT_BLOCK_MAX_COUNT + 1),
1243
+ ]),
1244
+ ),
1245
+ ).toEqual([
1246
+ {
1247
+ path: "container.ports.0.count",
1248
+ message: `Too big: expected number to be <=${PORT_BLOCK_MAX_COUNT}`,
1249
+ },
1250
+ ]);
1251
+ });
1252
+
1253
+ test("refuses a block whose host range cannot hold it", () => {
1254
+ expect(
1255
+ failure(
1256
+ withPorts([
1257
+ {
1258
+ ...blockPort("media", 20_000, 32),
1259
+ hostRange: {
1260
+ default: 20_000,
1261
+ min: 20_000,
1262
+ max: 20_015,
1263
+ },
1264
+ },
1265
+ ]),
1266
+ ),
1267
+ ).toEqual([
1268
+ {
1269
+ path: "container.ports.0.hostRange",
1270
+ message: "must span at least 32 ports to hold the block",
1271
+ },
1272
+ ]);
1273
+ });
1274
+
1275
+ const spanning = (key: string, min: number, max: number, count: number) => ({
1276
+ ...blockPort(key, min, count),
1277
+ hostRange: {
1278
+ default: min,
1279
+ min,
1280
+ max,
1281
+ },
1282
+ });
1283
+
1284
+ test("lets a single port span the whole cap", () => {
1285
+ const manifest = manifestSchema.parse(
1286
+ withPorts([
1287
+ spanning("game", 20_000, 20_999, 1),
1288
+ ]),
1289
+ );
1290
+
1291
+ expect(manifest.container.ports[0]?.hostRange.max).toBe(20_999);
1292
+ });
1293
+
1294
+ test("refuses a single port spanning one more than the cap", () => {
1295
+ expect(
1296
+ failure(
1297
+ withPorts([
1298
+ spanning("game", 20_000, 21_000, 1),
1299
+ ]),
1300
+ ),
1301
+ ).toEqual([
1302
+ {
1303
+ path: "container.ports.0.hostRange",
1304
+ message: "must span at most 1000 ports",
1305
+ },
1306
+ ]);
1307
+ });
1308
+
1309
+ test("counts the span of a block in slots, so 100 slots of 64 pass", () => {
1310
+ const manifest = manifestSchema.parse(
1311
+ withPorts([
1312
+ spanning("media", 20_000, 26_399, 64),
1313
+ ]),
1314
+ );
1315
+
1316
+ expect(manifest.container.ports[0]?.count).toBe(64);
1317
+ });
1318
+
1319
+ test("refuses a block spanning more slots than the cap", () => {
1320
+ expect(
1321
+ failure(
1322
+ withPorts([
1323
+ spanning("media", 20_000, 22_000, 2),
1324
+ ]),
1325
+ ),
1326
+ ).toEqual([
1327
+ {
1328
+ path: "container.ports.0.hostRange",
1329
+ message: "must span at most 1000 slots of 2 ports",
1330
+ },
1331
+ ]);
1332
+ });
1333
+
1334
+ test("refuses a block whose default leaves no room for the whole block", () => {
1335
+ expect(
1336
+ failure(
1337
+ withPorts([
1338
+ {
1339
+ ...blockPort("media", 20_000, 32),
1340
+ hostRange: {
1341
+ default: 20_950,
1342
+ min: 20_000,
1343
+ max: 20_959,
1344
+ },
1345
+ },
1346
+ ]),
1347
+ ),
1348
+ ).toEqual([
1349
+ {
1350
+ path: "container.ports.0.hostRange.default",
1351
+ message: "default must leave room for all 32 ports before max",
1352
+ },
1353
+ ]);
1354
+ });
1355
+
1356
+ test("refuses a non-mirrored block that runs past the last port", () => {
1357
+ expect(
1358
+ failure(
1359
+ withPorts([
1360
+ {
1361
+ ...blockPort("media", 20_000, 32),
1362
+ mirror: false,
1363
+ containerPort: 65_520,
1364
+ },
1365
+ ]),
1366
+ ),
1367
+ ).toEqual([
1368
+ {
1369
+ path: "container.ports.0.containerPort",
1370
+ message: "a block of 32 ports starting here runs past 65535",
1371
+ },
1372
+ ]);
1373
+ });
1374
+
1375
+ test("refuses the player-address fields on a block", () => {
1376
+ expect(
1377
+ failure(
1378
+ withPorts([
1379
+ {
1380
+ ...blockPort("media", 20_000, 32),
1381
+ name: text("Media"),
1382
+ hint: text("Nobody types this"),
1383
+ join: PortJoin.Separate,
1384
+ srv: {
1385
+ service: "_minecraft",
1386
+ },
1387
+ },
1388
+ ]),
1389
+ ),
1390
+ ).toEqual([
1391
+ {
1392
+ path: "container.ports.0.name",
1393
+ message: "a block of 32 ports is never a player address and takes no name",
1394
+ },
1395
+ {
1396
+ path: "container.ports.0.hint",
1397
+ message: "a block of 32 ports is never a player address and takes no hint",
1398
+ },
1399
+ {
1400
+ path: "container.ports.0.srv",
1401
+ message: "a block of 32 ports is never a player address and takes no srv",
1402
+ },
1403
+ {
1404
+ path: "container.ports.0.join",
1405
+ message: "a block of 32 ports is never a player address and takes no join",
1406
+ },
1407
+ ]);
1408
+ });
1409
+
1410
+ test("refuses a block that follows a port", () => {
1411
+ expect(
1412
+ failure(
1413
+ withPorts([
1414
+ udpPort("game", 20_000, 20_000),
1415
+ {
1416
+ ...blockPort("media", 21_000, 4),
1417
+ follows: {
1418
+ key: "game",
1419
+ offset: 1,
1420
+ },
1421
+ },
1422
+ ]),
1423
+ ),
1424
+ ).toEqual([
1425
+ {
1426
+ path: "container.ports.1.follows",
1427
+ message: "a block of 4 ports is never a player address and takes no follows",
1428
+ },
1429
+ ]);
1430
+ });
1431
+
1432
+ test("refuses a port that follows a block", () => {
1433
+ expect(
1434
+ failure(
1435
+ withPorts([
1436
+ blockPort("media", 20_000, 4),
1437
+ {
1438
+ ...udpPort("query", 21_000, 21_000),
1439
+ follows: {
1440
+ key: "media",
1441
+ offset: 1,
1442
+ },
1443
+ },
1444
+ ]),
1445
+ ),
1446
+ ).toEqual([
1447
+ {
1448
+ path: "container.ports.1.follows.key",
1449
+ message: '"media" is a block of 4 ports — a port only follows a single port',
1450
+ },
1451
+ ]);
1452
+ });
1453
+
1454
+ test("refuses srv on a tcp+udp port", () => {
1455
+ expect(
1456
+ failure(
1457
+ withPorts([
1458
+ {
1459
+ ...pairPort("game", 19_132),
1460
+ srv: {
1461
+ service: "_minecraft",
1462
+ },
1463
+ },
1464
+ ]),
1465
+ ),
1466
+ ).toEqual([
1467
+ {
1468
+ path: "container.ports.0.srv",
1469
+ message: "a tcp+udp port takes no srv — an SRV record names one protocol",
1470
+ },
1471
+ ]);
1472
+ });
1473
+
1474
+ test("refuses a tcp+udp port that follows another", () => {
1475
+ expect(
1476
+ failure(
1477
+ withPorts([
1478
+ pairPort("game", 19_132),
1479
+ {
1480
+ ...pairPort("query", 19_300),
1481
+ follows: {
1482
+ key: "game",
1483
+ offset: 1,
1484
+ },
1485
+ },
1486
+ ]),
1487
+ ),
1488
+ ).toEqual([
1489
+ {
1490
+ path: "container.ports.1.follows.key",
1491
+ message: 'port "query" carries tcp+udp and cannot follow another port',
1492
+ },
1493
+ ]);
1494
+ });
1495
+
1496
+ test("refuses a port that follows a tcp+udp port", () => {
1497
+ expect(
1498
+ failure(
1499
+ withPorts([
1500
+ pairPort("game", 19_132),
1501
+ {
1502
+ ...udpPort("query", 19_300, 19_300),
1503
+ follows: {
1504
+ key: "game",
1505
+ offset: 1,
1506
+ },
1507
+ },
1508
+ ]),
1509
+ ),
1510
+ ).toEqual([
1511
+ {
1512
+ path: "container.ports.1.follows.key",
1513
+ message: '"game" is a tcp+udp port — a port only follows one of its own protocol',
1514
+ },
1515
+ ]);
1516
+ });
1517
+
1518
+ test("refuses a tcp+udp port overlapping a udp port on the shared protocol", () => {
1519
+ expect(
1520
+ failure(
1521
+ withPorts([
1522
+ pairPort("game", 19_132),
1523
+ udpPort("query", 19_140, 19_140),
1524
+ ]),
1525
+ ),
1526
+ ).toEqual([
1527
+ {
1528
+ path: "container.ports.1.hostRange",
1529
+ message: 'host range overlaps port "game" — two udp ports cannot share host ports',
1530
+ },
1531
+ ]);
1532
+ });
1533
+
1534
+ test("lets a tcp port and a udp port share a host range", () => {
1535
+ const [tcp] = base().container.ports;
1536
+
1537
+ if (!tcp) {
1538
+ throw new Error("fixture is missing its port");
1539
+ }
1540
+
1541
+ const manifest = manifestSchema.parse(
1542
+ withPorts([
1543
+ tcp,
1544
+ udpPort("query", 25_565, 25_565),
1545
+ ]),
1546
+ );
1547
+
1548
+ expect(manifest.container.ports).toHaveLength(2);
1549
+ });
1550
+
1551
+ test("refuses a manifest whose pairs expand past the socket port limit", () => {
1552
+ expect(
1553
+ failure(
1554
+ withPorts(
1555
+ [
1556
+ 0,
1557
+ 1,
1558
+ 2,
1559
+ 3,
1560
+ 4,
1561
+ ].map((index) => pairPort(`p${index}`, 20_000 + index * 100)),
1562
+ ),
1563
+ ),
1564
+ ).toEqual([
1565
+ {
1566
+ path: "container.ports",
1567
+ message: "expands to 10 socket ports — a tcp+udp port counts twice and the total must stay at most 8",
1568
+ },
1569
+ ]);
1570
+ });
1571
+ });
1572
+
1573
+ const botBase = () => ({
1574
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
1575
+ kind: PackageKind.Bot,
1576
+ metadata: {
1577
+ id: "bot-node",
1578
+ name: text("Node.js Bot"),
1579
+ description: text("Run your own bot"),
1580
+ tagline: text("Your code, always on"),
1581
+ logo: "assets/logo.webp",
1582
+ banner: "assets/banner.webp",
1583
+ },
1584
+ resources: {
1585
+ minMemoryMb: 512,
1586
+ recommendedMemoryMb: 1024,
1587
+ minDiskMb: 2048,
1588
+ },
1589
+ container: {
1590
+ image: "serverkgg/bot-node",
1591
+ ports: [],
1592
+ runtime: {
1593
+ pidsLimit: 256,
1594
+ },
1595
+ },
1596
+ });
1597
+
1598
+ const botFailure = (input: unknown) => {
1599
+ const result = botManifestSchema.safeParse(input);
1600
+
1601
+ if (result.success) {
1602
+ throw new Error("expected the bot manifest to be rejected");
1603
+ }
1604
+
1605
+ return result.error.issues.map((issue) => ({
1606
+ path: issue.path.join("."),
1607
+ message: issue.message,
1608
+ }));
1609
+ };
1610
+
1611
+ const yamlSource = (document: unknown): PackageSource => {
1612
+ const files = new Map([
1613
+ [
1614
+ MANIFEST_FILE,
1615
+ new TextEncoder().encode(JSON.stringify(document)),
1616
+ ],
1617
+ ]);
1618
+
1619
+ return {
1620
+ list: () => [
1621
+ ...files.keys(),
1622
+ ],
1623
+ read: (path) => files.get(path) ?? null,
1624
+ };
1625
+ };
1626
+
1627
+ describe("bot packages", () => {
1628
+ test("parse with no ports and no category", () => {
1629
+ const manifest = botManifestSchema.parse(botBase());
1630
+
1631
+ expect(manifest.kind).toBe(PackageKind.Bot);
1632
+ expect(manifest.container.ports).toEqual([]);
1633
+ expect("category" in manifest.metadata).toBe(false);
1634
+ expect(manifest.secrets).toBeUndefined();
1635
+ expect(manifest.presence).toBeUndefined();
1636
+ });
1637
+
1638
+ test("default the ports to none when a bot declares no ports key", () => {
1639
+ const { container, ...rest } = botBase();
1640
+ const manifest = botManifestSchema.parse({
1641
+ ...rest,
1642
+ container: {
1643
+ image: container.image,
1644
+ },
1645
+ });
1646
+
1647
+ expect(manifest.container.ports).toEqual([]);
1648
+ });
1649
+
1650
+ test("refuse a secrets list with the environment vault named as the place for per-bot values", () => {
1651
+ expect(
1652
+ botFailure({
1653
+ ...botBase(),
1654
+ secrets: [],
1655
+ }),
1656
+ ).toEqual([
1657
+ {
1658
+ path: "secrets",
1659
+ message: "a bot package declares no platform secrets; per-bot values live in the environment vault",
1660
+ },
1661
+ ]);
1662
+ });
1663
+
1664
+ test("keep the environment vault off unless the package turns it on", () => {
1665
+ expect(botManifestSchema.parse(botBase()).environment.vault).toBe(false);
1666
+ expect(
1667
+ botManifestSchema.parse({
1668
+ ...botBase(),
1669
+ environment: {
1670
+ vault: true,
1671
+ },
1672
+ }).environment.vault,
1673
+ ).toBe(true);
1674
+ });
1675
+
1676
+ test("refuse a vault flag that is not a boolean", () => {
1677
+ expect(
1678
+ botFailure({
1679
+ ...botBase(),
1680
+ environment: {
1681
+ vault: "yes",
1682
+ },
1683
+ }).map((issue) => issue.path),
1684
+ ).toEqual([
1685
+ "environment.vault",
1686
+ ]);
1687
+ });
1688
+
1689
+ test("refuse a presence block", () => {
1690
+ expect(
1691
+ botFailure({
1692
+ ...botBase(),
1693
+ presence: {
1694
+ name: "player",
1695
+ },
1696
+ }),
1697
+ ).toEqual([
1698
+ {
1699
+ path: "presence",
1700
+ message: "a bot package has no player presence",
1701
+ },
1702
+ ]);
1703
+ });
1704
+
1705
+ test("still run the shared package checks", () => {
1706
+ expect(
1707
+ botFailure({
1708
+ ...botBase(),
1709
+ container: {
1710
+ ...botBase().container,
1711
+ image: "serverkgg/bot-python",
1712
+ },
1713
+ }),
1714
+ ).toEqual([
1715
+ {
1716
+ path: "container.image",
1717
+ message: 'must be "serverkgg/bot-node" to match metadata.id',
1718
+ },
1719
+ ]);
1720
+ });
1721
+
1722
+ test("keep a port a bot declares for a webhook", () => {
1723
+ const manifest = botManifestSchema.parse({
1724
+ ...botBase(),
1725
+ container: {
1726
+ ...botBase().container,
1727
+ ports: base().container.ports,
1728
+ },
1729
+ });
1730
+
1731
+ expect(manifest.container.ports).toHaveLength(1);
1732
+ });
1733
+
1734
+ test("reach the runtime reader like a game does", () => {
1735
+ expect(manifestRuntimeOf(botManifestSchema.parse(botBase()))).toEqual({
1736
+ ...MANIFEST_RUNTIME_DEFAULTS,
1737
+ pidsLimit: 256,
1738
+ });
1739
+ });
1740
+ });
1741
+
1742
+ describe("package kinds", () => {
1743
+ test("route all three kinds through packageSchema", () => {
1744
+ expect(packageSchema.parse(base()).kind).toBe(PackageKind.Game);
1745
+ expect(packageSchema.parse(botBase()).kind).toBe(PackageKind.Bot);
1746
+ expect(
1747
+ packageSchema.parse(
1748
+ guidePackage([
1749
+ guide("start"),
1750
+ ]),
1751
+ ).kind,
1752
+ ).toBe(PackageKind.Guide);
1753
+ });
1754
+
1755
+ test("keep a game package on the game rules, so it still needs a port", () => {
1756
+ const result = packageSchema.safeParse(withPorts([]));
1757
+
1758
+ expect(result.success).toBe(false);
1759
+ });
1760
+
1761
+ test("refuse a guide package where a runnable package is expected", () => {
1762
+ expect(
1763
+ runnablePackageSchema.safeParse(
1764
+ guidePackage([
1765
+ guide("start"),
1766
+ ]),
1767
+ ).success,
1768
+ ).toBe(false);
1769
+ });
1770
+
1771
+ test("tell a game from a bot", () => {
1772
+ const game = runnablePackageSchema.parse(base());
1773
+ const bot = runnablePackageSchema.parse(botBase());
1774
+
1775
+ expect(isGameManifest(game)).toBe(true);
1776
+ expect(isBotManifest(game)).toBe(false);
1777
+ expect(isBotManifest(bot)).toBe(true);
1778
+ expect(isGameManifest(bot)).toBe(false);
1779
+ });
1780
+
1781
+ test("keep the old game schema name pointing at the game schema for this release", () => {
1782
+ expect(manifestSchema).toBe(gameManifestSchema);
1783
+ });
1784
+
1785
+ test("read a bot package from its source", () => {
1786
+ const { manifest } = readPackage(yamlSource(botBase()));
1787
+
1788
+ expect(manifest.kind).toBe(PackageKind.Bot);
1789
+ expect(manifest.metadata.id).toBe("bot-node");
1790
+ });
1791
+
1792
+ test("read a game package from its source unchanged", () => {
1793
+ const { manifest } = readPackage(yamlSource(base()));
1794
+
1795
+ expect(isGameManifest(manifest) && manifest.secrets).toEqual([]);
1796
+ });
1797
+
1798
+ test("render every kind literal into the JSON schema", () => {
1799
+ const rendered = JSON.stringify(buildJsonSchema());
1800
+
1801
+ for (const kind of Object.values(PackageKind)) {
1802
+ expect(rendered).toContain(`"${kind}"`);
1803
+ }
1804
+ });
1805
+ });