mmt-testlight 0.4.3 → 0.4.4

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/dist/cli.js CHANGED
@@ -4917,15 +4917,17 @@ var init_testHelper = __esm({
4917
4917
  }
4918
4918
  emitStep(payload);
4919
4919
  };
4920
- setenv_ = (name, value) => {
4921
- setenvWithContext_(void 0, void 0, void 0, name, value);
4920
+ setenv_ = (variables) => {
4921
+ setenvWithContext_(void 0, void 0, void 0, variables);
4922
4922
  };
4923
- setenvWithContext_ = (reporter, runId, id, name, value) => {
4923
+ setenvWithContext_ = (reporter, runId, id, variables) => {
4924
+ if (!variables || typeof variables !== "object") {
4925
+ return;
4926
+ }
4924
4927
  const resolvedRunId = typeof runId === "string" ? runId : "";
4925
4928
  const payload = {
4926
4929
  scope: "setenv",
4927
- name,
4928
- value
4930
+ variables: { ...variables }
4929
4931
  };
4930
4932
  if (resolvedRunId) {
4931
4933
  payload.runId = resolvedRunId;
@@ -67763,7 +67765,7 @@ async function runJSCode(context) {
67763
67765
  const functionBody = `${helperDecls}
67764
67766
  ${randomDecls}
67765
67767
  const report_ = (...args) => mmtHelper.reportWithContext_(__reporter, __runId, __id, ...args);
67766
- const setenv_ = (name, value) => { if (typeof envVariables !== 'undefined') { try { envVariables[name] = value; } catch (_e) {} } mmtHelper.setenvWithContext_(__reporter, __runId, __id, name, value); };
67768
+ const setenv_ = (vars) => { if (typeof envVariables !== 'undefined' && vars && typeof vars === 'object') { for (const [k, v] of Object.entries(vars)) { try { envVariables[k] = v; } catch (_e) {} } } mmtHelper.setenvWithContext_(__reporter, __runId, __id, vars); };
67767
67769
  const check_ = (passed, type, raw, reportLevel, title, details, actual, expected) => mmtHelper.check_(passed, type, raw, reportLevel, title, details, actual, expected, report_, console, __checkLogMode);
67768
67770
  const checkExpects_ = (items, type, reportLevel, title, details) => mmtHelper.checkExpects_(items, type, reportLevel, title, details, report_, console, __checkLogMode);
67769
67771
  const checkAbort_ = () => { if (__abortSignal && __abortSignal.aborted) { throw new mmtHelper.TestAbortError(); } };
@@ -67937,6 +67939,7 @@ __export(src_exports, {
67937
67939
  apiRunResult: () => apiRunResult_exports,
67938
67940
  brunoParsePack: () => brunoParsePack_exports,
67939
67941
  curlConvertor: () => curlConvertor_exports,
67942
+ curlGenerator: () => curlGenerator_exports,
67940
67943
  dataImportProcessor: () => dataImportProcessor_exports,
67941
67944
  docHtml: () => docHtml_exports,
67942
67945
  docMarkdown: () => docMarkdown_exports,
@@ -72532,10 +72535,11 @@ var setenvToJSfunc = (setenv, root) => {
72532
72535
  if (entries.length === 0) {
72533
72536
  return "";
72534
72537
  }
72535
- return entries.map(([envKey, outputKeyOrValue]) => {
72538
+ const objEntries = entries.map(([envKey, outputKeyOrValue]) => {
72536
72539
  const valueExpr = typeof outputKeyOrValue === "string" ? toTemplateWithVars(outputKeyOrValue) : JSON.stringify(outputKeyOrValue);
72537
- return `setenv_(${JSON.stringify(envKey)}, ${valueExpr});`;
72538
- }).join("\n");
72540
+ return `${JSON.stringify(envKey)}: ${valueExpr}`;
72541
+ });
72542
+ return `setenv_({ ${objEntries.join(", ")} });`;
72539
72543
  };
72540
72544
  var runToJSfunc = (step) => {
72541
72545
  const alias = step.run;
@@ -73888,6 +73892,219 @@ function curlToAPI(command) {
73888
73892
  return api;
73889
73893
  }
73890
73894
 
73895
+ // ../core/src/curlGenerator.ts
73896
+ var curlGenerator_exports = {};
73897
+ __export(curlGenerator_exports, {
73898
+ buildCurlCommand: () => buildCurlCommand,
73899
+ buildCurlCommandSet: () => buildCurlCommandSet,
73900
+ buildCurlUrl: () => buildCurlUrl,
73901
+ formatCurlCommandSet: () => formatCurlCommandSet,
73902
+ quoteCurlArgument: () => quoteCurlArgument
73903
+ });
73904
+ function stringifyCurlValue(value) {
73905
+ if (value === void 0 || value === null || value === "") {
73906
+ return "";
73907
+ }
73908
+ return String(value);
73909
+ }
73910
+ function stringifyCurlBody(body) {
73911
+ if (body === void 0 || body === null || body === "") {
73912
+ return "";
73913
+ }
73914
+ if (typeof body === "string") {
73915
+ return body;
73916
+ }
73917
+ return JSON.stringify(body);
73918
+ }
73919
+ function buildCurlUrl(url, query) {
73920
+ const queryPairs = Object.entries(query).map(([key, value]) => [key, stringifyCurlValue(value)]).filter(([, value]) => value);
73921
+ if (!queryPairs.length) {
73922
+ return url;
73923
+ }
73924
+ try {
73925
+ const parsed = new URL(url);
73926
+ queryPairs.forEach(([key, value]) => parsed.searchParams.set(key, value));
73927
+ return parsed.toString();
73928
+ } catch {
73929
+ const params = new URLSearchParams();
73930
+ queryPairs.forEach(([key, value]) => params.set(key, value));
73931
+ const separator = url.includes("?") ? "&" : "?";
73932
+ return `${url}${separator}${params.toString()}`;
73933
+ }
73934
+ }
73935
+ function buildCurlParts(input, certificates) {
73936
+ const parts = [];
73937
+ const method = String(input.method || "GET").toUpperCase();
73938
+ if (method !== "GET") {
73939
+ parts.push({ kind: "pair", flag: "-X", value: method });
73940
+ }
73941
+ Object.entries(input.headers || {}).forEach(([key, value]) => {
73942
+ const headerValue2 = stringifyCurlValue(value);
73943
+ if (headerValue2) {
73944
+ parts.push({ kind: "pair", flag: "-H", value: `${key}: ${headerValue2}` });
73945
+ }
73946
+ });
73947
+ const cookiePairs = Object.entries(input.cookies || {}).map(([key, value]) => {
73948
+ const cookieValue = stringifyCurlValue(value);
73949
+ return cookieValue ? `${key}=${cookieValue}` : "";
73950
+ }).filter(Boolean);
73951
+ if (cookiePairs.length) {
73952
+ parts.push({
73953
+ kind: "pair",
73954
+ flag: "-H",
73955
+ value: `Cookie: ${cookiePairs.join("; ")}`
73956
+ });
73957
+ }
73958
+ const body = stringifyCurlBody(input.body);
73959
+ if (method !== "GET" && body) {
73960
+ parts.push({ kind: "pair", flag: "--data-raw", value: body });
73961
+ }
73962
+ const url = buildCurlUrl(input.url || "", input.query || {});
73963
+ if (!url) {
73964
+ return parts;
73965
+ }
73966
+ if (certificates?.insecure) {
73967
+ parts.push({ kind: "flag", text: "--insecure" });
73968
+ }
73969
+ if (certificates?.caPath) {
73970
+ parts.push({ kind: "pair", flag: "--cacert", value: certificates.caPath });
73971
+ }
73972
+ if (certificates?.certType === "P12" && certificates.cert) {
73973
+ parts.push({ kind: "pair", flag: "--cert-type", value: "P12" });
73974
+ parts.push({ kind: "pair", flag: "--cert", value: certificates.cert });
73975
+ } else if (certificates?.cert && certificates.key) {
73976
+ parts.push({ kind: "pair", flag: "--cert", value: certificates.cert });
73977
+ parts.push({ kind: "pair", flag: "--key", value: certificates.key });
73978
+ if (certificates.pass) {
73979
+ parts.push({ kind: "pair", flag: "--pass", value: certificates.pass });
73980
+ }
73981
+ }
73982
+ parts.push({ kind: "url", value: url });
73983
+ return parts;
73984
+ }
73985
+ function quoteCurlArgument(shell, value) {
73986
+ if (shell === "powershell") {
73987
+ return `'${value.replace(/'/g, "''")}'`;
73988
+ }
73989
+ if (shell === "cmd") {
73990
+ return `"${value.replace(/"/g, '""')}"`;
73991
+ }
73992
+ return `'${value.replace(/'/g, "'\\''")}'`;
73993
+ }
73994
+ function quoteNativeWindows(value) {
73995
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
73996
+ }
73997
+ function shouldQuoteCurlPairValue(flag) {
73998
+ return flag !== "-X" && flag !== "--request" && flag !== "--cert-type";
73999
+ }
74000
+ function formatPairValue(shell, flag, value) {
74001
+ if (!shouldQuoteCurlPairValue(flag)) {
74002
+ return value;
74003
+ }
74004
+ if (shell === "powershell") {
74005
+ return quoteNativeWindows(value);
74006
+ }
74007
+ return quoteCurlArgument(shell, value);
74008
+ }
74009
+ function renderPosixLine(parts) {
74010
+ const segments = [];
74011
+ for (const part of parts) {
74012
+ if (part.kind === "flag") {
74013
+ segments.push(part.text);
74014
+ continue;
74015
+ }
74016
+ if (part.kind === "pair") {
74017
+ segments.push(part.flag, formatPairValue("posix", part.flag, part.value));
74018
+ continue;
74019
+ }
74020
+ segments.push(quoteCurlArgument("posix", part.value));
74021
+ }
74022
+ return ["curl", ...segments].join(" ");
74023
+ }
74024
+ function renderPosixParts(parts, multiline) {
74025
+ if (!multiline) {
74026
+ return renderPosixLine(parts);
74027
+ }
74028
+ const chunks = [];
74029
+ for (const part of parts) {
74030
+ if (part.kind === "flag") {
74031
+ chunks.push(part.text);
74032
+ continue;
74033
+ }
74034
+ if (part.kind === "pair") {
74035
+ chunks.push(`${part.flag} ${formatPairValue("posix", part.flag, part.value)}`);
74036
+ continue;
74037
+ }
74038
+ chunks.push(quoteCurlArgument("posix", part.value));
74039
+ }
74040
+ const lines = [`curl ${chunks[0]}`];
74041
+ for (let i = 1; i < chunks.length; i++) {
74042
+ lines.push(` ${chunks[i]}`);
74043
+ }
74044
+ return lines.map((line, index) => index < lines.length - 1 ? `${line} \\` : line).join("\n");
74045
+ }
74046
+ function renderCmdParts(parts) {
74047
+ const segments = [];
74048
+ for (const part of parts) {
74049
+ if (part.kind === "flag") {
74050
+ segments.push(part.text);
74051
+ continue;
74052
+ }
74053
+ if (part.kind === "pair") {
74054
+ segments.push(part.flag, formatPairValue("cmd", part.flag, part.value));
74055
+ continue;
74056
+ }
74057
+ segments.push(quoteCurlArgument("cmd", part.value));
74058
+ }
74059
+ return ["curl", ...segments].join(" ");
74060
+ }
74061
+ function renderPowerShellParts(parts) {
74062
+ const args = [];
74063
+ for (const part of parts) {
74064
+ if (part.kind === "flag") {
74065
+ args.push(part.text);
74066
+ continue;
74067
+ }
74068
+ if (part.kind === "pair") {
74069
+ args.push(part.flag, formatPairValue("powershell", part.flag, part.value));
74070
+ continue;
74071
+ }
74072
+ args.push(quoteNativeWindows(part.value));
74073
+ }
74074
+ return `curl.exe --% ${args.join(" ")}`;
74075
+ }
74076
+ function buildCurlCommand(input, shell, certificates) {
74077
+ const parts = buildCurlParts(input, certificates);
74078
+ if (shell === "powershell") {
74079
+ return renderPowerShellParts(parts);
74080
+ }
74081
+ if (shell === "cmd") {
74082
+ return renderCmdParts(parts);
74083
+ }
74084
+ return renderPosixParts(parts, false);
74085
+ }
74086
+ function buildCurlCommandSet(input, certificates) {
74087
+ const parts = buildCurlParts(input, certificates);
74088
+ return {
74089
+ posix: renderPosixParts(parts, true),
74090
+ powershell: renderPowerShellParts(parts),
74091
+ cmd: renderCmdParts(parts)
74092
+ };
74093
+ }
74094
+ function formatCurlCommandSet(set) {
74095
+ return [
74096
+ "# Bash / macOS / Linux / Git Bash / WSL",
74097
+ set.posix,
74098
+ "",
74099
+ "# PowerShell (Windows)",
74100
+ '# Use curl.exe \u2014 "curl" is an alias for Invoke-WebRequest',
74101
+ set.powershell,
74102
+ "",
74103
+ "# CMD (Windows)",
74104
+ set.cmd
74105
+ ].join("\n");
74106
+ }
74107
+
73891
74108
  // ../core/src/docParsePack.ts
73892
74109
  var docParsePack_exports = {};
73893
74110
  __export(docParsePack_exports, {
@@ -78520,9 +78737,11 @@ __export(runConfig_exports, {
78520
78737
  mergeEnv: () => mergeEnv,
78521
78738
  mergeInputs: () => mergeInputs,
78522
78739
  mergeSuiteEnv: () => mergeSuiteEnv,
78740
+ normalizePresetNames: () => normalizePresetNames,
78523
78741
  resolveDocumentEnvVars: () => resolveDocumentEnvVars,
78524
78742
  resolveEnvFromDoc: () => resolveEnvFromDoc,
78525
78743
  resolvePresetEnv: () => resolvePresetEnv,
78744
+ resolvePresetsEnv: () => resolvePresetsEnv,
78526
78745
  selectFromVariables: () => selectFromVariables
78527
78746
  });
78528
78747
  function mergeInputs(params) {
@@ -78535,7 +78754,7 @@ function mergeEnv(params) {
78535
78754
  }
78536
78755
  function resolveEnvFromDoc(params) {
78537
78756
  const { doc, presetName, manualEnvvars } = params;
78538
- const presetEnv = presetName ? resolvePresetEnv(doc, presetName) : {};
78757
+ const presetEnv = resolvePresetsEnv(doc, presetName);
78539
78758
  return mergeEnv({ envvar: presetEnv, manualEnvvars });
78540
78759
  }
78541
78760
  function selectFromVariables(variables, key, choiceOrValue) {
@@ -78582,6 +78801,33 @@ function resolvePresetEnv(doc, presetName) {
78582
78801
  }
78583
78802
  return out;
78584
78803
  }
78804
+ function normalizePresetNames(presetNames) {
78805
+ if (presetNames == null) {
78806
+ return [];
78807
+ }
78808
+ const raw = Array.isArray(presetNames) ? presetNames : [presetNames];
78809
+ const names = [];
78810
+ for (const entry of raw) {
78811
+ const s = String(entry).trim();
78812
+ if (!s) {
78813
+ continue;
78814
+ }
78815
+ for (const part of s.split(",")) {
78816
+ const name = part.trim();
78817
+ if (name) {
78818
+ names.push(name);
78819
+ }
78820
+ }
78821
+ }
78822
+ return names;
78823
+ }
78824
+ function resolvePresetsEnv(doc, presetNames) {
78825
+ const out = {};
78826
+ for (const name of normalizePresetNames(presetNames)) {
78827
+ Object.assign(out, resolvePresetEnv(doc, name));
78828
+ }
78829
+ return out;
78830
+ }
78585
78831
  function mergeSuiteEnv(params) {
78586
78832
  const {
78587
78833
  baseEnv = {},
@@ -84373,7 +84619,7 @@ var import_path2 = __toESM(require("path"));
84373
84619
  var import_module = require("module");
84374
84620
  var requireFromRunArgs = (0, import_module.createRequire)(__filename);
84375
84621
  var { resolveUserPath, resolveUserPathPreferExisting } = requireFromRunArgs("../src/pathNormalize.cjs");
84376
- var { mergeEnv: mergeEnv2, resolvePresetEnv: resolvePresetEnv2 } = runConfig_exports || {};
84622
+ var { mergeEnv: mergeEnv2, resolvePresetsEnv: resolvePresetsEnv2, normalizePresetNames: normalizePresetNames2 } = runConfig_exports || {};
84377
84623
  var OMIT_SENTINEL2 = (omitKeyword_exports || {}).OMIT_SENTINEL || "__MMT_OMIT__";
84378
84624
  var LOG_LEVEL_PRIORITY = {
84379
84625
  error: 0,
@@ -84639,7 +84885,7 @@ async function buildCliRunArgs(file, opts) {
84639
84885
  let envvar = void 0;
84640
84886
  let networkConfig = void 0;
84641
84887
  const envFileOpt = opts.envFile;
84642
- const presetName = opts.preset;
84888
+ const presetNames = normalizePresetNames2(opts.preset);
84643
84889
  let envFileDir = dir;
84644
84890
  let suiteEnvConfig;
84645
84891
  try {
@@ -84656,7 +84902,7 @@ async function buildCliRunArgs(file, opts) {
84656
84902
  envFileDir = import_path2.default.dirname(p);
84657
84903
  const doc = await loadEnvDoc(p, findProjectRootForCli(p) || void 0);
84658
84904
  const defaultEnv = resolveDefaultEnvVariables(doc.variables);
84659
- const presetEnv = resolvePresetEnv2(doc, presetName);
84905
+ const presetEnv = resolvePresetsEnv2(doc, presetNames);
84660
84906
  envvar = mergeEnv2({ baseEnv: defaultEnv, envvar: presetEnv, manualEnvvars });
84661
84907
  if (doc.certificates || doc.setting) {
84662
84908
  networkConfig = buildNetworkConfigFromEnv(
@@ -84685,7 +84931,7 @@ async function buildCliRunArgs(file, opts) {
84685
84931
  }
84686
84932
  if (suiteEnvFilePath && import_fs2.default.existsSync(suiteEnvFilePath)) {
84687
84933
  const suiteEnvDoc = await loadEnvDoc(suiteEnvFilePath, projectRoot || void 0);
84688
- suitePresetEnv = resolvePresetEnv2(suiteEnvDoc, suiteEnvConfig.preset);
84934
+ suitePresetEnv = resolvePresetsEnv2(suiteEnvDoc, suiteEnvConfig.preset);
84689
84935
  }
84690
84936
  }
84691
84937
  const baseEnv = { ...envvar || {} };
@@ -84772,6 +85018,16 @@ function resolveCliVersion() {
84772
85018
  return "0.0.0";
84773
85019
  }
84774
85020
  var CLI_VERSION = resolveCliVersion();
85021
+ function collectPreset(value, previous) {
85022
+ const names = [];
85023
+ for (const part of String(value).split(",")) {
85024
+ const name = part.trim();
85025
+ if (name) {
85026
+ names.push(name);
85027
+ }
85028
+ }
85029
+ return previous.concat(names.length ? names : [value]);
85030
+ }
84775
85031
  var jsRunnerModulePromise;
84776
85032
  async function loadJsRunnerModule() {
84777
85033
  if (!jsRunnerModulePromise) {
@@ -84955,23 +85211,23 @@ program.name("testlight").description("Multimeter CLI \u2014 run .mmt API tests,
84955
85211
  " -o, --out <file> Write result JSON to file",
84956
85212
  " -i, --input <k=v...> Input variables (repeatable)",
84957
85213
  " -e, --env <k=v...> Environment variables (repeatable)",
84958
- " --env-file <path> Environment file (.mmt/.yaml)",
84959
- " --preset <name> Preset from env file (e.g. runner.dev)",
84960
- " --example <name|#n> Named example or index (#1 is first)",
85214
+ " -F, --env-file <path> Environment file (.mmt/.yaml)",
85215
+ " -P, --preset <name> Preset from env file (repeatable)",
85216
+ " -x, --example <name|#n> Named example or index (#1 is first)",
84961
85217
  " -p, --print-js Print generated JS before executing",
84962
- " --report <format> junit | mmt | html | md | md-detailed",
84963
- " --report-file <path> Report output path",
85218
+ " -r, --report <format> junit | mmt | html | md | md-detailed",
85219
+ " -R, --report-file <path> Report output path",
84964
85220
  "",
84965
85221
  "Examples:",
84966
85222
  " testlight run path/to/test.mmt",
84967
- " testlight run path/to/test.mmt -e api_url=https://test.mmt.dev -q",
85223
+ " testlight run path/to/test.mmt -F env.mmt -P runner.dev -P custom.prod",
84968
85224
  " testlight run path/to/suite.mmt --report html",
84969
85225
  "",
84970
85226
  "Run `testlight <command> --help` for command-specific options."
84971
85227
  ].join("\n")
84972
85228
  );
84973
85229
  program.option(
84974
- "--log-level <level>",
85230
+ "-L, --log-level <level>",
84975
85231
  "Set log level (error|warn|info|debug|trace)",
84976
85232
  "info"
84977
85233
  );
@@ -84982,19 +85238,21 @@ program.command("run").description("Run a .mmt file").argument("<file>", "Test f
84982
85238
  "-e, --env <values...>",
84983
85239
  "Environment variables as key value pairs or key=val (repeatable)"
84984
85240
  ).option(
84985
- "--env-file <path>",
85241
+ "-F, --env-file <path>",
84986
85242
  "Environment file (.mmt/.yaml) to read variables from"
84987
85243
  ).option(
84988
- "--preset <name>",
84989
- "Preset name from env file (e.g., runner.dev) or just name under runner"
85244
+ "-P, --preset <name>",
85245
+ "Preset from env file (repeatable; e.g. runner.dev or group.name)",
85246
+ collectPreset,
85247
+ []
84990
85248
  ).option(
84991
- "--example <name|#n>",
85249
+ "-x, --example <name|#n>",
84992
85250
  "Run a named example (matches name) or numeric index (#1 = first)"
84993
85251
  ).option("-p, --print-js", "Print generated JS before executing", false).option(
84994
- "--report <format>",
85252
+ "-r, --report <format>",
84995
85253
  "Generate test report: junit, mmt, html, md, or md-detailed"
84996
85254
  ).option(
84997
- "--report-file <path>",
85255
+ "-R, --report-file <path>",
84998
85256
  "Output path for the report file (default depends on format)"
84999
85257
  ).option(
85000
85258
  "--no-real-threads",
@@ -85178,13 +85436,15 @@ program.command("print-js").argument("<file>", "Test file (.yaml/.yml/.json/.mmt
85178
85436
  "-e, --env <values...>",
85179
85437
  "Environment variables as key value pairs or key=val (repeatable)"
85180
85438
  ).option(
85181
- "--env-file <path>",
85439
+ "-F, --env-file <path>",
85182
85440
  "Environment file (.mmt/.yaml) to read variables from"
85183
85441
  ).option(
85184
- "--preset <name>",
85185
- "Preset name from env file (e.g., runner.dev) or just name under runner"
85442
+ "-P, --preset <name>",
85443
+ "Preset from env file (repeatable; e.g. runner.dev or group.name)",
85444
+ collectPreset,
85445
+ []
85186
85446
  ).option(
85187
- "--example <name|#n>",
85447
+ "-x, --example <name|#n>",
85188
85448
  "Select a named example (matches name) or numeric index (#1 = first)"
85189
85449
  ).action(async (file, opts) => {
85190
85450
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mmt-testlight",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Multimeter CLI test runner",
5
5
  "private": false,
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -35,6 +35,17 @@ function resolveCliVersion(): string {
35
35
  }
36
36
  const CLI_VERSION = resolveCliVersion();
37
37
 
38
+ function collectPreset(value: string, previous: string[]): string[] {
39
+ const names: string[] = [];
40
+ for (const part of String(value).split(',')) {
41
+ const name = part.trim();
42
+ if (name) {
43
+ names.push(name);
44
+ }
45
+ }
46
+ return previous.concat(names.length ? names : [value]);
47
+ }
48
+
38
49
  type JsRunnerModule = typeof import('mmt-core/jsRunner');
39
50
  let jsRunnerModulePromise: Promise<JsRunnerModule>|undefined;
40
51
 
@@ -247,23 +258,23 @@ program.name('testlight')
247
258
  ' -o, --out <file> Write result JSON to file',
248
259
  ' -i, --input <k=v...> Input variables (repeatable)',
249
260
  ' -e, --env <k=v...> Environment variables (repeatable)',
250
- ' --env-file <path> Environment file (.mmt/.yaml)',
251
- ' --preset <name> Preset from env file (e.g. runner.dev)',
252
- ' --example <name|#n> Named example or index (#1 is first)',
261
+ ' -F, --env-file <path> Environment file (.mmt/.yaml)',
262
+ ' -P, --preset <name> Preset from env file (repeatable)',
263
+ ' -x, --example <name|#n> Named example or index (#1 is first)',
253
264
  ' -p, --print-js Print generated JS before executing',
254
- ' --report <format> junit | mmt | html | md | md-detailed',
255
- ' --report-file <path> Report output path',
265
+ ' -r, --report <format> junit | mmt | html | md | md-detailed',
266
+ ' -R, --report-file <path> Report output path',
256
267
  '',
257
268
  'Examples:',
258
269
  ' testlight run path/to/test.mmt',
259
- ' testlight run path/to/test.mmt -e api_url=https://test.mmt.dev -q',
270
+ ' testlight run path/to/test.mmt -F env.mmt -P runner.dev -P custom.prod',
260
271
  ' testlight run path/to/suite.mmt --report html',
261
272
  '',
262
273
  'Run `testlight <command> --help` for command-specific options.',
263
274
  ].join('\n'));
264
275
 
265
276
  program.option(
266
- '--log-level <level>',
277
+ '-L, --log-level <level>',
267
278
  'Set log level (error|warn|info|debug|trace)',
268
279
  'info');
269
280
 
@@ -279,20 +290,22 @@ program.command('run')
279
290
  '-e, --env <values...>',
280
291
  'Environment variables as key value pairs or key=val (repeatable)')
281
292
  .option(
282
- '--env-file <path>',
293
+ '-F, --env-file <path>',
283
294
  'Environment file (.mmt/.yaml) to read variables from')
284
295
  .option(
285
- '--preset <name>',
286
- 'Preset name from env file (e.g., runner.dev) or just name under runner')
296
+ '-P, --preset <name>',
297
+ 'Preset from env file (repeatable; e.g. runner.dev or group.name)',
298
+ collectPreset,
299
+ [])
287
300
  .option(
288
- '--example <name|#n>',
301
+ '-x, --example <name|#n>',
289
302
  'Run a named example (matches name) or numeric index (#1 = first)')
290
303
  .option('-p, --print-js', 'Print generated JS before executing', false)
291
304
  .option(
292
- '--report <format>',
305
+ '-r, --report <format>',
293
306
  'Generate test report: junit, mmt, html, md, or md-detailed')
294
307
  .option(
295
- '--report-file <path>',
308
+ '-R, --report-file <path>',
296
309
  'Output path for the report file (default depends on format)')
297
310
  .option(
298
311
  '--no-real-threads',
@@ -498,13 +511,15 @@ program.command('print-js')
498
511
  '-e, --env <values...>',
499
512
  'Environment variables as key value pairs or key=val (repeatable)')
500
513
  .option(
501
- '--env-file <path>',
514
+ '-F, --env-file <path>',
502
515
  'Environment file (.mmt/.yaml) to read variables from')
503
516
  .option(
504
- '--preset <name>',
505
- 'Preset name from env file (e.g., runner.dev) or just name under runner')
517
+ '-P, --preset <name>',
518
+ 'Preset from env file (repeatable; e.g. runner.dev or group.name)',
519
+ collectPreset,
520
+ [])
506
521
  .option(
507
- '--example <name|#n>',
522
+ '-x, --example <name|#n>',
508
523
  'Select a named example (matches name) or numeric index (#1 = first)')
509
524
  .action(async (file: string, opts: {stages?: boolean}) => {
510
525
  try {
package/src/pkg-entry.cjs CHANGED
@@ -165,7 +165,7 @@ function createPkgJsRunner() {
165
165
  '__checkLogMode',
166
166
  `${helperDecls}\n${randomDecls}\n` +
167
167
  `const report_ = (...args) => mmtHelper.reportWithContext_ ? mmtHelper.reportWithContext_(__reporter, __runId, __id, ...args) : undefined;\n` +
168
- `const setenv_ = (name, value) => { try { envVariables[name] = value; } catch (_e) {} if (mmtHelper.setenvWithContext_) mmtHelper.setenvWithContext_(__reporter, __runId, __id, name, value); };\n` +
168
+ `const setenv_ = (vars) => { if (vars && typeof vars === 'object') { for (const [k, v] of Object.entries(vars)) { try { envVariables[k] = v; } catch (_e) {} } } if (mmtHelper.setenvWithContext_) mmtHelper.setenvWithContext_(__reporter, __runId, __id, vars); };\n` +
169
169
  `const check_ = (passed, type, raw, reportLevel, title, details, actual, expected) => mmtHelper.check_(passed, type, raw, reportLevel, title, details, actual, expected, report_, console, __checkLogMode);\n` +
170
170
  `const checkExpects_ = (items, type, reportLevel, title, details) => mmtHelper.checkExpects_ ? mmtHelper.checkExpects_(items, type, reportLevel, title, details, report_, console, __checkLogMode) : undefined;\n` +
171
171
  `const checkAbort_ = () => { if (__abortSignal && __abortSignal.aborted) { const e = new Error('Test run was stopped'); e.name = 'TestAbortError'; throw e; } };\n` +
@@ -522,16 +522,16 @@ function printHelp() {
522
522
  const options = formatHelpRows([
523
523
  ['-h, --help', 'Show help'],
524
524
  ['-v, --version', 'Show version'],
525
- ['--log-level <level>', 'Set log level (error|warn|info|debug|trace)'],
525
+ ['-L, --log-level <level>', 'Set log level (error|warn|info|debug|trace)'],
526
526
  ]);
527
527
  const runOptions = formatHelpRows([
528
528
  ['-q, --quiet', 'Minimal output'],
529
529
  ['-o, --out <file>', 'Write result JSON to file'],
530
530
  ['-i, --input <k=v...>', 'Input variables (repeatable)'],
531
531
  ['-e, --env <k=v...>', 'Environment variables (repeatable)'],
532
- ['--env-file <path>', 'Environment file (.mmt/.yaml)'],
533
- ['--preset <name>', 'Preset from env file (e.g. runner.dev)'],
534
- ['--example <name|#n>', 'Named example or index (#1 is first)'],
532
+ ['-F, --env-file <path>', 'Environment file (.mmt/.yaml)'],
533
+ ['-P, --preset <name>', 'Preset from env file (repeatable)'],
534
+ ['-x, --example <name|#n>', 'Named example or index (#1 is first)'],
535
535
  ['--print-js', 'Print generated JS before executing'],
536
536
  ['--debug-env', 'Print resolved env vars'],
537
537
  ]);
@@ -567,12 +567,12 @@ function printRunHelp() {
567
567
  ['-o, --out <file>', 'Write result JSON to file'],
568
568
  ['-i, --input <k=v...>', 'Input variables (repeatable)'],
569
569
  ['-e, --env <k=v...>', 'Environment variables (repeatable)'],
570
- ['--env-file <path>', 'Environment file (.mmt/.yaml)'],
571
- ['--preset <name>', 'Preset from env file (e.g. runner.dev)'],
572
- ['--example <name|#n>', 'Named example or index (#1 is first)'],
570
+ ['-F, --env-file <path>', 'Environment file (.mmt/.yaml)'],
571
+ ['-P, --preset <name>', 'Preset from env file (repeatable)'],
572
+ ['-x, --example <name|#n>', 'Named example or index (#1 is first)'],
573
573
  ['--print-js', 'Print generated JS before executing'],
574
574
  ['--debug-env', 'Print resolved env vars'],
575
- ['--log-level <level>', 'Set log level (error|warn|info|debug|trace)'],
575
+ ['-L, --log-level <level>', 'Set log level (error|warn|info|debug|trace)'],
576
576
  ]);
577
577
  const examples = formatHelpRows([
578
578
  ['testlight run path/to/test.mmt'],
@@ -598,9 +598,9 @@ function printPrintJsHelp() {
598
598
  ['-h, --help', 'Show help'],
599
599
  ['-i, --input <k=v...>', 'Input variables (repeatable)'],
600
600
  ['-e, --env <k=v...>', 'Environment variables (repeatable)'],
601
- ['--env-file <path>', 'Environment file (.mmt/.yaml)'],
602
- ['--preset <name>', 'Preset from env file (e.g. runner.dev)'],
603
- ['--example <name|#n>', 'Named example or index (#1 is first)'],
601
+ ['-F, --env-file <path>', 'Environment file (.mmt/.yaml)'],
602
+ ['-P, --preset <name>', 'Preset from env file (repeatable)'],
603
+ ['-x, --example <name|#n>', 'Named example or index (#1 is first)'],
604
604
  ]);
605
605
  const out = [
606
606
  'Usage: testlight print-js [options] <file>',
@@ -653,6 +653,39 @@ function parsePairs(tokens) {
653
653
  return out;
654
654
  }
655
655
 
656
+ function pushPresetOpt(opts, value) {
657
+ if (!Array.isArray(opts.preset)) {
658
+ opts.preset = opts.preset ? [opts.preset] : [];
659
+ }
660
+ if (value == null) {
661
+ return;
662
+ }
663
+ for (const part of String(value).split(',')) {
664
+ const name = part.trim();
665
+ if (name) {
666
+ opts.preset.push(name);
667
+ }
668
+ }
669
+ }
670
+
671
+ function resolvePresetsFromDoc(runConfig, doc, presetNames) {
672
+ if (!runConfig || !doc) {
673
+ return {};
674
+ }
675
+ if (typeof runConfig.resolvePresetsEnv === 'function') {
676
+ return runConfig.resolvePresetsEnv(doc, presetNames) || {};
677
+ }
678
+ const list = Array.isArray(presetNames) ? presetNames :
679
+ (presetNames ? [presetNames] : []);
680
+ let out = {};
681
+ if (typeof runConfig.resolvePresetEnv === 'function') {
682
+ for (const name of list) {
683
+ Object.assign(out, runConfig.resolvePresetEnv(doc, name) || {});
684
+ }
685
+ }
686
+ return out;
687
+ }
688
+
656
689
  function loadEnvDoc(envPath) {
657
690
  try {
658
691
  // eslint-disable-next-line global-require
@@ -690,7 +723,7 @@ function parseRunArgv(argv) {
690
723
  input: [],
691
724
  env: [],
692
725
  envFile: undefined,
693
- preset: undefined,
726
+ preset: [],
694
727
  example: undefined,
695
728
  printJs: false,
696
729
  debugEnv: false,
@@ -711,7 +744,7 @@ function parseRunArgv(argv) {
711
744
  if (a === '-h' || a === '--help') {
712
745
  continue;
713
746
  }
714
- if (a === '--log-level') {
747
+ if (a === '--log-level' || a === '-L') {
715
748
  consume(i + 1);
716
749
  i += 1;
717
750
  continue;
@@ -758,17 +791,17 @@ function parseRunArgv(argv) {
758
791
  }
759
792
  continue;
760
793
  }
761
- if (a === '--env-file') {
794
+ if (a === '--env-file' || a === '-F') {
762
795
  consume(i + 1);
763
796
  opts.envFile = argv[++i];
764
797
  continue;
765
798
  }
766
- if (a === '--preset') {
799
+ if (a === '--preset' || a === '-P') {
767
800
  consume(i + 1);
768
- opts.preset = argv[++i];
801
+ pushPresetOpt(opts, argv[++i]);
769
802
  continue;
770
803
  }
771
- if (a === '--example') {
804
+ if (a === '--example' || a === '-x') {
772
805
  consume(i + 1);
773
806
  opts.example = argv[++i];
774
807
  continue;
@@ -802,7 +835,7 @@ function parsePrintJsArgv(argv) {
802
835
  input: [],
803
836
  env: [],
804
837
  envFile: undefined,
805
- preset: undefined,
838
+ preset: [],
806
839
  example: undefined,
807
840
  };
808
841
  let filePath;
@@ -834,15 +867,15 @@ function parsePrintJsArgv(argv) {
834
867
  }
835
868
  continue;
836
869
  }
837
- if (a === '--env-file') {
870
+ if (a === '--env-file' || a === '-F') {
838
871
  opts.envFile = argv[++i];
839
872
  continue;
840
873
  }
841
- if (a === '--preset') {
842
- opts.preset = argv[++i];
874
+ if (a === '--preset' || a === '-P') {
875
+ pushPresetOpt(opts, argv[++i]);
843
876
  continue;
844
877
  }
845
- if (a === '--example') {
878
+ if (a === '--example' || a === '-x') {
846
879
  opts.example = argv[++i];
847
880
  continue;
848
881
  }
@@ -857,17 +890,19 @@ function parsePrintJsArgv(argv) {
857
890
  }
858
891
 
859
892
  function parseLogLevel(argv) {
860
- const idx = argv.indexOf('--log-level');
861
- if (idx < 0) {
862
- return 'info';
863
- }
864
- const v = argv[idx + 1];
865
- if (!v) {
866
- return 'info';
867
- }
868
- const level = String(v).toLowerCase();
869
- if (['error', 'warn', 'info', 'debug', 'trace'].includes(level)) {
870
- return level;
893
+ for (let i = 1; i < argv.length; i++) {
894
+ const a = argv[i];
895
+ if (a === '--log-level' || a === '-L') {
896
+ const v = argv[i + 1];
897
+ if (!v) {
898
+ return 'info';
899
+ }
900
+ const level = String(v).toLowerCase();
901
+ if (['error', 'warn', 'info', 'debug', 'trace'].includes(level)) {
902
+ return level;
903
+ }
904
+ return 'info';
905
+ }
871
906
  }
872
907
  return 'info';
873
908
  }
@@ -1023,9 +1058,8 @@ async function main() {
1023
1058
  const doc = loadEnvDoc(p);
1024
1059
  // Defaults come from env file variables; optional preset overlays them.
1025
1060
  let baseEnv = (doc && doc.variables) ? {...doc.variables} : {};
1026
- if (parsed.opts.preset && runConfig &&
1027
- typeof runConfig.resolvePresetEnv === 'function') {
1028
- const presetEnv = runConfig.resolvePresetEnv(doc, parsed.opts.preset) || {};
1061
+ if (parsed.opts.preset.length && runConfig) {
1062
+ const presetEnv = resolvePresetsFromDoc(runConfig, doc, parsed.opts.preset);
1029
1063
  baseEnv = {...baseEnv, ...presetEnv};
1030
1064
  }
1031
1065
  envvar = runConfig.mergeEnv({ envvar: baseEnv, manualEnvvars });
@@ -1033,7 +1067,7 @@ async function main() {
1033
1067
  try {
1034
1068
  process.stdout.write(`debug envFile raw: ${envFileRaw}\n`);
1035
1069
  process.stdout.write(`debug envFile resolved: ${p}\n`);
1036
- process.stdout.write(`debug preset: ${parsed.opts.preset || '(none)'}\n`);
1070
+ process.stdout.write(`debug preset: ${parsed.opts.preset.length ? parsed.opts.preset.join(',') : '(none)'}\n`);
1037
1071
  process.stdout.write(`debug envDoc variables keys: ${doc && doc.variables ? Object.keys(doc.variables).join(',') : '(none)'}\n`);
1038
1072
  process.stdout.write(`debug envDoc presets keys: ${doc && doc.presets ? Object.keys(doc.presets).join(',') : '(none)'}\n`);
1039
1073
  process.stdout.write(`debug baseEnv keys: ${baseEnv ? Object.keys(baseEnv).join(',') : '(none)'}\n`);
@@ -1123,8 +1157,9 @@ async function main() {
1123
1157
  envFileForPreset = loadEnvDoc(p);
1124
1158
  }
1125
1159
  }
1126
- if (envFileForPreset && coreRunConfig.resolvePresetEnv) {
1127
- suitePresetEnv = coreRunConfig.resolvePresetEnv(envFileForPreset, suiteEnv.preset) || {};
1160
+ if (envFileForPreset && coreRunConfig) {
1161
+ suitePresetEnv = resolvePresetsFromDoc(
1162
+ coreRunConfig, envFileForPreset, suiteEnv.preset) || {};
1128
1163
  }
1129
1164
  }
1130
1165
 
package/src/runArgs.ts CHANGED
@@ -18,7 +18,7 @@ const {resolveUserPath, resolveUserPathPreferExisting} = requireFromRunArgs('../
18
18
 
19
19
  export type ReportFormat = 'junit' | 'mmt' | 'html' | 'md' | 'md-detailed';
20
20
 
21
- const {mergeEnv, resolvePresetEnv} =
21
+ const {mergeEnv, resolvePresetsEnv, normalizePresetNames} =
22
22
  ((mmtcore as any).runConfig || {}) as any;
23
23
  const OMIT_SENTINEL =
24
24
  ((mmtcore as any).omitKeyword || {}).OMIT_SENTINEL || '__MMT_OMIT__';
@@ -347,7 +347,7 @@ export async function buildCliRunArgs(file: string, opts: AnyOpts): Promise<Pars
347
347
  let envvar: Record<string, any>|undefined = undefined;
348
348
  let networkConfig: NetworkConfig|undefined = undefined;
349
349
  const envFileOpt = opts.envFile as string | undefined;
350
- const presetName = opts.preset as string | undefined;
350
+ const presetNames = normalizePresetNames(opts.preset as string | string[] | undefined);
351
351
  let envFileDir = dir;
352
352
 
353
353
  // Detect if this is a suite file and check for suite environment config
@@ -368,7 +368,7 @@ export async function buildCliRunArgs(file: string, opts: AnyOpts): Promise<Pars
368
368
  envFileDir = path.dirname(p);
369
369
  const doc = await loadEnvDoc(p, findProjectRootForCli(p) || undefined);
370
370
  const defaultEnv = resolveDefaultEnvVariables(doc.variables);
371
- const presetEnv = resolvePresetEnv(doc, presetName);
371
+ const presetEnv = resolvePresetsEnv(doc, presetNames);
372
372
  envvar = mergeEnv({baseEnv: defaultEnv, envvar: presetEnv, manualEnvvars});
373
373
  // Build network config from file-driven HTTP settings and certificates.
374
374
  if (doc.certificates || doc.setting) {
@@ -403,7 +403,7 @@ export async function buildCliRunArgs(file: string, opts: AnyOpts): Promise<Pars
403
403
  if (suiteEnvFilePath && fs.existsSync(suiteEnvFilePath)) {
404
404
  const suiteEnvDoc =
405
405
  await loadEnvDoc(suiteEnvFilePath, projectRoot || undefined);
406
- suitePresetEnv = resolvePresetEnv(suiteEnvDoc, suiteEnvConfig.preset);
406
+ suitePresetEnv = resolvePresetsEnv(suiteEnvDoc, suiteEnvConfig.preset);
407
407
  }
408
408
  }
409
409