@checkstack/healthcheck-script-backend 0.3.16 → 0.4.0

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.
@@ -8,14 +8,15 @@ import type { ScriptTransportClient } from "./transport-client";
8
8
  /**
9
9
  * Unit tests for the Inline Script Collector.
10
10
  *
11
- * Tests cover:
12
- * - Basic script execution
13
- * - Return value handling
14
- * - Error handling
15
- * - Timeout protection
11
+ * The collector runs each script in a freshly-spawned Bun subprocess
12
+ * against a temp `.mjs` file. The subprocess overhead is ~10-20 ms per
13
+ * test, which is acceptable for the small number of cases here and keeps
14
+ * the tests close to what users actually experience at runtime (real
15
+ * ESM, real `node:*` imports, real `process.exit()`).
16
16
  */
17
17
 
18
- // Mock client (not actually used by InlineScriptCollector, but required by interface)
18
+ // Mock transport client the inline-script collector doesn't actually
19
+ // use it, but the CollectorStrategy interface requires one.
19
20
  const mockClient: ScriptTransportClient = {
20
21
  exec: async () => ({
21
22
  exitCode: 0,
@@ -29,8 +30,8 @@ function createConfig(
29
30
  overrides: Partial<InlineScriptConfig> = {},
30
31
  ): InlineScriptConfig {
31
32
  return {
32
- script: "return { success: true };",
33
- timeout: 5000,
33
+ script: "export default { success: true };",
34
+ timeout: 10_000,
34
35
  ...overrides,
35
36
  };
36
37
  }
@@ -38,10 +39,6 @@ function createConfig(
38
39
  describe("InlineScriptCollector", () => {
39
40
  const collector = new InlineScriptCollector();
40
41
 
41
- // ─────────────────────────────────────────────────────────────
42
- // Metadata
43
- // ─────────────────────────────────────────────────────────────
44
-
45
42
  describe("metadata", () => {
46
43
  it("has correct basic metadata", () => {
47
44
  expect(collector.id).toBe("inline-script");
@@ -54,14 +51,10 @@ describe("InlineScriptCollector", () => {
54
51
  });
55
52
  });
56
53
 
57
- // ─────────────────────────────────────────────────────────────
58
- // Basic Execution
59
- // ─────────────────────────────────────────────────────────────
60
-
61
- describe("execute - basic", () => {
62
- it("executes script returning success object", async () => {
54
+ describe("modern ESM scripts", () => {
55
+ it("executes a script with `export default {object}`", async () => {
63
56
  const config = createConfig({
64
- script: 'return { success: true, message: "All good" };',
57
+ script: `export default { success: true, message: "All good", value: 42 };`,
65
58
  });
66
59
 
67
60
  const result = await collector.execute({
@@ -72,12 +65,25 @@ describe("InlineScriptCollector", () => {
72
65
 
73
66
  expect(result.result.success).toBe(true);
74
67
  expect(result.result.message).toBe("All good");
68
+ expect(result.result.value).toBe(42);
75
69
  expect(result.error).toBeUndefined();
76
70
  });
77
71
 
78
- it("executes script returning boolean", async () => {
72
+ it("executes a script that imports from node:os", async () => {
73
+ // This is the user-reported regression: `import { loadavg } from "os"`
74
+ // failed at parse time with "Unexpected token '{'" because the
75
+ // previous executor wrapped scripts in `new Function(...)`. Real
76
+ // ESM execution makes it just work.
79
77
  const config = createConfig({
80
- script: "return true;",
78
+ script: `
79
+ import { loadavg, cpus } from "node:os";
80
+ const load = loadavg()[0];
81
+ export default {
82
+ success: load >= 0,
83
+ message: \`cpus=\${cpus().length} load=\${load.toFixed(2)}\`,
84
+ value: load,
85
+ };
86
+ `,
81
87
  });
82
88
 
83
89
  const result = await collector.execute({
@@ -87,11 +93,16 @@ describe("InlineScriptCollector", () => {
87
93
  });
88
94
 
89
95
  expect(result.result.success).toBe(true);
96
+ expect(typeof result.result.value).toBe("number");
97
+ expect(result.result.message).toMatch(/cpus=\d+ load=/);
90
98
  });
91
99
 
92
- it("executes script with no return (defaults to success)", async () => {
100
+ it("supports top-level await", async () => {
93
101
  const config = createConfig({
94
- script: "const x = 1 + 1;",
102
+ script: `
103
+ await new Promise((r) => setTimeout(r, 10));
104
+ export default { success: true, message: "awaited" };
105
+ `,
95
106
  });
96
107
 
97
108
  const result = await collector.execute({
@@ -101,13 +112,43 @@ describe("InlineScriptCollector", () => {
101
112
  });
102
113
 
103
114
  expect(result.result.success).toBe(true);
115
+ expect(result.result.message).toBe("awaited");
104
116
  });
105
117
 
106
- it("executes async script", async () => {
118
+ it("exposes `defineHealthCheck` as a runtime global (no import required)", async () => {
119
+ // The editor advertises `defineHealthCheck` both as an ambient global
120
+ // and as a named export from `@checkstack/healthcheck`. The global
121
+ // form lets Monaco autocomplete `defineHea…` on a fresh module
122
+ // without the user typing an import first. This test confirms the
123
+ // runner actually installs the global onto `globalThis` so the
124
+ // script runs.
125
+ const config = createConfig({
126
+ script: `export default defineHealthCheck({ success: true, value: 99, message: "via global" });`,
127
+ });
128
+
129
+ const result = await collector.execute({
130
+ config,
131
+ client: mockClient,
132
+ pluginId: "script",
133
+ });
134
+
135
+ expect(result.result.success).toBe(true);
136
+ expect(result.result.value).toBe(99);
137
+ expect(result.result.message).toBe("via global");
138
+ });
139
+
140
+ it("resolves the virtual `@checkstack/healthcheck` defineHealthCheck helper at runtime", async () => {
141
+ // The editor advertises `import { defineHealthCheck } from
142
+ // "@checkstack/healthcheck"` for IntelliSense + return-shape type
143
+ // checking. At runtime, the collector rewrites that import to a
144
+ // sibling helper file. This test verifies the rewrite actually
145
+ // resolves — if it ever regressed (e.g. the regex stopped matching),
146
+ // the script would throw "Cannot find module @checkstack/healthcheck"
147
+ // and we'd catch it here.
107
148
  const config = createConfig({
108
149
  script: `
109
- await Promise.resolve();
110
- return { success: true, message: "async works" };
150
+ import { defineHealthCheck } from "@checkstack/healthcheck";
151
+ export default defineHealthCheck({ success: true, value: 7, message: "via helper" });
111
152
  `,
112
153
  });
113
154
 
@@ -118,12 +159,17 @@ describe("InlineScriptCollector", () => {
118
159
  });
119
160
 
120
161
  expect(result.result.success).toBe(true);
121
- expect(result.result.message).toBe("async works");
162
+ expect(result.result.value).toBe(7);
163
+ expect(result.result.message).toBe("via helper");
122
164
  });
123
165
 
124
- it("captures numeric value from script", async () => {
166
+ it("supports `export default async function (context)` callback form", async () => {
125
167
  const config = createConfig({
126
- script: "return { success: true, value: 42 };",
168
+ script: `
169
+ export default async function (ctx) {
170
+ return { success: true, value: typeof ctx === "object" ? 1 : 0 };
171
+ };
172
+ `,
127
173
  });
128
174
 
129
175
  const result = await collector.execute({
@@ -132,18 +178,54 @@ describe("InlineScriptCollector", () => {
132
178
  pluginId: "script",
133
179
  });
134
180
 
135
- expect(result.result.value).toBe(42);
181
+ expect(result.result.success).toBe(true);
182
+ expect(result.result.value).toBe(1);
136
183
  });
137
184
  });
138
185
 
139
- // ─────────────────────────────────────────────────────────────
140
- // Failure Handling
141
- // ─────────────────────────────────────────────────────────────
186
+ describe("legacy IIFE-style scripts (backwards compatibility)", () => {
187
+ it("still supports `return { success: true }` at the top level", async () => {
188
+ const config = createConfig({
189
+ script: `return { success: true, message: "All good" };`,
190
+ });
191
+
192
+ const result = await collector.execute({
193
+ config,
194
+ client: mockClient,
195
+ pluginId: "script",
196
+ });
197
+
198
+ expect(result.result.success).toBe(true);
199
+ expect(result.result.message).toBe("All good");
200
+ });
201
+
202
+ it("still supports `return true`", async () => {
203
+ const config = createConfig({ script: "return true;" });
204
+
205
+ const result = await collector.execute({
206
+ config,
207
+ client: mockClient,
208
+ pluginId: "script",
209
+ });
142
210
 
143
- describe("execute - failures", () => {
144
- it("handles script returning false", async () => {
211
+ expect(result.result.success).toBe(true);
212
+ });
213
+
214
+ it("treats a script with no return as successful", async () => {
215
+ const config = createConfig({ script: "const x = 1 + 1;" });
216
+
217
+ const result = await collector.execute({
218
+ config,
219
+ client: mockClient,
220
+ pluginId: "script",
221
+ });
222
+
223
+ expect(result.result.success).toBe(true);
224
+ });
225
+
226
+ it("captures stdout as the run message when no message is returned", async () => {
145
227
  const config = createConfig({
146
- script: "return false;",
228
+ script: `console.log("hello from script"); return true;`,
147
229
  });
148
230
 
149
231
  const result = await collector.execute({
@@ -152,13 +234,28 @@ describe("InlineScriptCollector", () => {
152
234
  pluginId: "script",
153
235
  });
154
236
 
237
+ expect(result.result.success).toBe(true);
238
+ expect(result.result.message).toContain("hello from script");
239
+ });
240
+ });
241
+
242
+ describe("failure handling", () => {
243
+ it("treats `return false` as failure", async () => {
244
+ const config = createConfig({ script: "return false;" });
245
+
246
+ const result = await collector.execute({
247
+ config,
248
+ client: mockClient,
249
+ pluginId: "script",
250
+ });
251
+
155
252
  expect(result.result.success).toBe(false);
156
253
  expect(result.error).toBeDefined();
157
254
  });
158
255
 
159
- it("handles script returning success: false", async () => {
256
+ it("treats `return { success: false, message: ... }` as failure", async () => {
160
257
  const config = createConfig({
161
- script: 'return { success: false, message: "Check failed" };',
258
+ script: `return { success: false, message: "Check failed" };`,
162
259
  });
163
260
 
164
261
  const result = await collector.execute({
@@ -172,9 +269,9 @@ describe("InlineScriptCollector", () => {
172
269
  expect(result.error).toBe("Check failed");
173
270
  });
174
271
 
175
- it("handles script errors", async () => {
272
+ it("captures thrown errors", async () => {
176
273
  const config = createConfig({
177
- script: 'throw new Error("Something went wrong");',
274
+ script: `throw new Error("Something went wrong");`,
178
275
  });
179
276
 
180
277
  const result = await collector.execute({
@@ -187,10 +284,8 @@ describe("InlineScriptCollector", () => {
187
284
  expect(result.error).toContain("Something went wrong");
188
285
  });
189
286
 
190
- it("handles syntax errors", async () => {
191
- const config = createConfig({
192
- script: "const x = {",
193
- });
287
+ it("captures syntax errors", async () => {
288
+ const config = createConfig({ script: "const x = {" });
194
289
 
195
290
  const result = await collector.execute({
196
291
  config,
@@ -203,16 +298,12 @@ describe("InlineScriptCollector", () => {
203
298
  });
204
299
  });
205
300
 
206
- // ─────────────────────────────────────────────────────────────
207
- // Timeout
208
- // ─────────────────────────────────────────────────────────────
209
-
210
- describe("execute - timeout", () => {
211
- it("times out long-running scripts", async () => {
301
+ describe("timeout", () => {
302
+ it("kills long-running scripts when the timeout expires", async () => {
212
303
  const config = createConfig({
213
304
  script: `
214
- await new Promise(resolve => setTimeout(resolve, 5000));
215
- return { success: true };
305
+ await new Promise((r) => setTimeout(r, 5000));
306
+ export default { success: true };
216
307
  `,
217
308
  timeout: 1000,
218
309
  });
@@ -226,12 +317,28 @@ describe("InlineScriptCollector", () => {
226
317
  expect(result.result.success).toBe(false);
227
318
  expect(result.result.timedOut).toBe(true);
228
319
  expect(result.error).toContain("timed out");
229
- }, 10000);
320
+ }, 15_000);
230
321
  });
231
322
 
232
- // ─────────────────────────────────────────────────────────────
233
- // Aggregation
234
- // ─────────────────────────────────────────────────────────────
323
+ describe("context.config", () => {
324
+ it("exposes the collector configuration as `context.config`", async () => {
325
+ const config = createConfig({
326
+ script: `
327
+ // @ts-ignore — context is injected by the runner
328
+ export default { success: true, value: context.config.timeout };
329
+ `,
330
+ });
331
+
332
+ const result = await collector.execute({
333
+ config,
334
+ client: mockClient,
335
+ pluginId: "script",
336
+ });
337
+
338
+ expect(result.result.success).toBe(true);
339
+ expect(result.result.value).toBe(10_000);
340
+ });
341
+ });
235
342
 
236
343
  describe("mergeResult", () => {
237
344
  it("aggregates execution time and success rate", () => {
@@ -249,8 +356,8 @@ describe("InlineScriptCollector", () => {
249
356
  aggregated = collector.mergeResult(aggregated, run2 as never);
250
357
  aggregated = collector.mergeResult(aggregated, run3 as never);
251
358
 
252
- expect(aggregated.avgExecutionTimeMs.avg).toBe(150); // (100+200+150)/3
253
- expect(aggregated.successRate.rate).toBeCloseTo(67, 0); // 2/3 * 100 = ~67
359
+ expect(aggregated.avgExecutionTimeMs.avg).toBe(150);
360
+ expect(aggregated.successRate.rate).toBeCloseTo(67, 0);
254
361
  });
255
362
  });
256
363
  });