@elench/testkit 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.
package/lib/config.mjs CHANGED
@@ -2,6 +2,10 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { fileURLToPath } from "url";
4
4
 
5
+ const RUNNER_MANIFEST = "runner.manifest.json";
6
+ const TESTKIT_CONFIG = "testkit.config.json";
7
+ const VALID_FRAMEWORKS = new Set(["k6", "playwright"]);
8
+
5
9
  /**
6
10
  * Parse a .env file into an object. Supports KEY=VALUE, KEY='VALUE', KEY="VALUE".
7
11
  * Skips comments (#) and blank lines.
@@ -16,8 +20,10 @@ export function parseDotenv(filePath) {
16
20
  if (eq === -1) continue;
17
21
  const key = trimmed.slice(0, eq).trim();
18
22
  let val = trimmed.slice(eq + 1).trim();
19
- if ((val.startsWith("'") && val.endsWith("'")) ||
20
- (val.startsWith('"') && val.endsWith('"'))) {
23
+ if (
24
+ (val.startsWith("'") && val.endsWith("'")) ||
25
+ (val.startsWith('"') && val.endsWith('"'))
26
+ ) {
21
27
  val = val.slice(1, -1);
22
28
  }
23
29
  env[key] = val;
@@ -25,105 +31,62 @@ export function parseDotenv(filePath) {
25
31
  return env;
26
32
  }
27
33
 
28
- /**
29
- * Read the manifest and return the service names.
30
- * Used by the CLI to resolve ambiguous positional args.
31
- */
32
34
  export function getServiceNames(cwd) {
33
35
  const dir = cwd || process.cwd();
34
- const manifestPath = path.join(dir, "testkit.manifest.json");
35
- if (!fs.existsSync(manifestPath)) return [];
36
- const m = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
37
- if (!isObject(m.services)) return [];
38
- return Object.keys(m.services);
36
+ const runnerPath = path.join(dir, RUNNER_MANIFEST);
37
+ if (!fs.existsSync(runnerPath)) return [];
38
+ const runner = JSON.parse(fs.readFileSync(runnerPath, "utf8"));
39
+ if (!isObject(runner.services)) return [];
40
+ return Object.keys(runner.services);
39
41
  }
40
42
 
41
- /**
42
- * Load configs for all (or one) service from the manifest.
43
- * Returns an array of { name, productDir, stateDir, manifest } objects.
44
- *
45
- * opts.service — filter to a single service name
46
- * opts.dir — explicit product directory
47
- */
48
43
  export function loadConfigs(opts = {}) {
49
- const cwd = process.cwd();
50
- const productDir = resolveProductDir(cwd, opts.dir);
51
- const manifestPath = path.join(productDir, "testkit.manifest.json");
52
- const raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
44
+ const productDir = resolveProductDir(process.cwd(), opts.dir);
45
+ const runner = loadRunnerManifest(productDir);
46
+ const config = loadTestkitConfig(productDir);
47
+ validateConfigCoverage(runner, config);
53
48
 
54
- if (!isObject(raw.services)) {
55
- throw new Error(`Manifest must have a "services" object (${manifestPath})`);
56
- }
57
-
58
- // Load product .env into process.env (product wins on conflict)
59
49
  const dotenv = parseDotenv(path.join(productDir, ".env"));
60
50
  Object.assign(process.env, dotenv);
61
51
 
62
- if (!process.env.NEON_API_KEY) {
63
- throw new Error(
64
- `NEON_API_KEY not found. Set it in your shell environment, .envrc, or .env`
65
- );
66
- }
67
-
68
- const entries = Object.entries(raw.services);
69
-
70
- // Filter by service name if requested
52
+ const serviceEntries = Object.entries(runner.services);
71
53
  const filtered = opts.service
72
- ? entries.filter(([name]) => name === opts.service)
73
- : entries;
54
+ ? serviceEntries.filter(([name]) => name === opts.service)
55
+ : serviceEntries;
74
56
 
75
57
  if (opts.service && filtered.length === 0) {
76
- const available = entries.map(([n]) => n).join(", ");
58
+ const available = serviceEntries.map(([name]) => name).join(", ");
77
59
  throw new Error(`Service "${opts.service}" not found. Available: ${available}`);
78
60
  }
79
61
 
80
- return filtered.map(([name, svc]) => {
81
- validateService(name, svc, manifestPath);
82
-
83
- // Check service secrets are available
84
- const tk = svc.testkit;
85
- for (const key of [...(tk.fly?.secrets || []), ...(tk.k6?.secrets || []), ...(tk.dal?.secrets || [])]) {
86
- if (!process.env[key]) {
87
- throw new Error(`Secret "${key}" (service "${name}") not found in env or .env`);
88
- }
89
- }
90
- // Check dependent secrets
91
- for (const dep of tk.depends || []) {
92
- for (const key of dep.fly?.secrets || []) {
93
- if (!process.env[key]) {
94
- throw new Error(`Secret "${key}" (service "${name}", dep "${dep.name}") not found in env or .env`);
95
- }
96
- }
62
+ return filtered.map(([name, runnerService]) => {
63
+ const serviceConfig = config.services[name];
64
+ if (!serviceConfig) {
65
+ throw new Error(
66
+ `Service "${name}" exists in ${RUNNER_MANIFEST} but is missing from ${TESTKIT_CONFIG}`
67
+ );
97
68
  }
98
69
 
99
- // Each service gets its own state dir: .testkit/<service>/
100
- const stateDir = entries.length === 1
101
- ? path.join(productDir, ".testkit")
102
- : path.join(productDir, ".testkit", name);
70
+ validateMergedService(name, runnerService, serviceConfig, productDir);
103
71
 
104
72
  return {
105
73
  name,
106
74
  productDir,
107
- stateDir,
108
- manifest: { testkit: tk, suites: svc.suites },
75
+ stateDir: path.join(productDir, ".testkit", name),
76
+ suites: runnerService.suites,
77
+ testkit: serviceConfig,
109
78
  };
110
79
  });
111
80
  }
112
81
 
113
- /**
114
- * Require FLY_API_TOKEN. Call this only when HTTP tests need Fly infrastructure.
115
- */
116
- export function requireFlyToken(serviceName) {
117
- if (!process.env.FLY_API_TOKEN) {
82
+ export function requireNeonApiKey() {
83
+ if (!process.env.NEON_API_KEY) {
118
84
  throw new Error(
119
- `FLY_API_TOKEN not found. Set it in your shell environment, .envrc, or .env`
85
+ "NEON_API_KEY not found. Set it in your shell environment, .envrc, or .env"
120
86
  );
121
87
  }
122
88
  }
123
89
 
124
- /**
125
- * Resolve the bundled k6-sql binary for DAL tests. Returns the absolute path.
126
- */
127
90
  export function resolveDalBinary() {
128
91
  const thisFile = fileURLToPath(import.meta.url);
129
92
  const abs = path.resolve(path.dirname(thisFile), "..", "vendor", "k6");
@@ -133,150 +96,306 @@ export function resolveDalBinary() {
133
96
  return abs;
134
97
  }
135
98
 
136
- // ── Directory resolution ────────────────────────────────────────────────
99
+ export function resolveServiceCwd(productDir, maybeRelative) {
100
+ return path.resolve(productDir, maybeRelative || ".");
101
+ }
102
+
103
+ function loadRunnerManifest(productDir) {
104
+ const manifestPath = path.join(productDir, RUNNER_MANIFEST);
105
+ const raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
106
+
107
+ if (!isObject(raw.services)) {
108
+ throw new Error(`${RUNNER_MANIFEST} must have a "services" object (${manifestPath})`);
109
+ }
110
+
111
+ for (const [serviceName, service] of Object.entries(raw.services)) {
112
+ if (!isObject(service)) {
113
+ throw new Error(`Service "${serviceName}" in ${RUNNER_MANIFEST} must be an object`);
114
+ }
115
+ if (!isObject(service.suites)) {
116
+ throw new Error(`Service "${serviceName}" in ${RUNNER_MANIFEST} must define suites`);
117
+ }
118
+
119
+ for (const [suiteType, suites] of Object.entries(service.suites)) {
120
+ if (!Array.isArray(suites)) {
121
+ throw new Error(
122
+ `Service "${serviceName}" suite type "${suiteType}" must be an array`
123
+ );
124
+ }
125
+
126
+ const seenNames = new Set();
127
+ for (const suite of suites) {
128
+ if (!isObject(suite)) {
129
+ throw new Error(
130
+ `Service "${serviceName}" suite type "${suiteType}" contains a non-object suite`
131
+ );
132
+ }
133
+ if (typeof suite.name !== "string" || !suite.name.length) {
134
+ throw new Error(
135
+ `Service "${serviceName}" suite type "${suiteType}" has a suite with no name`
136
+ );
137
+ }
138
+ if (seenNames.has(suite.name)) {
139
+ throw new Error(
140
+ `Service "${serviceName}" suite type "${suiteType}" has duplicate suite name "${suite.name}"`
141
+ );
142
+ }
143
+ seenNames.add(suite.name);
144
+
145
+ if (!Array.isArray(suite.files) || suite.files.length === 0) {
146
+ throw new Error(
147
+ `Service "${serviceName}" suite "${suite.name}" must define one or more files`
148
+ );
149
+ }
150
+ for (const file of suite.files) {
151
+ if (typeof file !== "string" || !file.length) {
152
+ throw new Error(
153
+ `Service "${serviceName}" suite "${suite.name}" contains an invalid file entry`
154
+ );
155
+ }
156
+ }
157
+
158
+ const framework = suite.framework || "k6";
159
+ if (!VALID_FRAMEWORKS.has(framework)) {
160
+ throw new Error(
161
+ `Service "${serviceName}" suite "${suite.name}" uses unsupported framework "${framework}"`
162
+ );
163
+ }
164
+
165
+ if (suite.testkit !== undefined) {
166
+ if (!isObject(suite.testkit)) {
167
+ throw new Error(
168
+ `Service "${serviceName}" suite "${suite.name}" testkit config must be an object`
169
+ );
170
+ }
171
+ if (
172
+ suite.testkit.maxFileConcurrency !== undefined &&
173
+ (!Number.isInteger(suite.testkit.maxFileConcurrency) ||
174
+ suite.testkit.maxFileConcurrency <= 0)
175
+ ) {
176
+ throw new Error(
177
+ `Service "${serviceName}" suite "${suite.name}" testkit.maxFileConcurrency must be a positive integer`
178
+ );
179
+ }
180
+ if (
181
+ suite.testkit.weight !== undefined &&
182
+ (!Number.isInteger(suite.testkit.weight) || suite.testkit.weight <= 0)
183
+ ) {
184
+ throw new Error(
185
+ `Service "${serviceName}" suite "${suite.name}" testkit.weight must be a positive integer`
186
+ );
187
+ }
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ return raw;
194
+ }
195
+
196
+ function loadTestkitConfig(productDir) {
197
+ const configPath = path.join(productDir, TESTKIT_CONFIG);
198
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
199
+
200
+ if (!isObject(raw.services)) {
201
+ throw new Error(`${TESTKIT_CONFIG} must have a "services" object (${configPath})`);
202
+ }
203
+
204
+ for (const [serviceName, service] of Object.entries(raw.services)) {
205
+ validateServiceConfig(serviceName, service, configPath);
206
+ }
207
+
208
+ return raw;
209
+ }
210
+
211
+ function validateConfigCoverage(runner, config) {
212
+ for (const serviceName of Object.keys(config.services)) {
213
+ if (!runner.services[serviceName]) {
214
+ throw new Error(
215
+ `Service "${serviceName}" exists in ${TESTKIT_CONFIG} but not in ${RUNNER_MANIFEST}`
216
+ );
217
+ }
218
+
219
+ for (const depName of config.services[serviceName].dependsOn || []) {
220
+ if (!config.services[depName]) {
221
+ throw new Error(
222
+ `Service "${serviceName}" depends on "${depName}", but ${depName} is missing from ${TESTKIT_CONFIG}`
223
+ );
224
+ }
225
+ if (!runner.services[depName]) {
226
+ throw new Error(
227
+ `Service "${serviceName}" depends on "${depName}", but ${depName} is missing from ${RUNNER_MANIFEST}`
228
+ );
229
+ }
230
+ }
231
+ }
232
+ }
137
233
 
138
234
  function resolveProductDir(cwd, explicitDir) {
139
235
  if (explicitDir) {
140
- const p = path.resolve(cwd, explicitDir);
141
- if (fs.existsSync(path.join(p, "testkit.manifest.json"))) return p;
142
- throw new Error(`No testkit.manifest.json in ${p}`);
236
+ const dir = path.resolve(cwd, explicitDir);
237
+ ensureProductFiles(dir);
238
+ return dir;
143
239
  }
144
240
 
145
- // Check cwd
146
- if (fs.existsSync(path.join(cwd, "testkit.manifest.json"))) return cwd;
241
+ ensureProductFiles(cwd);
242
+ return cwd;
243
+ }
147
244
 
148
- throw new Error(
149
- `No testkit.manifest.json in current directory. ` +
150
- `Either cd into a product directory or use --dir.`
245
+ function ensureProductFiles(dir) {
246
+ const missing = [RUNNER_MANIFEST, TESTKIT_CONFIG].filter(
247
+ (file) => !fs.existsSync(path.join(dir, file))
151
248
  );
249
+ if (missing.length > 0) {
250
+ throw new Error(
251
+ `Expected ${missing.join(" and ")} in ${dir}. Either cd into a product directory or use --dir.`
252
+ );
253
+ }
152
254
  }
153
255
 
154
- /**
155
- * Check if the given name (relative to cwd) is a sibling directory with a manifest.
156
- * Used by the CLI to distinguish dir args from service/type args.
157
- */
158
256
  export function isSiblingProduct(name) {
159
257
  const candidate = path.join(process.cwd(), name);
160
- return fs.existsSync(path.join(candidate, "testkit.manifest.json"));
258
+ return (
259
+ fs.existsSync(path.join(candidate, RUNNER_MANIFEST)) &&
260
+ fs.existsSync(path.join(candidate, TESTKIT_CONFIG))
261
+ );
161
262
  }
162
263
 
163
- // ── Per-service validation ──────────────────────────────────────────────
164
-
165
- function validateService(name, svc, manifestPath) {
166
- const errors = [];
167
- const ctx = `service "${name}" in ${manifestPath}`;
264
+ function validateMergedService(name, runnerService, serviceConfig, productDir) {
265
+ const usesLocalExecution = Object.values(runnerService.suites).some((suites) =>
266
+ suites.some((suite) => suite.framework !== "k6" || !isDalSuiteType(suite, runnerService, suites))
267
+ );
168
268
 
169
- if (!isObject(svc.testkit)) {
170
- errors.push(`${ctx}: testkit is required and must be an object`);
171
- } else {
172
- const tk = svc.testkit;
269
+ if (usesLocalExecution && !isObject(serviceConfig.local)) {
270
+ throw new Error(
271
+ `Service "${name}" defines non-DAL suites but has no local runtime in ${TESTKIT_CONFIG}`
272
+ );
273
+ }
173
274
 
174
- if (!isObject(tk.neon)) {
175
- errors.push(`${ctx}: testkit.neon is required`);
176
- } else {
177
- requireString(errors, tk.neon, `${ctx}: testkit.neon.projectId`, "projectId");
178
- requireString(errors, tk.neon, `${ctx}: testkit.neon.dbName`, "dbName");
179
- if (tk.neon.reset !== undefined && typeof tk.neon.reset !== "boolean") {
180
- errors.push(`${ctx}: testkit.neon.reset must be a boolean`);
275
+ if (serviceConfig.dependsOn) {
276
+ for (const dep of serviceConfig.dependsOn) {
277
+ if (dep === name) {
278
+ throw new Error(`Service "${name}" cannot depend on itself`);
181
279
  }
182
280
  }
281
+ }
183
282
 
184
- if (!isObject(tk.fly)) {
185
- errors.push(`${ctx}: testkit.fly is required`);
186
- } else {
187
- requireString(errors, tk.fly, `${ctx}: testkit.fly.app`, "app");
188
- requireString(errors, tk.fly, `${ctx}: testkit.fly.org`, "org");
189
- optionalStringArray(errors, tk.fly, `${ctx}: testkit.fly.secrets`, "secrets");
283
+ if (serviceConfig.local?.cwd) {
284
+ const cwdPath = resolveServiceCwd(productDir, serviceConfig.local.cwd);
285
+ if (!fs.existsSync(cwdPath)) {
286
+ throw new Error(
287
+ `Service "${name}" local.cwd does not exist: ${serviceConfig.local.cwd}`
288
+ );
190
289
  }
290
+ }
191
291
 
192
- if (tk.migrate !== undefined) {
193
- if (!isObject(tk.migrate)) {
194
- errors.push(`${ctx}: testkit.migrate must be an object`);
195
- } else {
196
- requireString(errors, tk.migrate, `${ctx}: testkit.migrate.cmd`, "cmd");
197
- }
292
+ if (serviceConfig.migrate?.cwd) {
293
+ const cwdPath = resolveServiceCwd(productDir, serviceConfig.migrate.cwd);
294
+ if (!fs.existsSync(cwdPath)) {
295
+ throw new Error(
296
+ `Service "${name}" migrate.cwd does not exist: ${serviceConfig.migrate.cwd}`
297
+ );
198
298
  }
299
+ }
199
300
 
200
- if (tk.k6 !== undefined && !isObject(tk.k6)) {
201
- errors.push(`${ctx}: testkit.k6 must be an object`);
301
+ if (serviceConfig.seed?.cwd) {
302
+ const cwdPath = resolveServiceCwd(productDir, serviceConfig.seed.cwd);
303
+ if (!fs.existsSync(cwdPath)) {
304
+ throw new Error(
305
+ `Service "${name}" seed.cwd does not exist: ${serviceConfig.seed.cwd}`
306
+ );
202
307
  }
308
+ }
309
+ }
203
310
 
204
- if (tk.dal !== undefined && !isObject(tk.dal)) {
205
- errors.push(`${ctx}: testkit.dal must be an object`);
206
- }
311
+ function validateServiceConfig(name, service, configPath) {
312
+ if (!isObject(service)) {
313
+ throw new Error(`Service "${name}" in ${configPath} must be an object`);
314
+ }
207
315
 
208
- if (tk.depends !== undefined) {
209
- if (!Array.isArray(tk.depends)) {
210
- errors.push(`${ctx}: testkit.depends must be an array`);
211
- } else {
212
- for (let i = 0; i < tk.depends.length; i++) {
213
- const dep = tk.depends[i];
214
- const dp = `${ctx}: testkit.depends[${i}]`;
215
- if (!isObject(dep)) {
216
- errors.push(`${dp} must be an object`);
217
- continue;
218
- }
219
- requireString(errors, dep, `${dp}.name`, "name");
220
- requireString(errors, dep, `${dp}.dockerfile`, "dockerfile");
221
- if (!isObject(dep.fly)) {
222
- errors.push(`${dp}.fly is required and must be an object`);
223
- } else {
224
- requireString(errors, dep.fly, `${dp}.fly.app`, "app");
225
- requireString(errors, dep.fly, `${dp}.fly.org`, "org");
226
- optionalStringArray(errors, dep.fly, `${dp}.fly.secrets`, "secrets");
227
- }
228
- }
229
- }
316
+ if (service.dependsOn !== undefined) {
317
+ if (!Array.isArray(service.dependsOn) || service.dependsOn.some((v) => typeof v !== "string")) {
318
+ throw new Error(`Service "${name}" dependsOn must be an array of service names`);
230
319
  }
231
320
  }
232
321
 
233
- if (!isObject(svc.suites)) {
234
- errors.push(`${ctx}: suites is required and must be an object`);
235
- } else {
236
- for (const type of Object.keys(svc.suites)) {
237
- const arr = svc.suites[type];
238
- if (!Array.isArray(arr)) {
239
- errors.push(`${ctx}: suites.${type} must be an array`);
240
- continue;
241
- }
242
- for (let i = 0; i < arr.length; i++) {
243
- const suite = arr[i];
244
- const prefix = `${ctx}: suites.${type}[${i}]`;
245
- requireString(errors, suite, `${prefix}.name`, "name");
246
- requireNonEmptyStringArray(errors, suite, `${prefix}.files`, "files");
247
- if (suite.pre !== undefined && (typeof suite.pre !== "string" || suite.pre.length === 0)) {
248
- errors.push(`${prefix}.pre must be a non-empty string (shell command)`);
249
- }
250
- }
322
+ if (service.database !== undefined) {
323
+ if (!isObject(service.database)) {
324
+ throw new Error(`Service "${name}" database must be an object`);
325
+ }
326
+ const db = service.database;
327
+ if (db.provider !== "neon") {
328
+ throw new Error(`Service "${name}" database.provider must be "neon"`);
329
+ }
330
+ requireString(db, "projectId", `Service "${name}" database.projectId`);
331
+ requireString(db, "dbName", `Service "${name}" database.dbName`);
332
+ if (db.branchName !== undefined && typeof db.branchName !== "string") {
333
+ throw new Error(`Service "${name}" database.branchName must be a string`);
334
+ }
335
+ if (db.reset !== undefined && typeof db.reset !== "boolean") {
336
+ throw new Error(`Service "${name}" database.reset must be a boolean`);
251
337
  }
252
338
  }
253
339
 
254
- if (errors.length) throw new Error(errors.join("\n"));
255
- }
340
+ if (service.migrate !== undefined) {
341
+ if (!isObject(service.migrate)) {
342
+ throw new Error(`Service "${name}" migrate must be an object`);
343
+ }
344
+ requireString(service.migrate, "cmd", `Service "${name}" migrate.cmd`);
345
+ if (service.migrate.cwd !== undefined && typeof service.migrate.cwd !== "string") {
346
+ throw new Error(`Service "${name}" migrate.cwd must be a string`);
347
+ }
348
+ }
256
349
 
257
- // ── Helpers ─────────────────────────────────────────────────────────────
350
+ if (service.seed !== undefined) {
351
+ if (!isObject(service.seed)) {
352
+ throw new Error(`Service "${name}" seed must be an object`);
353
+ }
354
+ requireString(service.seed, "cmd", `Service "${name}" seed.cmd`);
355
+ if (service.seed.cwd !== undefined && typeof service.seed.cwd !== "string") {
356
+ throw new Error(`Service "${name}" seed.cwd must be a string`);
357
+ }
358
+ }
258
359
 
259
- function isObject(v) {
260
- return v !== null && typeof v === "object" && !Array.isArray(v);
360
+ if (service.local !== undefined) {
361
+ if (!isObject(service.local)) {
362
+ throw new Error(`Service "${name}" local must be an object`);
363
+ }
364
+ requireString(service.local, "start", `Service "${name}" local.start`);
365
+ requireString(service.local, "baseUrl", `Service "${name}" local.baseUrl`);
366
+ requireString(service.local, "readyUrl", `Service "${name}" local.readyUrl`);
367
+ if (
368
+ service.local.port !== undefined &&
369
+ (!Number.isInteger(service.local.port) || service.local.port <= 0)
370
+ ) {
371
+ throw new Error(`Service "${name}" local.port must be a positive integer`);
372
+ }
373
+ if (service.local.cwd !== undefined && typeof service.local.cwd !== "string") {
374
+ throw new Error(`Service "${name}" local.cwd must be a string`);
375
+ }
376
+ if (
377
+ service.local.readyTimeoutMs !== undefined &&
378
+ (!Number.isInteger(service.local.readyTimeoutMs) || service.local.readyTimeoutMs <= 0)
379
+ ) {
380
+ throw new Error(`Service "${name}" local.readyTimeoutMs must be a positive integer`);
381
+ }
382
+ if (service.local.env !== undefined && !isObject(service.local.env)) {
383
+ throw new Error(`Service "${name}" local.env must be an object`);
384
+ }
385
+ }
261
386
  }
262
387
 
263
- function requireString(errors, obj, msg, key) {
388
+ function requireString(obj, key, label) {
264
389
  if (typeof obj[key] !== "string" || obj[key].length === 0) {
265
- errors.push(`${msg} is required`);
390
+ throw new Error(`${label} must be a non-empty string`);
266
391
  }
267
392
  }
268
393
 
269
- function optionalStringArray(errors, obj, msg, key) {
270
- const val = obj[key];
271
- if (val === undefined) return;
272
- if (!Array.isArray(val) || !val.every(v => typeof v === "string")) {
273
- errors.push(`${msg} must be a string[]`);
274
- }
394
+ function isDalSuiteType(suite, runnerService, suitesForType) {
395
+ if (suite.framework && suite.framework !== "k6") return false;
396
+ return suitesForType === runnerService.suites.dal;
275
397
  }
276
398
 
277
- function requireNonEmptyStringArray(errors, obj, msg, key) {
278
- const val = obj[key];
279
- if (!Array.isArray(val) || val.length === 0 || !val.every(v => typeof v === "string")) {
280
- errors.push(`${msg} is required and must be a non-empty string[]`);
281
- }
399
+ function isObject(value) {
400
+ return value !== null && typeof value === "object" && !Array.isArray(value);
282
401
  }