simulci 0.2.0 → 0.3.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.
Files changed (2) hide show
  1. package/dist/index.js +349 -176
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28797,7 +28797,7 @@ function verdictOf(gates) {
28797
28797
  //#endregion
28798
28798
  //#region ../engine/src/version.ts
28799
28799
  /** The simulci version. release-please updates it with every release. */
28800
- const VERSION = "0.2.0";
28800
+ const VERSION = "0.3.0";
28801
28801
  /** The schema version of every JSON document the engine emits or reads. A breaking change bumps it. */
28802
28802
  const SCHEMA_VERSIONS = {
28803
28803
  scenario: 1,
@@ -32235,7 +32235,134 @@ function diskSnapshotStore(root) {
32235
32235
  };
32236
32236
  }
32237
32237
  //#endregion
32238
+ //#region src/args.ts
32239
+ /**
32240
+ * A command line as citty reads it, with two of its gaps filled. citty 0.2 parses with node:util's parseArgs and no
32241
+ * `multiple`, so a flag given twice keeps only its last value: a `repeated` flag keeps them all. And a string flag
32242
+ * given no value takes the next flag as one: a `bare` flag that stands last, or before another flag, takes its bare
32243
+ * value instead.
32244
+ */
32245
+ function readArgs(rawArgs, def, opts) {
32246
+ const raw = withBareValues(rawArgs, def, opts.bare ?? {});
32247
+ const repeated = new Set(opts.repeated);
32248
+ const values = {};
32249
+ for (const t of optionTokens(raw, def)) if (repeated.has(t.key)) (values[t.key] ??= []).push(t.value ?? "");
32250
+ return {
32251
+ ...parseArgs$2([...raw], def),
32252
+ ...values
32253
+ };
32254
+ }
32255
+ /** A bare flag, as `--flag=<its bare value>`. */
32256
+ function withBareValues(raw, def, bare) {
32257
+ const out = [...raw];
32258
+ for (const t of optionTokens(raw, def)) {
32259
+ const value = bare[t.key];
32260
+ if (value !== void 0 && !t.inline && (t.value === void 0 || t.value.startsWith("-"))) out[t.index] = `--${kebab(t.key)}=${value}`;
32261
+ }
32262
+ return out;
32263
+ }
32264
+ /** The flags of a command line, tokenized as citty's parser sees them: without its `--no-` negations. */
32265
+ function optionTokens(raw, def) {
32266
+ const { options, keys } = optionsOf(def);
32267
+ const end = raw.indexOf("--");
32268
+ const kept = raw.flatMap((arg, i) => (end === -1 || i < end) && arg.startsWith("--no-") ? [] : [i]);
32269
+ const { tokens } = parseArgs({
32270
+ args: kept.map((i) => raw[i]),
32271
+ options,
32272
+ strict: false,
32273
+ allowPositionals: true,
32274
+ tokens: true
32275
+ });
32276
+ return tokens.flatMap((t) => {
32277
+ const key = t.kind === "option" ? keys.get(t.name) : void 0;
32278
+ if (t.kind !== "option" || key === void 0) return [];
32279
+ return [{
32280
+ key,
32281
+ index: kept[t.index],
32282
+ value: t.value,
32283
+ inline: t.inlineValue === true
32284
+ }];
32285
+ });
32286
+ }
32287
+ /** The options citty declares for a command's args: each name, its kebab- and camelCase forms and its aliases, typed. */
32288
+ function optionsOf(def) {
32289
+ const options = {};
32290
+ const keys = /* @__PURE__ */ new Map();
32291
+ for (const [key, arg] of Object.entries(def)) {
32292
+ if (arg.type === "positional") continue;
32293
+ const type = arg.type === "boolean" ? "boolean" : "string";
32294
+ const aliases = "alias" in arg ? [arg.alias ?? []].flat() : [];
32295
+ for (const name of /* @__PURE__ */ new Set([
32296
+ key,
32297
+ kebab(key),
32298
+ camel(key),
32299
+ ...aliases
32300
+ ])) {
32301
+ options[name] = { type };
32302
+ keys.set(name, key);
32303
+ }
32304
+ const short = aliases.find((a) => a.length === 1);
32305
+ if (short !== void 0) options[key] = {
32306
+ type,
32307
+ short
32308
+ };
32309
+ }
32310
+ return {
32311
+ options,
32312
+ keys
32313
+ };
32314
+ }
32315
+ const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
32316
+ const camel = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
32317
+ /** A flag given a value it does not take: the command prints the message and exits 2. */
32318
+ var FlagError = class extends Error {
32319
+ name = "FlagError";
32320
+ };
32321
+ /** `value` when it is one of `choices`; else a `FlagError` naming what `--<flag>` takes. */
32322
+ function oneOf(flag, value, choices) {
32323
+ if (choices.some((c) => c === value)) return value;
32324
+ throw new FlagError(`--${flag} takes ${spoken(choices)}, not '${String(value)}'`);
32325
+ }
32326
+ /** A list as a sentence says it: `a, b or c`. */
32327
+ const spoken = (items) => items.length > 1 ? `${items.slice(0, -1).join(", ")} or ${items.at(-1)}` : items.join("");
32328
+ /**
32329
+ * The exit code of a command's body; 2 when it throws, with the error's message on stderr (its stack under
32330
+ * `SIMULCI_DEBUG=1`). citty would print the stack and exit 1, which `simulci test` uses for a failed case.
32331
+ */
32332
+ function exitCodeOf(body, io) {
32333
+ try {
32334
+ return body();
32335
+ } catch (e) {
32336
+ io.stderr(`${errorText(e, io.env.SIMULCI_DEBUG === "1")}\n`);
32337
+ return 2;
32338
+ }
32339
+ }
32340
+ function errorText(e, stack) {
32341
+ if (!(e instanceof Error)) return String(e);
32342
+ return stack && e.stack !== void 0 ? e.stack : e.message;
32343
+ }
32344
+ /** A flag's values as a list: none, one, or every one it was given. */
32345
+ const values = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
32346
+ /** The `--assume` entries and the `--assume-file` list; an `AssumeError` when one does not parse or the file cannot be read. */
32347
+ function assumeOf(assume, file) {
32348
+ return parseAssume(values(assume), file === void 0 ? void 0 : readAssumeFile(file));
32349
+ }
32350
+ function readAssumeFile(file) {
32351
+ try {
32352
+ return readFileSync(resolve(file), "utf8");
32353
+ } catch (e) {
32354
+ throw new AssumeError(`--assume-file ${file}: ${e.code === "ENOENT" ? "no such file" : e.message}`);
32355
+ }
32356
+ }
32357
+ //#endregion
32238
32358
  //#region src/plan.ts
32359
+ const FORMATS = [
32360
+ "text",
32361
+ "json",
32362
+ "mermaid"
32363
+ ];
32364
+ const DETAILS$1 = ["steps", "legs"];
32365
+ const AS_TEST = ["jobs", "steps"];
32239
32366
  /** Builds the scenario from the arguments: `key=value` lists become records, repeated flags become arrays. */
32240
32367
  function scenarioFromArgs(args) {
32241
32368
  const s = { event: args.event };
@@ -32251,13 +32378,13 @@ function scenarioFromArgs(args) {
32251
32378
  const v = args[key];
32252
32379
  if (v !== void 0 && v !== "") s[key] = v;
32253
32380
  }
32254
- const changed = list$1(args.changed);
32381
+ const changed = list(args.changed);
32255
32382
  if (changed.length) s.changed = changed;
32256
- const inputs = pairs(list$1(args.input));
32383
+ const inputs = pairs(list(args.input));
32257
32384
  if (Object.keys(inputs).length) s.inputs = Object.fromEntries(Object.entries(inputs).map(([k, v]) => [k, typed(v)]));
32258
- const vars = pairs(list$1(args.var));
32385
+ const vars = pairs(list(args.var));
32259
32386
  if (Object.keys(vars).length) s.vars = vars;
32260
- const secrets = list$1(args.secret);
32387
+ const secrets = list(args.secret);
32261
32388
  if (secrets.length) s.secrets = secrets;
32262
32389
  if (args.actor !== void 0 || args.fork) {
32263
32390
  s.actor = {};
@@ -32266,7 +32393,7 @@ function scenarioFromArgs(args) {
32266
32393
  }
32267
32394
  return s;
32268
32395
  }
32269
- const list$1 = (v) => (v === void 0 ? [] : Array.isArray(v) ? v : [v]).flatMap((x) => x.split(",")).map((x) => x.trim()).filter(Boolean);
32396
+ const list = (v) => values(v).flatMap((x) => x.split(",")).map((x) => x.trim()).filter(Boolean);
32270
32397
  function pairs(items) {
32271
32398
  const out = {};
32272
32399
  for (const item of items) {
@@ -32287,116 +32414,145 @@ function render(plan, format) {
32287
32414
  if (format === "mermaid") return renderMermaid(plan);
32288
32415
  return renderText(plan);
32289
32416
  }
32417
+ const PLAN_ARGS = {
32418
+ dir: {
32419
+ type: "string",
32420
+ description: "The repository checkout",
32421
+ default: "."
32422
+ },
32423
+ event: {
32424
+ type: "string",
32425
+ description: "The event (push, pull_request, workflow_dispatch, …)",
32426
+ default: "push"
32427
+ },
32428
+ action: {
32429
+ type: "string",
32430
+ description: "The activity type (opened, labeled, …)"
32431
+ },
32432
+ ref: {
32433
+ type: "string",
32434
+ description: "The git ref (refs/heads/main, or a branch name)"
32435
+ },
32436
+ branch: {
32437
+ type: "string",
32438
+ description: "The branch (refs/heads/<branch>)"
32439
+ },
32440
+ tag: {
32441
+ type: "string",
32442
+ description: "The tag (refs/tags/<tag>)"
32443
+ },
32444
+ base: {
32445
+ type: "string",
32446
+ description: "The base branch of a pull request"
32447
+ },
32448
+ head: {
32449
+ type: "string",
32450
+ description: "The head branch of a pull request"
32451
+ },
32452
+ changed: {
32453
+ type: "string",
32454
+ description: "Changed files, comma-separated or repeated"
32455
+ },
32456
+ input: {
32457
+ type: "string",
32458
+ description: "A workflow_dispatch input, name=value (repeatable)"
32459
+ },
32460
+ var: {
32461
+ type: "string",
32462
+ description: "A variable, NAME=value (repeatable)"
32463
+ },
32464
+ secret: {
32465
+ type: "string",
32466
+ description: "A secret that exists, by name (repeatable)"
32467
+ },
32468
+ actor: {
32469
+ type: "string",
32470
+ description: "The actor login"
32471
+ },
32472
+ fork: {
32473
+ type: "boolean",
32474
+ description: "The pull request comes from a fork",
32475
+ default: false
32476
+ },
32477
+ message: {
32478
+ type: "string",
32479
+ description: "The head commit message"
32480
+ },
32481
+ format: {
32482
+ type: "string",
32483
+ description: spoken(FORMATS),
32484
+ default: "text"
32485
+ },
32486
+ detail: {
32487
+ type: "string",
32488
+ description: spoken(DETAILS$1),
32489
+ default: "steps"
32490
+ },
32491
+ assume: {
32492
+ type: "string",
32493
+ description: "A mock: build.unit=failure, build.unit.outputs.sha=abc (repeatable)"
32494
+ },
32495
+ "assume-file": {
32496
+ type: "string",
32497
+ description: "A YAML list of mocks"
32498
+ },
32499
+ "as-test": {
32500
+ type: "string",
32501
+ description: "Print a test case instead of the plan: jobs (also a bare --as-test) or steps"
32502
+ },
32503
+ workflow: {
32504
+ type: "string",
32505
+ description: "With --as-test: the workflow the case is about"
32506
+ }
32507
+ };
32290
32508
  const planCommand = defineCommand({
32291
32509
  meta: {
32292
32510
  name: "plan",
32293
32511
  description: "Which jobs and steps run for a scenario, and why"
32294
32512
  },
32295
- args: {
32296
- dir: {
32297
- type: "string",
32298
- description: "The repository checkout",
32299
- default: "."
32300
- },
32301
- event: {
32302
- type: "string",
32303
- description: "The event (push, pull_request, workflow_dispatch, …)",
32304
- default: "push"
32305
- },
32306
- action: {
32307
- type: "string",
32308
- description: "The activity type (opened, labeled, …)"
32309
- },
32310
- ref: {
32311
- type: "string",
32312
- description: "The git ref (refs/heads/main, or a branch name)"
32313
- },
32314
- branch: {
32315
- type: "string",
32316
- description: "The branch (refs/heads/<branch>)"
32317
- },
32318
- tag: {
32319
- type: "string",
32320
- description: "The tag (refs/tags/<tag>)"
32321
- },
32322
- base: {
32323
- type: "string",
32324
- description: "The base branch of a pull request"
32325
- },
32326
- head: {
32327
- type: "string",
32328
- description: "The head branch of a pull request"
32329
- },
32330
- changed: {
32331
- type: "string",
32332
- description: "Changed files, comma-separated or repeated"
32333
- },
32334
- input: {
32335
- type: "string",
32336
- description: "A workflow_dispatch input, name=value (repeatable)"
32337
- },
32338
- var: {
32339
- type: "string",
32340
- description: "A variable, NAME=value (repeatable)"
32341
- },
32342
- secret: {
32343
- type: "string",
32344
- description: "A secret that exists, by name (repeatable)"
32345
- },
32346
- actor: {
32347
- type: "string",
32348
- description: "The actor login"
32349
- },
32350
- fork: {
32351
- type: "boolean",
32352
- description: "The pull request comes from a fork",
32353
- default: false
32354
- },
32355
- message: {
32356
- type: "string",
32357
- description: "The head commit message"
32358
- },
32359
- format: {
32360
- type: "string",
32361
- description: "text, json or mermaid",
32362
- default: "text"
32363
- },
32364
- detail: {
32365
- type: "string",
32366
- description: "legs or steps",
32367
- default: "steps"
32368
- },
32369
- assume: {
32370
- type: "string",
32371
- description: "A mock: build.unit=failure, build.unit.outputs.sha=abc (repeatable)"
32372
- },
32373
- assumeFile: {
32374
- type: "string",
32375
- description: "A YAML list of mocks"
32376
- },
32377
- asTest: {
32378
- type: "string",
32379
- description: "Print a test case instead of the plan: jobs or steps"
32380
- },
32381
- workflow: {
32382
- type: "string",
32383
- description: "With --as-test: the workflow the case is about"
32384
- }
32385
- },
32386
- run({ args }) {
32387
- const out = planOutput(args);
32388
- process.stdout.write(out.text);
32389
- if (out.code) process.exitCode = out.code;
32513
+ args: PLAN_ARGS,
32514
+ run({ rawArgs }) {
32515
+ const io = {
32516
+ stderr: (t) => process.stderr.write(t),
32517
+ env: process.env
32518
+ };
32519
+ process.exitCode = exitCodeOf(() => {
32520
+ const out = planOutput(planArgs(rawArgs));
32521
+ (out.code ? process.stderr : process.stdout).write(out.text);
32522
+ return out.code;
32523
+ }, io);
32390
32524
  }
32391
32525
  });
32392
- /** What `simulci plan` prints, and the exit code when it cannot. */
32526
+ /**
32527
+ * The arguments of a `simulci plan` command line: every value of a repeated flag, and `jobs` for a bare `--as-test`.
32528
+ * A `FlagError` when `--format`, `--detail` or `--as-test` is given a value it does not take.
32529
+ */
32530
+ function planArgs(rawArgs) {
32531
+ const a = readArgs(rawArgs, PLAN_ARGS, {
32532
+ repeated: [
32533
+ "changed",
32534
+ "input",
32535
+ "var",
32536
+ "secret",
32537
+ "assume"
32538
+ ],
32539
+ bare: { "as-test": "jobs" }
32540
+ });
32541
+ return {
32542
+ ...a,
32543
+ format: oneOf("format", a.format, FORMATS),
32544
+ detail: oneOf("detail", a.detail, DETAILS$1),
32545
+ asTest: a.asTest === void 0 || a.asTest === false ? void 0 : oneOf("as-test", a.asTest, AS_TEST)
32546
+ };
32547
+ }
32548
+ /** What `simulci plan` prints, and the exit code: 2, with the message, when it cannot. */
32393
32549
  function planOutput(a) {
32394
32550
  const files = readWorkflowFiles(a.dir);
32395
32551
  if (Object.keys(files).length === 0) return {
32396
32552
  text: `no workflows under ${a.dir}/.github/workflows\n`,
32397
32553
  code: 2
32398
32554
  };
32399
- const assume = assumeOf(a);
32555
+ const assume = mocksOf(a);
32400
32556
  if (typeof assume === "string") return {
32401
32557
  text: assume,
32402
32558
  code: 2
@@ -32406,16 +32562,16 @@ function planOutput(a) {
32406
32562
  const plan = simulate({
32407
32563
  files,
32408
32564
  scenario,
32409
- detail: a.detail === "legs" ? "legs" : "steps",
32565
+ detail: a.detail,
32410
32566
  mocks: { cli: assume }
32411
32567
  });
32412
- if (a.asTest === void 0 || a.asTest === false) return {
32568
+ if (a.asTest === void 0) return {
32413
32569
  text: render(plan, a.format),
32414
32570
  code: 0
32415
32571
  };
32416
32572
  return asTest(plan, a, scenario, assume);
32417
32573
  } catch (e) {
32418
- if (!(e instanceof MocksError)) throw e;
32574
+ if (!(e instanceof MocksError || e instanceof ScenarioError)) throw e;
32419
32575
  return {
32420
32576
  text: `${e.message}\n`,
32421
32577
  code: 2
@@ -32423,9 +32579,9 @@ function planOutput(a) {
32423
32579
  }
32424
32580
  }
32425
32581
  /** The `--assume` entries, or the message to print when they do not parse. */
32426
- function assumeOf(a) {
32582
+ function mocksOf(a) {
32427
32583
  try {
32428
- return parseAssume(a.assume === void 0 ? [] : Array.isArray(a.assume) ? a.assume : [a.assume], a.assumeFile === void 0 ? void 0 : readFileSync(a.assumeFile, "utf8"));
32584
+ return assumeOf(a.assume, a.assumeFile);
32429
32585
  } catch (e) {
32430
32586
  if (!(e instanceof AssumeError)) throw e;
32431
32587
  return `${e.message}\n`;
@@ -32439,19 +32595,26 @@ function asTest(plan, a, scenario, assume) {
32439
32595
  text: "no workflow to write a test for\n",
32440
32596
  code: 2
32441
32597
  };
32598
+ const path = workflow.startsWith(".github/workflows/") ? workflow : `.github/workflows/${workflow}`;
32599
+ if (!plan.workflows.some((w) => w.depth === 0 && w.path === path)) return {
32600
+ text: `--workflow ${workflow}: no ${path} in the checkout\n`,
32601
+ code: 2
32602
+ };
32442
32603
  return {
32443
32604
  text: fileFromPlan({
32444
32605
  plan,
32445
32606
  workflow,
32446
32607
  scenario,
32447
32608
  assume,
32448
- detail: a.asTest === "steps" ? "steps" : "jobs"
32609
+ detail: a.asTest ?? "jobs"
32449
32610
  }),
32450
32611
  code: 0
32451
32612
  };
32452
32613
  }
32453
32614
  //#endregion
32454
32615
  //#region src/test.ts
32616
+ /** `simulci test`: the route tests under `.github/simulci/tests`, or the files named. */
32617
+ const DETAILS = ["steps", "legs"];
32455
32618
  /** Runs the command; returns the exit code. */
32456
32619
  function runTestCommand(a, io) {
32457
32620
  const ready = prepare(a, io);
@@ -32461,7 +32624,7 @@ function runTestCommand(a, io) {
32461
32624
  ci: a.ci ?? io.env.CI === "true",
32462
32625
  update: a.update ?? false,
32463
32626
  strict: a.strict ?? false,
32464
- snapshotDetail: a.snapshotDetail === "legs" ? "legs" : "steps",
32627
+ snapshotDetail: a.snapshotDetail ?? "steps",
32465
32628
  snapshots: diskSnapshotStore(root)
32466
32629
  };
32467
32630
  if (a.filter !== void 0) options.filter = a.filter;
@@ -32480,7 +32643,7 @@ function runTestCommand(a, io) {
32480
32643
  /** The checkout, the test files, the project and the cli mocks; or the exit code when one of them is not there. */
32481
32644
  function prepare(a, io) {
32482
32645
  const root = resolve(a.dir);
32483
- const files = discoverTests(root, list(a.files));
32646
+ const files = discoverTests(root, values(a.files));
32484
32647
  const misplaced = misplacedTests(files);
32485
32648
  if (misplaced.length) {
32486
32649
  io.stderr(`T000 ${misplaced.join(", ")}: a test file under .github/workflows would be run by GitHub; put it under ${TESTS_DIR}\n`);
@@ -32492,12 +32655,11 @@ function prepare(a, io) {
32492
32655
  return 2;
32493
32656
  }
32494
32657
  try {
32495
- const fileText = a.assumeFile === void 0 ? void 0 : readFileSync(resolve(a.assumeFile), "utf8");
32496
32658
  return {
32497
32659
  root,
32498
32660
  files,
32499
32661
  project: loadFiles(workflows),
32500
- cli: parseAssume(list(a.assume), fileText)
32662
+ cli: assumeOf(a.assume, a.assumeFile)
32501
32663
  };
32502
32664
  } catch (e) {
32503
32665
  if (!(e instanceof AssumeError)) throw e;
@@ -32513,79 +32675,90 @@ function report(result, a, io) {
32513
32675
  writeFileSync(resolve(a.junit), renderJUnit(result));
32514
32676
  }
32515
32677
  }
32516
- const list = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
32678
+ const TEST_ARGS = {
32679
+ files: {
32680
+ type: "positional",
32681
+ description: "Test files (default: every *.test.yml under .github/simulci/tests)",
32682
+ required: false
32683
+ },
32684
+ dir: {
32685
+ type: "string",
32686
+ description: "The repository checkout",
32687
+ default: "."
32688
+ },
32689
+ junit: {
32690
+ type: "string",
32691
+ description: "Write a JUnit report to this file"
32692
+ },
32693
+ update: {
32694
+ type: "boolean",
32695
+ alias: "u",
32696
+ description: "Write every snapshot and prune stale ones",
32697
+ default: false
32698
+ },
32699
+ "snapshot-detail": {
32700
+ type: "string",
32701
+ description: spoken(DETAILS),
32702
+ default: "steps"
32703
+ },
32704
+ filter: {
32705
+ type: "string",
32706
+ alias: "t",
32707
+ description: "Run the cases whose name contains the text"
32708
+ },
32709
+ ci: {
32710
+ type: "boolean",
32711
+ description: "Never write snapshots; a missing one fails; only is an error (default when CI=true)"
32712
+ },
32713
+ strict: {
32714
+ type: "boolean",
32715
+ description: "Ignore allowMaybe; stale snapshots fail",
32716
+ default: false
32717
+ },
32718
+ annotations: {
32719
+ type: "boolean",
32720
+ description: "Print GitHub annotations (default when GITHUB_ACTIONS=true)"
32721
+ },
32722
+ assume: {
32723
+ type: "string",
32724
+ description: "A mock for every case: build.unit=failure, build.unit.outputs.sha=abc (repeatable)"
32725
+ },
32726
+ "assume-file": {
32727
+ type: "string",
32728
+ description: "A YAML list of mocks for every case"
32729
+ }
32730
+ };
32517
32731
  const testCommand = defineCommand({
32518
32732
  meta: {
32519
32733
  name: "test",
32520
32734
  description: "Run the route tests under .github/simulci/tests"
32521
32735
  },
32522
- args: {
32523
- files: {
32524
- type: "positional",
32525
- description: "Test files (default: every *.test.yml under .github/simulci/tests)",
32526
- required: false
32527
- },
32528
- dir: {
32529
- type: "string",
32530
- description: "The repository checkout",
32531
- default: "."
32532
- },
32533
- junit: {
32534
- type: "string",
32535
- description: "Write a JUnit report to this file"
32536
- },
32537
- update: {
32538
- type: "boolean",
32539
- alias: "u",
32540
- description: "Write every snapshot and prune stale ones",
32541
- default: false
32542
- },
32543
- snapshotDetail: {
32544
- type: "string",
32545
- description: "steps or legs",
32546
- default: "steps"
32547
- },
32548
- filter: {
32549
- type: "string",
32550
- alias: "t",
32551
- description: "Run the cases whose name contains the text"
32552
- },
32553
- ci: {
32554
- type: "boolean",
32555
- description: "Never write snapshots; a missing one fails; only is an error (default when CI=true)"
32556
- },
32557
- strict: {
32558
- type: "boolean",
32559
- description: "Ignore allowMaybe; stale snapshots fail",
32560
- default: false
32561
- },
32562
- annotations: {
32563
- type: "boolean",
32564
- description: "Print GitHub annotations (default when GITHUB_ACTIONS=true)"
32565
- },
32566
- assume: {
32567
- type: "string",
32568
- description: "A mock for every case: build.unit=failure, build.unit.outputs.sha=abc (repeatable)"
32569
- },
32570
- assumeFile: {
32571
- type: "string",
32572
- description: "A YAML list of mocks for every case"
32573
- }
32574
- },
32575
- run({ args }) {
32576
- const a = args;
32577
- const files = a._?.length ? a._ : a.files;
32578
- const code = runTestCommand({
32579
- ...a,
32580
- ...files === void 0 ? {} : { files }
32581
- }, {
32736
+ args: TEST_ARGS,
32737
+ run({ rawArgs }) {
32738
+ const io = {
32582
32739
  stdout: (t) => process.stdout.write(t),
32583
32740
  stderr: (t) => process.stderr.write(t),
32584
32741
  env: process.env
32585
- });
32586
- process.exitCode = code;
32742
+ };
32743
+ process.exitCode = exitCodeOf(() => runTestCommand(testArgs(rawArgs), io), io);
32587
32744
  }
32588
32745
  });
32746
+ /**
32747
+ * The arguments of a `simulci test` command line: every test file named, and every `--assume`. A `FlagError` when
32748
+ * `--snapshot-detail` is given a value it does not take.
32749
+ */
32750
+ function testArgs(rawArgs) {
32751
+ const a = readArgs(rawArgs, TEST_ARGS, { repeated: ["assume"] });
32752
+ const args = {
32753
+ ...a,
32754
+ snapshotDetail: oneOf("snapshot-detail", a.snapshotDetail, DETAILS)
32755
+ };
32756
+ const files = a._;
32757
+ return files.length ? {
32758
+ ...args,
32759
+ files
32760
+ } : args;
32761
+ }
32589
32762
  runMain(defineCommand({
32590
32763
  meta: {
32591
32764
  name: "simulci",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simulci",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Static simulator and test framework for GitHub Actions: which jobs and steps run for a scenario, and why.",
5
5
  "keywords": [
6
6
  "ci",