@serverkgg/bridge 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/package.json +3 -2
  2. package/rules/package.md +60 -0
  3. package/src/blog/articleFrontmatter.test.ts +10 -10
  4. package/src/blog/compile.test.ts +9 -9
  5. package/src/bot/botDeploy.test.ts +16 -16
  6. package/src/bot/botEnvironment.test.ts +8 -8
  7. package/src/bot/botIdentity.test.ts +12 -12
  8. package/src/bot/botPanel.test.ts +13 -13
  9. package/src/bot/botPanel.ts +4 -4
  10. package/src/bot/botState.test.ts +3 -3
  11. package/src/bot/botTerminal.test.ts +5 -5
  12. package/src/bot/botVariables.test.ts +8 -8
  13. package/src/bot/detect/botToken.test.ts +6 -6
  14. package/src/bot/detect/detect.test.ts +21 -21
  15. package/src/bot/detect/detectTable.test.ts +5 -5
  16. package/src/bot/detect/envExample.test.ts +2 -2
  17. package/src/bot/detect/envScan.test.ts +4 -4
  18. package/src/bot/detect/inferBotPlatform.test.ts +4 -4
  19. package/src/bot/detect/keys.test.ts +3 -3
  20. package/src/bot/detect/sensitiveKey.test.ts +5 -5
  21. package/src/bot/detect/serverkJson.test.ts +5 -5
  22. package/src/catalogs/curseforge.test.ts +35 -35
  23. package/src/catalogs/thunderstore.test.ts +9 -9
  24. package/src/cli/detectTable.test.ts +3 -3
  25. package/src/cli/link.test.ts +11 -11
  26. package/src/cli/main.ts +8 -0
  27. package/src/cli/rules.test.ts +198 -0
  28. package/src/cli/rules.ts +188 -0
  29. package/src/cli/validate.test.ts +52 -30
  30. package/src/cli/validate.ts +2 -0
  31. package/src/codecs/ini.test.ts +13 -13
  32. package/src/codecs/javaProperties.test.ts +11 -11
  33. package/src/codecs/sourceCfg.test.ts +14 -14
  34. package/src/codecs/xmlProperties.test.ts +10 -10
  35. package/src/core/bridgePort.test.ts +6 -6
  36. package/src/core/bridgeUserError.test.ts +4 -4
  37. package/src/core/bridgeValue.test.ts +5 -5
  38. package/src/core/describeDownloadFailure.test.ts +12 -12
  39. package/src/core/describeDownloadFailure.ts +10 -10
  40. package/src/guides/markdown.test.ts +31 -31
  41. package/src/install/installStamp.test.ts +9 -9
  42. package/src/manifest/extension.ts +36 -0
  43. package/src/manifest/index.ts +1 -0
  44. package/src/manifest/manifest.test.ts +208 -90
  45. package/src/manifest/manifest.ts +34 -0
  46. package/src/net/loopback.test.ts +10 -10
  47. package/src/presence/rosterSync.test.ts +7 -7
  48. package/src/probes/a2s.test.ts +5 -5
  49. package/src/probes/minecraftPing.test.ts +17 -17
  50. package/src/probes/raknetPing.test.ts +13 -13
  51. package/src/rcon/rconAccess.test.ts +20 -20
  52. package/src/rcon/rconAccess.ts +13 -13
  53. package/src/rcon/rconClient.test.ts +49 -49
  54. package/src/rcon/rconFrame.test.ts +14 -14
  55. package/src/rcon/rconIds.test.ts +4 -4
  56. package/src/rcon/rconWriter.test.ts +6 -6
  57. package/src/runtime/bridgeActivity.test.ts +15 -15
  58. package/src/runtime/bridgeBackupLease.test.ts +9 -9
  59. package/src/runtime/bridgeConfigReader.test.ts +5 -5
  60. package/src/runtime/bridgeConsoleHandler.test.ts +8 -8
  61. package/src/runtime/bridgeContext.test.ts +5 -5
  62. package/src/runtime/bridgeDriver.test.ts +94 -94
  63. package/src/runtime/bridgeEvents.test.ts +15 -15
  64. package/src/runtime/bridgeFiles.test.ts +54 -54
  65. package/src/runtime/bridgeInstall.test.ts +10 -10
  66. package/src/runtime/bridgeLiveConfig.test.ts +6 -6
  67. package/src/runtime/bridgePending.test.ts +6 -6
  68. package/src/runtime/bridgeRedaction.test.ts +16 -16
  69. package/src/runtime/bridgeRegistry.test.ts +52 -52
  70. package/src/runtime/bridgeRegistry.ts +2 -2
  71. package/src/runtime/bridgeServer.test.ts +31 -31
  72. package/src/runtime/bridgeSetup.test.ts +27 -27
  73. package/src/runtime/bridgeSetup.ts +1 -1
  74. package/src/runtime/bridgeSetupHandlers.test.ts +13 -13
  75. package/src/runtime/bridgeSignal.test.ts +7 -7
  76. package/src/runtime/bridgeSupervisor.test.ts +42 -42
  77. package/src/runtime/serve.ts +4 -4
  78. package/src/steam/avatars.test.ts +13 -13
  79. package/src/steam/steamApp.test.ts +8 -8
  80. package/src/steam/steamcmd.test.ts +17 -17
  81. package/src/steam/steamcmd.ts +4 -4
  82. package/src/steam/steamcmdNarrator.test.ts +4 -4
  83. package/src/steam/workshop.test.ts +5 -5
  84. package/src/utils/changedSettingKeys.test.ts +5 -5
  85. package/src/utils/describeExecFailure.test.ts +4 -4
  86. package/src/utils/describeExecFailure.ts +1 -1
  87. package/src/utils/sameSettingValue.test.ts +7 -7
  88. package/src/utils/token.test.ts +7 -7
  89. package/src/wine/protonCommand.test.ts +5 -5
  90. package/src/wine/wineCommand.test.ts +6 -6
  91. package/src/wine/winePrefix.test.ts +6 -6
  92. package/src/wire/activity.test.ts +8 -8
  93. package/src/wire/panel.test.ts +28 -28
@@ -0,0 +1,198 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import {
6
+ RULES_FILE,
7
+ readShippedRules,
8
+ rulesReportLines,
9
+ rulesState,
10
+ runRules,
11
+ SHIPPED_RULES_PATH,
12
+ writeRules,
13
+ } from "./rules";
14
+
15
+ const SHIPPED = "# Serverk package\n\nthe conventions\n";
16
+
17
+ describe("rules", () => {
18
+ let root = "";
19
+ let target = "";
20
+
21
+ const writeTarget = (text: string) => {
22
+ mkdirSync(dirname(target), {
23
+ recursive: true,
24
+ });
25
+ writeFileSync(target, text);
26
+ };
27
+
28
+ beforeEach(() => {
29
+ root = mkdtempSync(`${tmpdir()}/serverk-rules-`);
30
+ target = join(root, RULES_FILE);
31
+ });
32
+
33
+ afterEach(() => {
34
+ rmSync(root, {
35
+ force: true,
36
+ recursive: true,
37
+ });
38
+ });
39
+
40
+ it("writes the file and its folders when it is missing", () => {
41
+ expect(writeRules(root, SHIPPED)).toEqual({
42
+ kind: "written",
43
+ });
44
+
45
+ expect(readFileSync(target, "utf8")).toBe(SHIPPED);
46
+ });
47
+
48
+ it("rewrites a file that is out of date", () => {
49
+ writeTarget("# edited by hand\n");
50
+
51
+ expect(writeRules(root, SHIPPED)).toEqual({
52
+ kind: "written",
53
+ });
54
+
55
+ expect(readFileSync(target, "utf8")).toBe(SHIPPED);
56
+ });
57
+
58
+ it("leaves an identical file untouched", () => {
59
+ writeTarget(SHIPPED);
60
+
61
+ const before = statSync(target).mtimeMs;
62
+
63
+ expect(writeRules(root, SHIPPED)).toEqual({
64
+ kind: "unchanged",
65
+ });
66
+
67
+ expect(statSync(target).mtimeMs).toBe(before);
68
+ });
69
+
70
+ it("reports a target it cannot write instead of throwing", () => {
71
+ writeFileSync(join(root, ".claude"), "a file where the folder should be");
72
+
73
+ const result = writeRules(root, SHIPPED);
74
+
75
+ expect(result.kind).toBe("failed");
76
+ expect(existsSync(target)).toBe(false);
77
+ });
78
+
79
+ it("names the state of the file on disk", () => {
80
+ expect(rulesState(root, SHIPPED)).toBe("missing");
81
+
82
+ writeTarget("# stale\n");
83
+
84
+ expect(rulesState(root, SHIPPED)).toBe("stale");
85
+
86
+ writeTarget(SHIPPED);
87
+
88
+ expect(rulesState(root, SHIPPED)).toBe("current");
89
+ });
90
+
91
+ it("writes the shipped rules when run", async () => {
92
+ await runRules([
93
+ "--dir",
94
+ root,
95
+ ]);
96
+
97
+ expect(readFileSync(target, "utf8")).toBe(readShippedRules());
98
+ });
99
+
100
+ it("succeeds without writing when the target cannot be written", async () => {
101
+ writeFileSync(join(root, ".claude"), "a file where the folder should be");
102
+
103
+ await expect(
104
+ runRules([
105
+ "--dir",
106
+ root,
107
+ ]),
108
+ ).resolves.toBeUndefined();
109
+ });
110
+
111
+ it("check fails when the file is missing and writes nothing", async () => {
112
+ await expect(
113
+ runRules([
114
+ "--dir",
115
+ root,
116
+ "--check",
117
+ ]),
118
+ ).rejects.toThrow(`${RULES_FILE} is missing — run "serverk-bridge rules"`);
119
+
120
+ expect(existsSync(target)).toBe(false);
121
+ });
122
+
123
+ it("check fails when the file is out of date and leaves it alone", async () => {
124
+ writeTarget("# stale\n");
125
+
126
+ await expect(
127
+ runRules([
128
+ "--dir",
129
+ root,
130
+ "--check",
131
+ ]),
132
+ ).rejects.toThrow(`${RULES_FILE} is out of date — run "serverk-bridge rules"`);
133
+
134
+ expect(readFileSync(target, "utf8")).toBe("# stale\n");
135
+ });
136
+
137
+ it("check passes when the file matches the shipped rules", async () => {
138
+ writeTarget(readShippedRules());
139
+
140
+ await expect(
141
+ runRules([
142
+ "--dir",
143
+ root,
144
+ "--check",
145
+ ]),
146
+ ).resolves.toBeUndefined();
147
+ });
148
+
149
+ it("validate warns about a missing or stale file and says nothing about a current one", () => {
150
+ expect(rulesReportLines(root)).toEqual([
151
+ ` warning: ${RULES_FILE} is missing — run "serverk-bridge rules"`,
152
+ ]);
153
+
154
+ writeTarget("# stale\n");
155
+
156
+ expect(rulesReportLines(root)).toEqual([
157
+ ` warning: ${RULES_FILE} is out of date — run "serverk-bridge rules"`,
158
+ ]);
159
+
160
+ writeTarget(readShippedRules());
161
+
162
+ expect(rulesReportLines(root)).toEqual([]);
163
+ });
164
+ });
165
+
166
+ describe("the shipped rules", () => {
167
+ const packageRoot = join(dirname(SHIPPED_RULES_PATH), "..");
168
+
169
+ it("sit at rules/package.md beside the bridge's package.json", () => {
170
+ expect(SHIPPED_RULES_PATH).toBe(join(packageRoot, "rules/package.md"));
171
+ expect(existsSync(join(packageRoot, "package.json"))).toBe(true);
172
+ });
173
+
174
+ it("are published with the module", () => {
175
+ const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
176
+
177
+ expect(manifest.name).toBe("@serverkgg/bridge");
178
+ expect(manifest.files).toContain("rules");
179
+ });
180
+
181
+ it("always load, with no paths frontmatter", () => {
182
+ const text = readShippedRules();
183
+
184
+ expect(text.startsWith("---")).toBe(false);
185
+ expect(text).not.toMatch(/^paths:/m);
186
+ });
187
+
188
+ it("send the reader to the project instructions in ../serverk/CLAUDE.md", () => {
189
+ expect(readShippedRules()).toContain("../serverk/CLAUDE.md");
190
+ });
191
+
192
+ it("say they are generated and must not be edited in place", () => {
193
+ const opening = readShippedRules().split("\n").slice(0, 3).join("\n");
194
+
195
+ expect(opening).toContain("serverk-bridge rules");
196
+ expect(opening).toContain("Do not edit it here");
197
+ });
198
+ });
@@ -0,0 +1,188 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseArgs } from "node:util";
5
+ import { errorText } from "../runtime/bridgeLog";
6
+
7
+ export const RULES_FILE = ".claude/rules/package.md";
8
+
9
+ export const SHIPPED_RULES_PATH = fileURLToPath(new URL("../../rules/package.md", import.meta.url));
10
+
11
+ const RULES_COMMAND = "serverk-bridge rules";
12
+
13
+ const HELP = `serverk-bridge rules [options]
14
+
15
+ write ${RULES_FILE}, the conventions for package code, from the bridge this package pins.
16
+ the file is generated on every install, so it is gitignored and never edited in place.
17
+ a write that fails is reported and never fails the install.
18
+
19
+ options:
20
+ --dir <path> package directory (default: current directory)
21
+ --check change nothing, fail when the file is missing or out of date
22
+ --help show this help`;
23
+
24
+ export type RulesState = "current" | "missing" | "stale";
25
+
26
+ export type RulesResult =
27
+ | {
28
+ kind: "unchanged";
29
+ }
30
+ | {
31
+ kind: "written";
32
+ }
33
+ | {
34
+ kind: "failed";
35
+ reason: string;
36
+ };
37
+
38
+ export const readShippedRules = () => {
39
+ return readFileSync(SHIPPED_RULES_PATH, "utf8");
40
+ };
41
+
42
+ const rulesPath = (dir: string) => {
43
+ return join(resolve(dir), RULES_FILE);
44
+ };
45
+
46
+ const readCurrent = (dir: string) => {
47
+ try {
48
+ return readFileSync(rulesPath(dir), "utf8");
49
+ } catch {
50
+ return null;
51
+ }
52
+ };
53
+
54
+ export const rulesState = (dir: string, shipped: string): RulesState => {
55
+ const current = readCurrent(dir);
56
+
57
+ if (current === null) {
58
+ return "missing";
59
+ }
60
+
61
+ return current === shipped ? "current" : "stale";
62
+ };
63
+
64
+ export const writeRules = (dir: string, shipped: string): RulesResult => {
65
+ if (rulesState(dir, shipped) === "current") {
66
+ return {
67
+ kind: "unchanged",
68
+ };
69
+ }
70
+
71
+ const target = rulesPath(dir);
72
+
73
+ try {
74
+ mkdirSync(dirname(target), {
75
+ recursive: true,
76
+ });
77
+ writeFileSync(target, shipped);
78
+ } catch (error) {
79
+ return {
80
+ kind: "failed",
81
+ reason: errorText(error),
82
+ };
83
+ }
84
+
85
+ return {
86
+ kind: "written",
87
+ };
88
+ };
89
+
90
+ export const rulesReportLines = (dir: string): string[] => {
91
+ let shipped: string;
92
+
93
+ try {
94
+ shipped = readShippedRules();
95
+ } catch {
96
+ return [];
97
+ }
98
+
99
+ const state = rulesState(dir, shipped);
100
+
101
+ if (state === "current") {
102
+ return [];
103
+ }
104
+
105
+ return [
106
+ ` warning: ${RULES_FILE} is ${state === "missing" ? "missing" : "out of date"} — run "${RULES_COMMAND}"`,
107
+ ];
108
+ };
109
+
110
+ const checkRules = (dir: string) => {
111
+ const state = rulesState(dir, readShippedRules());
112
+
113
+ if (state === "missing") {
114
+ throw new Error(`${RULES_FILE} is missing — run "${RULES_COMMAND}"`);
115
+ }
116
+
117
+ if (state === "stale") {
118
+ throw new Error(`${RULES_FILE} is out of date — run "${RULES_COMMAND}"`);
119
+ }
120
+
121
+ console.log(`${RULES_FILE} is up to date`);
122
+ };
123
+
124
+ const applyRules = (dir: string) => {
125
+ let shipped: string;
126
+
127
+ try {
128
+ shipped = readShippedRules();
129
+ } catch (error) {
130
+ console.error(`skipped ${RULES_FILE}: the bridge ships no rules (${errorText(error)})`);
131
+
132
+ return;
133
+ }
134
+
135
+ const result = writeRules(dir, shipped);
136
+
137
+ switch (result.kind) {
138
+ case "unchanged": {
139
+ return;
140
+ }
141
+
142
+ case "written": {
143
+ console.log(`wrote ${RULES_FILE}`);
144
+
145
+ return;
146
+ }
147
+
148
+ case "failed": {
149
+ console.error(`could not write ${RULES_FILE}: ${result.reason}`);
150
+
151
+ return;
152
+ }
153
+ }
154
+ };
155
+
156
+ export const runRules = async (args: string[]) => {
157
+ const { values } = parseArgs({
158
+ args,
159
+ options: {
160
+ dir: {
161
+ type: "string",
162
+ default: ".",
163
+ },
164
+ check: {
165
+ type: "boolean",
166
+ default: false,
167
+ },
168
+ help: {
169
+ type: "boolean",
170
+ default: false,
171
+ },
172
+ },
173
+ });
174
+
175
+ if (values.help) {
176
+ console.log(HELP);
177
+
178
+ return;
179
+ }
180
+
181
+ if (values.check) {
182
+ checkRules(values.dir);
183
+
184
+ return;
185
+ }
186
+
187
+ applyRules(values.dir);
188
+ };
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
1
+ import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
@@ -16,6 +16,7 @@ import {
16
16
  type BridgeTab,
17
17
  type BridgeTableSection,
18
18
  } from "../panel";
19
+ import { RULES_FILE, readShippedRules, writeRules } from "./rules";
19
20
  import { availabilityReportLines, placementReportLines, runValidate, setupReportLines } from "./validate";
20
21
 
21
22
  const text = (value: string) => ({
@@ -54,7 +55,7 @@ describe("validate", () => {
54
55
  });
55
56
  });
56
57
 
57
- test("refuses a file: bridge dependency", async () => {
58
+ it("refuses a file: bridge dependency", async () => {
58
59
  writePackage({
59
60
  "@serverkgg/bridge": "file:../serverk/modules/bridge",
60
61
  });
@@ -64,7 +65,7 @@ describe("validate", () => {
64
65
  );
65
66
  });
66
67
 
67
- test("refuses a link: bridge dependency", async () => {
68
+ it("refuses a link: bridge dependency", async () => {
68
69
  writePackage({
69
70
  "@serverkgg/bridge": "link:@serverkgg/bridge",
70
71
  });
@@ -72,7 +73,7 @@ describe("validate", () => {
72
73
  await expect(validate()).rejects.toThrow('depends on @serverkgg/bridge via "link:@serverkgg/bridge"');
73
74
  });
74
75
 
75
- test("refuses a file: bridge devDependency", async () => {
76
+ it("refuses a file: bridge devDependency", async () => {
76
77
  writeFileSync(
77
78
  join(root, "package.json"),
78
79
  JSON.stringify({
@@ -86,7 +87,7 @@ describe("validate", () => {
86
87
  await expect(validate()).rejects.toThrow("pin a published version");
87
88
  });
88
89
 
89
- test("accepts a published bridge dependency and moves on to the manifest", async () => {
90
+ it("accepts a published bridge dependency and moves on to the manifest", async () => {
90
91
  writePackage({
91
92
  "@serverkgg/bridge": "0.0.3",
92
93
  });
@@ -94,7 +95,7 @@ describe("validate", () => {
94
95
  await expect(validate()).rejects.toThrow("missing serverk.yml");
95
96
  });
96
97
 
97
- test("ignores a package without a bridge dependency", async () => {
98
+ it("ignores a package without a bridge dependency", async () => {
98
99
  writePackage({
99
100
  zod: "4.4.3",
100
101
  });
@@ -102,19 +103,19 @@ describe("validate", () => {
102
103
  await expect(validate()).rejects.toThrow("missing serverk.yml");
103
104
  });
104
105
 
105
- test("ignores a package.json that is absent", async () => {
106
+ it("ignores a package.json that is absent", async () => {
106
107
  await expect(validate()).rejects.toThrow("missing serverk.yml");
107
108
  });
108
109
  });
109
110
 
110
111
  describe("the setup report", () => {
111
- test("says none for a driver that declares no setup", () => {
112
+ it("says none for a driver that declares no setup", () => {
112
113
  expect(setupReportLines(undefined)).toEqual([
113
114
  " setup: none",
114
115
  ]);
115
116
  });
116
117
 
117
- test("names every step, its kind, whether it is required, and where it lands", () => {
118
+ it("names every step, its kind, whether it is required, and where it lands", () => {
118
119
  const setup: BridgeSetupModule = {
119
120
  kind: BridgeKind.Setup,
120
121
  steps: [
@@ -138,8 +139,8 @@ describe("the setup report", () => {
138
139
  },
139
140
  {
140
141
  kind: BridgeSetupStepKind.Open,
141
- id: "txadmin",
142
- title: text("txAdmin"),
142
+ id: "dashboard",
143
+ title: text("Dashboard"),
143
144
  required: false,
144
145
  target: {
145
146
  tab: GuideOpenTab.Panel,
@@ -165,12 +166,12 @@ describe("the setup report", () => {
165
166
  " setup: 4 step(s)",
166
167
  " license — form, required → license/license [LICENSE_KEY]",
167
168
  " verify — driver, required → driver (needs the game running)",
168
- ' txadmin — open, optional → panel tab "license"',
169
+ ' dashboard — open, optional → panel tab "license"',
169
170
  " invite — open, optional → access",
170
171
  ]);
171
172
  });
172
173
 
173
- test("leaves a driver step that needs nothing running with no target", () => {
174
+ it("leaves a driver step that needs nothing running with no target", () => {
174
175
  expect(
175
176
  setupReportLines({
176
177
  kind: BridgeKind.Setup,
@@ -192,7 +193,7 @@ describe("the setup report", () => {
192
193
  ]);
193
194
  });
194
195
 
195
- test("reads an omitted required as required, the way the platform does", () => {
196
+ it("reads an omitted required as required, the way the platform does", () => {
196
197
  expect(
197
198
  setupReportLines({
198
199
  kind: BridgeKind.Setup,
@@ -259,7 +260,7 @@ describe("the placement report", () => {
259
260
  },
260
261
  });
261
262
 
262
- test("says nothing about a roster table that declares where it belongs", () => {
263
+ it("says nothing about a roster table that declares where it belongs", () => {
263
264
  expect(
264
265
  placementReportLines(
265
266
  driverWithTabs([
@@ -277,7 +278,7 @@ describe("the placement report", () => {
277
278
  ]);
278
279
  });
279
280
 
280
- test("warns that the platform absorbs an unplaced players collection by convention", () => {
281
+ it("warns that the platform absorbs an unplaced players collection by convention", () => {
281
282
  expect(
282
283
  placementReportLines(
283
284
  driverWithTabs([
@@ -295,7 +296,7 @@ describe("the placement report", () => {
295
296
  ]);
296
297
  });
297
298
 
298
- test("warns about a tab that takes the name of a platform sidebar entry", () => {
299
+ it("warns about a tab that takes the name of a platform sidebar entry", () => {
299
300
  expect(
300
301
  placementReportLines(
301
302
  driverWithTabs([
@@ -313,7 +314,7 @@ describe("the placement report", () => {
313
314
  ]);
314
315
  });
315
316
 
316
- test("leaves a players tab alone while it is the roster tab", () => {
317
+ it("leaves a players tab alone while it is the roster tab", () => {
317
318
  expect(
318
319
  placementReportLines(
319
320
  driverWithTabs([
@@ -331,7 +332,7 @@ describe("the placement report", () => {
331
332
  ]);
332
333
  });
333
334
 
334
- test("warns about a players tab that holds no roster", () => {
335
+ it("warns about a players tab that holds no roster", () => {
335
336
  expect(
336
337
  placementReportLines(
337
338
  driverWithTabs([
@@ -349,7 +350,7 @@ describe("the placement report", () => {
349
350
  ]);
350
351
  });
351
352
 
352
- test("says nothing about a tab that declares no sections at all", () => {
353
+ it("says nothing about a tab that declares no sections at all", () => {
353
354
  expect(
354
355
  placementReportLines(
355
356
  driverWithTabs([
@@ -363,7 +364,7 @@ describe("the placement report", () => {
363
364
  ).toEqual([]);
364
365
  });
365
366
 
366
- test("still warns about an empty players tab, which holds no roster", () => {
367
+ it("still warns about an empty players tab, which holds no roster", () => {
367
368
  expect(
368
369
  placementReportLines(
369
370
  driverWithTabs([
@@ -379,7 +380,7 @@ describe("the placement report", () => {
379
380
  ]);
380
381
  });
381
382
 
382
- test("says nothing about a tab that keeps a leftover section", () => {
383
+ it("says nothing about a tab that keeps a leftover section", () => {
383
384
  expect(
384
385
  placementReportLines(
385
386
  driverWithTabs([
@@ -493,6 +494,7 @@ describe("validating a bot package", () => {
493
494
  writeFileSync(join(root, "assets/logo.webp"), webp(256, 256));
494
495
  writeFileSync(join(root, "assets/banner.webp"), webp(1280, 720));
495
496
  writeFileSync(join(root, "src/driver.ts"), BOT_DRIVER);
497
+ writeRules(root, readShippedRules());
496
498
  };
497
499
 
498
500
  const validate = () => {
@@ -521,7 +523,7 @@ describe("validating a bot package", () => {
521
523
  });
522
524
  });
523
525
 
524
- test("validates a bot with no ports and says which kind it read", async () => {
526
+ it("validates a bot with no ports and says which kind it read", async () => {
525
527
  writeBot(botManifest());
526
528
 
527
529
  await validate();
@@ -535,7 +537,27 @@ describe("validating a bot package", () => {
535
537
  expect(lines.some((line) => line.includes("warning"))).toBe(false);
536
538
  });
537
539
 
538
- test("says when a bot turns the environment vault on", async () => {
540
+ it("warns about a missing rule file and still validates", async () => {
541
+ writeBot(botManifest());
542
+ rmSync(join(root, RULES_FILE));
543
+
544
+ await validate();
545
+
546
+ expect(lines).toContain("bot-node is valid");
547
+ expect(lines).toContain(` warning: ${RULES_FILE} is missing — run "serverk-bridge rules"`);
548
+ });
549
+
550
+ it("warns about a stale rule file and still validates", async () => {
551
+ writeBot(botManifest());
552
+ writeFileSync(join(root, RULES_FILE), "# edited by hand\n");
553
+
554
+ await validate();
555
+
556
+ expect(lines).toContain("bot-node is valid");
557
+ expect(lines).toContain(` warning: ${RULES_FILE} is out of date — run "serverk-bridge rules"`);
558
+ });
559
+
560
+ it("says when a bot turns the environment vault on", async () => {
539
561
  writeBot(
540
562
  botManifest({
541
563
  environment: {
@@ -549,7 +571,7 @@ describe("validating a bot package", () => {
549
571
  expect(lines).toContain(" environment: vault on");
550
572
  });
551
573
 
552
- test("fails a bot whose detection table is missing", async () => {
574
+ it("fails a bot whose detection table is missing", async () => {
553
575
  writeBot(botManifest(), null);
554
576
 
555
577
  await expect(validate()).rejects.toThrow(
@@ -557,7 +579,7 @@ describe("validating a bot package", () => {
557
579
  );
558
580
  });
559
581
 
560
- test("fails a bot whose detection table is stale", async () => {
582
+ it("fails a bot whose detection table is stale", async () => {
561
583
  writeBot(botManifest(), "{}\n");
562
584
 
563
585
  await expect(validate()).rejects.toThrow(
@@ -565,7 +587,7 @@ describe("validating a bot package", () => {
565
587
  );
566
588
  });
567
589
 
568
- test("warns about a port on a bot but still validates it", async () => {
590
+ it("warns about a port on a bot but still validates it", async () => {
569
591
  writeBot(
570
592
  botManifest({
571
593
  container: {
@@ -585,7 +607,7 @@ describe("validating a bot package", () => {
585
607
  );
586
608
  });
587
609
 
588
- test("fails a bot that declares platform secrets", async () => {
610
+ it("fails a bot that declares platform secrets", async () => {
589
611
  writeBot(
590
612
  botManifest({
591
613
  secrets: [
@@ -620,7 +642,7 @@ describe("availability report", () => {
620
642
  modules,
621
643
  });
622
644
 
623
- test("warns about a protected module that cannot say when its action is unavailable", () => {
645
+ it("warns about a protected module that cannot say when its action is unavailable", () => {
624
646
  expect(
625
647
  availabilityReportLines(
626
648
  driverWithModules({
@@ -643,7 +665,7 @@ describe("availability report", () => {
643
665
  ]);
644
666
  });
645
667
 
646
- test("says nothing when the protected module declares the hook or protects nothing", () => {
668
+ it("says nothing when the protected module declares the hook or protects nothing", () => {
647
669
  expect(
648
670
  availabilityReportLines(
649
671
  driverWithModules({
@@ -31,6 +31,7 @@ import {
31
31
  } from "../protocol";
32
32
  import { validateDriver, validatePortActivation } from "../runtime/bridgeDriver";
33
33
  import { directorySource } from "./directorySource";
34
+ import { rulesReportLines } from "./rules";
34
35
 
35
36
  const HELP = `serverk-bridge validate [options]
36
37
 
@@ -446,6 +447,7 @@ const validateRunnablePackage = async (
446
447
  ...botPortLines(manifest),
447
448
  ...botEnvironmentLines(manifest),
448
449
  ...detectLines,
450
+ ...rulesReportLines(dir),
449
451
  ]) {
450
452
  console.log(line);
451
453
  }