@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.
- package/CHANGELOG.md +124 -0
- package/package.json +5 -5
- package/src/concurrency.test.ts +147 -0
- package/src/execute-collector.test.ts +47 -9
- package/src/execute-collector.ts +80 -16
- package/src/inline-script-collector.test.ts +165 -58
- package/src/inline-script-collector.ts +152 -229
- package/src/security.test.ts +63 -7
- package/src/strategy.test.ts +7 -14
- package/src/strategy.ts +17 -72
- package/src/transport-client.ts +10 -3
|
@@ -8,14 +8,15 @@ import type { ScriptTransportClient } from "./transport-client";
|
|
|
8
8
|
/**
|
|
9
9
|
* Unit tests for the Inline Script Collector.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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: "
|
|
33
|
-
timeout:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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("
|
|
100
|
+
it("supports top-level await", async () => {
|
|
93
101
|
const config = createConfig({
|
|
94
|
-
script:
|
|
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("
|
|
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
|
-
|
|
110
|
-
|
|
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.
|
|
162
|
+
expect(result.result.value).toBe(7);
|
|
163
|
+
expect(result.result.message).toBe("via helper");
|
|
122
164
|
});
|
|
123
165
|
|
|
124
|
-
it("
|
|
166
|
+
it("supports `export default async function (context)` callback form", async () => {
|
|
125
167
|
const config = createConfig({
|
|
126
|
-
script:
|
|
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.
|
|
181
|
+
expect(result.result.success).toBe(true);
|
|
182
|
+
expect(result.result.value).toBe(1);
|
|
136
183
|
});
|
|
137
184
|
});
|
|
138
185
|
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
|
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("
|
|
256
|
+
it("treats `return { success: false, message: ... }` as failure", async () => {
|
|
160
257
|
const config = createConfig({
|
|
161
|
-
script:
|
|
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("
|
|
272
|
+
it("captures thrown errors", async () => {
|
|
176
273
|
const config = createConfig({
|
|
177
|
-
script:
|
|
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("
|
|
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
|
-
|
|
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(
|
|
215
|
-
|
|
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
|
-
},
|
|
320
|
+
}, 15_000);
|
|
230
321
|
});
|
|
231
322
|
|
|
232
|
-
|
|
233
|
-
|
|
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);
|
|
253
|
-
expect(aggregated.successRate.rate).toBeCloseTo(67, 0);
|
|
359
|
+
expect(aggregated.avgExecutionTimeMs.avg).toBe(150);
|
|
360
|
+
expect(aggregated.successRate.rate).toBeCloseTo(67, 0);
|
|
254
361
|
});
|
|
255
362
|
});
|
|
256
363
|
});
|