@c9up/helix 0.1.7 → 0.1.9

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 (124) hide show
  1. package/README.md +282 -11
  2. package/bin/helix.js +329 -13
  3. package/dist/cli/failed-cache.d.ts +27 -0
  4. package/dist/cli/failed-cache.d.ts.map +1 -0
  5. package/dist/cli/failed-cache.js +64 -0
  6. package/dist/cli/failed-cache.js.map +1 -0
  7. package/dist/cli/glob.d.ts +36 -0
  8. package/dist/cli/glob.d.ts.map +1 -0
  9. package/dist/cli/glob.js +185 -0
  10. package/dist/cli/glob.js.map +1 -0
  11. package/dist/cli/native.d.ts +4 -0
  12. package/dist/cli/native.d.ts.map +1 -1
  13. package/dist/cli/native.js.map +1 -1
  14. package/dist/cli/pool.d.ts +11 -0
  15. package/dist/cli/pool.d.ts.map +1 -1
  16. package/dist/cli/pool.js +12 -0
  17. package/dist/cli/pool.js.map +1 -1
  18. package/dist/cli/reporter.d.ts +11 -0
  19. package/dist/cli/reporter.d.ts.map +1 -1
  20. package/dist/cli/reporter.js +53 -0
  21. package/dist/cli/reporter.js.map +1 -1
  22. package/dist/cli/run.d.ts +41 -0
  23. package/dist/cli/run.d.ts.map +1 -1
  24. package/dist/cli/run.js +181 -21
  25. package/dist/cli/run.js.map +1 -1
  26. package/dist/cli/runner.d.ts +19 -0
  27. package/dist/cli/runner.d.ts.map +1 -0
  28. package/dist/cli/runner.js +15 -0
  29. package/dist/cli/runner.js.map +1 -0
  30. package/dist/cli/suites.d.ts +102 -0
  31. package/dist/cli/suites.d.ts.map +1 -0
  32. package/dist/cli/suites.js +226 -0
  33. package/dist/cli/suites.js.map +1 -0
  34. package/dist/index.d.ts +10 -9
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +9 -8
  37. package/dist/index.js.map +1 -1
  38. package/dist/japa/core.d.ts +106 -0
  39. package/dist/japa/core.d.ts.map +1 -0
  40. package/dist/japa/core.js +128 -0
  41. package/dist/japa/core.js.map +1 -0
  42. package/dist/runtime/assert.d.ts +174 -0
  43. package/dist/runtime/assert.d.ts.map +1 -0
  44. package/dist/runtime/assert.js +591 -0
  45. package/dist/runtime/assert.js.map +1 -0
  46. package/dist/runtime/asymmetric.d.ts +34 -0
  47. package/dist/runtime/asymmetric.d.ts.map +1 -0
  48. package/dist/runtime/asymmetric.js +78 -0
  49. package/dist/runtime/asymmetric.js.map +1 -0
  50. package/dist/runtime/bootstrap.d.ts +47 -0
  51. package/dist/runtime/bootstrap.d.ts.map +1 -0
  52. package/dist/runtime/bootstrap.js +166 -0
  53. package/dist/runtime/bootstrap.js.map +1 -0
  54. package/dist/runtime/cli-args.d.ts +77 -0
  55. package/dist/runtime/cli-args.d.ts.map +1 -0
  56. package/dist/runtime/cli-args.js +85 -0
  57. package/dist/runtime/cli-args.js.map +1 -0
  58. package/dist/runtime/configure.d.ts +214 -0
  59. package/dist/runtime/configure.d.ts.map +1 -0
  60. package/dist/runtime/configure.js +164 -0
  61. package/dist/runtime/configure.js.map +1 -0
  62. package/dist/runtime/context.d.ts +84 -0
  63. package/dist/runtime/context.d.ts.map +1 -0
  64. package/dist/runtime/context.js +146 -0
  65. package/dist/runtime/context.js.map +1 -0
  66. package/dist/runtime/emitter.d.ts +141 -0
  67. package/dist/runtime/emitter.d.ts.map +1 -0
  68. package/dist/runtime/emitter.js +79 -0
  69. package/dist/runtime/emitter.js.map +1 -0
  70. package/dist/runtime/equals.d.ts.map +1 -1
  71. package/dist/runtime/equals.js +21 -0
  72. package/dist/runtime/equals.js.map +1 -1
  73. package/dist/runtime/expect.d.ts +22 -1
  74. package/dist/runtime/expect.d.ts.map +1 -1
  75. package/dist/runtime/expect.js +15 -1
  76. package/dist/runtime/expect.js.map +1 -1
  77. package/dist/runtime/global-hooks.d.ts +31 -0
  78. package/dist/runtime/global-hooks.d.ts.map +1 -0
  79. package/dist/runtime/global-hooks.js +126 -0
  80. package/dist/runtime/global-hooks.js.map +1 -0
  81. package/dist/runtime/index.d.ts +16 -3
  82. package/dist/runtime/index.d.ts.map +1 -1
  83. package/dist/runtime/index.js +7 -1
  84. package/dist/runtime/index.js.map +1 -1
  85. package/dist/runtime/matchers.d.ts +5 -0
  86. package/dist/runtime/matchers.d.ts.map +1 -1
  87. package/dist/runtime/matchers.js +117 -0
  88. package/dist/runtime/matchers.js.map +1 -1
  89. package/dist/runtime/run.d.ts +63 -1
  90. package/dist/runtime/run.d.ts.map +1 -1
  91. package/dist/runtime/run.js +696 -79
  92. package/dist/runtime/run.js.map +1 -1
  93. package/dist/runtime/runner.d.ts +97 -0
  94. package/dist/runtime/runner.d.ts.map +1 -0
  95. package/dist/runtime/runner.js +160 -0
  96. package/dist/runtime/runner.js.map +1 -0
  97. package/dist/runtime/suite-config.d.ts +23 -0
  98. package/dist/runtime/suite-config.d.ts.map +1 -0
  99. package/dist/runtime/suite-config.js +52 -0
  100. package/dist/runtime/suite-config.js.map +1 -0
  101. package/dist/runtime/suite-taps.d.ts +122 -0
  102. package/dist/runtime/suite-taps.d.ts.map +1 -0
  103. package/dist/runtime/suite-taps.js +218 -0
  104. package/dist/runtime/suite-taps.js.map +1 -0
  105. package/dist/runtime/suite.d.ts +316 -10
  106. package/dist/runtime/suite.d.ts.map +1 -1
  107. package/dist/runtime/suite.js +307 -8
  108. package/dist/runtime/suite.js.map +1 -1
  109. package/dist/runtime/test-context.d.ts +88 -1
  110. package/dist/runtime/test-context.d.ts.map +1 -1
  111. package/dist/runtime/test-context.js +170 -2
  112. package/dist/runtime/test-context.js.map +1 -1
  113. package/dist/runtime/vi/fake-timers.d.ts.map +1 -1
  114. package/dist/runtime/vi/fake-timers.js +3 -4
  115. package/dist/runtime/vi/fake-timers.js.map +1 -1
  116. package/dist/runtime/worker.d.ts.map +1 -1
  117. package/dist/runtime/worker.js +87 -5
  118. package/dist/runtime/worker.js.map +1 -1
  119. package/index.darwin-arm64.node +0 -0
  120. package/index.darwin-x64.node +0 -0
  121. package/index.linux-arm64-gnu.node +0 -0
  122. package/index.linux-x64-gnu.node +0 -0
  123. package/index.win32-x64-msvc.node +0 -0
  124. package/package.json +10 -1
package/bin/helix.js CHANGED
@@ -38,7 +38,62 @@ const FLAG_SPEC = {
38
38
  tsx: { kind: "boolean", help: "Use tsx loader for workers (default: true)" },
39
39
  include: { kind: "string[]", help: "Glob patterns to include" },
40
40
  exclude: { kind: "string[]", help: "Glob patterns to exclude" },
41
- grep: { kind: "string", help: "Filter tests by name (not yet implemented)" },
41
+ grep: {
42
+ kind: "string",
43
+ help: "Only run tests whose full name matches (regex or substring)",
44
+ },
45
+ retries: { kind: "number", help: "Extra attempts on failure (default 0)" },
46
+ tags: {
47
+ kind: "string",
48
+ help: "Comma-separated tag filter (@fast, ~@slow to exclude); OR by default",
49
+ },
50
+ "match-all": {
51
+ kind: "boolean",
52
+ help: "Require ALL --tags instead of any (Japa parity)",
53
+ },
54
+ // Japa's parser accepts both spellings (`matchAll` with `match-all` as its
55
+ // alias), so both work here too.
56
+ matchAll: {
57
+ kind: "boolean",
58
+ help: "Alias of --match-all",
59
+ },
60
+ tests: {
61
+ kind: "string",
62
+ help: "Comma-separated exact test titles to run (Japa --tests)",
63
+ },
64
+ groups: {
65
+ kind: "string",
66
+ help: "Comma-separated exact group titles to run (Japa --groups)",
67
+ },
68
+ suite: {
69
+ kind: "string",
70
+ help: "Name of the suite these files belong to (Japa meta.suite)",
71
+ },
72
+ files: {
73
+ kind: "string",
74
+ help: "Comma-separated substrings matched against test file paths (Japa --files)",
75
+ },
76
+ reporters: {
77
+ kind: "string",
78
+ help: "Comma-separated reporters to activate, e.g. spec,json (Japa --reporters)",
79
+ },
80
+ bail: { kind: "boolean", help: "Stop at the first failure (Japa --bail)" },
81
+ "bail-layer": {
82
+ kind: "string",
83
+ help: "How far a bail reaches: group|suite|runner (Japa --bail-layer)",
84
+ },
85
+ failed: {
86
+ kind: "boolean",
87
+ help: "Re-run only the tests that failed last run (Japa --failed)",
88
+ },
89
+ "list-pinned": {
90
+ kind: "boolean",
91
+ help: "Print the tests marked .pin() and run nothing (Japa --list-pinned)",
92
+ },
93
+ "force-exit": {
94
+ kind: "boolean",
95
+ help: "process.exit() as soon as the run ends, without draining the event loop (Japa --force-exit)",
96
+ },
42
97
  watch: { kind: "boolean", help: "Watch mode — re-run on file changes" },
43
98
  "watch-debounce": {
44
99
  kind: "number",
@@ -188,12 +243,50 @@ function findTsxLoader() {
188
243
  return undefined;
189
244
  }
190
245
 
246
+ /** Trailing `.test.ts` / `.spec.js` / … stripped, so `--files=user` matches `user.test.ts`. */
247
+ const TEST_SUFFIX = /(\.(test|spec))?\.[cm]?[jt]sx?$/;
248
+
249
+ /**
250
+ * Japa's `--files` rule: keep a file when its path ENDS WITH the filter, or
251
+ * when every segment of the filter (read right-to-left, `*` matching anything)
252
+ * matches the corresponding path segment of the file minus its test suffix.
253
+ * `--files=user` keeps `tests/unit/user.test.ts`; `--files=unit/*` keeps
254
+ * everything under a `unit` directory.
255
+ */
256
+ function filterByFileFilters(files, raw) {
257
+ if (raw === undefined) return files;
258
+ const filters = String(raw)
259
+ .split(",")
260
+ .map((f) => f.trim())
261
+ .filter((f) => f.length > 0);
262
+ if (filters.length === 0) return files;
263
+ return files.filter((file) => {
264
+ const unix = file.split(path.sep).join("/");
265
+ const withoutSuffix = unix.replace(TEST_SUFFIX, "");
266
+ return filters.some((filter) => {
267
+ if (unix.endsWith(filter)) return true;
268
+ const filterSegments = filter.split("/").reverse();
269
+ const fileSegments = withoutSuffix.split("/").reverse();
270
+ return filterSegments.every(
271
+ (segment, i) =>
272
+ fileSegments[i] !== undefined &&
273
+ (segment === "*" || fileSegments[i].endsWith(segment)),
274
+ );
275
+ });
276
+ });
277
+ }
278
+
191
279
  function printHelp() {
192
280
  const lines = [
193
281
  "helix — Vitest-compatible test runner",
194
282
  "",
195
283
  "Usage:",
196
- " helix test [files...|dirs...]",
284
+ " helix test [files...|dirs...|suites...]",
285
+ "",
286
+ " Positionals name test files or directories. When every one of them",
287
+ " matches a suite declared in helix.config.*, they name SUITES instead",
288
+ " (AdonisJS `node ace test unit functional`). With no positional and a",
289
+ " config present, every suite runs, in order.",
197
290
  "",
198
291
  "Flags:",
199
292
  ];
@@ -290,15 +383,66 @@ async function main() {
290
383
  ).href;
291
384
  try {
292
385
  // Probe by resolving through dynamic import; Node throws synchronously.
293
- const { run } = await import(runModule);
386
+ const { run, runSuites } = await import(runModule);
294
387
  const discoverModule = pathToFileURL(
295
388
  useDist
296
389
  ? path.resolve(here, "../dist/cli/discover.js")
297
390
  : path.resolve(here, "../src/cli/discover.ts"),
298
391
  ).href;
299
392
  const { discover } = await import(discoverModule);
393
+ const suitesModule = pathToFileURL(
394
+ useDist
395
+ ? path.resolve(here, "../dist/cli/suites.js")
396
+ : path.resolve(here, "../src/cli/suites.ts"),
397
+ ).href;
398
+ const {
399
+ loadHelixConfig,
400
+ resolveHelixConfig,
401
+ resolveSuiteFiles,
402
+ selectSuites,
403
+ } = await import(suitesModule);
404
+
405
+ // AdonisJS parity: positionals may name SUITES declared in
406
+ // `helix.config.*` (`helix test unit`). When they don't — or there is no
407
+ // config — they stay what they have always been: paths.
408
+ const helixConfig = await loadHelixConfig(process.cwd());
409
+ const selectedSuites = selectSuites(helixConfig, parsed.positional);
410
+
411
+ // AdonisJS `tests/bootstrap.ts`: resolved once here, imported by every
412
+ // worker before its test file. Forwarded through the env so it reaches
413
+ // the worker under BOTH orchestrators.
414
+ const bootstrapModule = pathToFileURL(
415
+ useDist
416
+ ? path.resolve(here, "../dist/runtime/bootstrap.js")
417
+ : path.resolve(here, "../src/runtime/bootstrap.ts"),
418
+ ).href;
419
+ const { resolveBootstrap } = await import(bootstrapModule);
420
+ const { runGlobalHooks } = await import(
421
+ pathToFileURL(
422
+ useDist
423
+ ? path.resolve(here, "../dist/runtime/global-hooks.js")
424
+ : path.resolve(here, "../src/runtime/global-hooks.ts"),
425
+ ).href
426
+ );
427
+ process.env.HELIX_BOOTSTRAP =
428
+ resolveBootstrap(process.cwd(), helixConfig.bootstrap) ?? "";
429
+ // A `suites[].configure` callback lives in the config module; the worker
430
+ // re-imports it by path, since the function itself cannot be forwarded.
431
+ // Only named when a suite actually declares one, so a project without any
432
+ // pays nothing.
433
+ const declaresConfigure = (helixConfig.suites ?? []).some(
434
+ (suite) => typeof suite.configure === "function",
435
+ );
436
+ process.env.HELIX_SUITE_CONFIG = declaresConfigure
437
+ ? (resolveHelixConfig(process.cwd()) ?? "")
438
+ : "";
300
439
 
301
- const expanded = await expandPositionals(parsed.positional, discover);
440
+ const expanded = selectedSuites
441
+ ? []
442
+ : filterByFileFilters(
443
+ await expandPositionals(parsed.positional, discover),
444
+ parsed.flags.files,
445
+ );
302
446
  const tsxLoader = findTsxLoader();
303
447
  if (parsed.flags.tsx !== false && !tsxLoader) {
304
448
  process.stderr.write(
@@ -340,17 +484,41 @@ async function main() {
340
484
  root: process.cwd(),
341
485
  files: expanded,
342
486
  threads: parsed.flags.threads,
343
- timeoutMs: parsed.flags.timeout,
487
+ timeoutMs: parsed.flags.timeout ?? helixConfig.timeout,
344
488
  reporter: parsed.flags.reporter,
489
+ reporters: parsed.flags.reporters
490
+ ? String(parsed.flags.reporters)
491
+ .split(",")
492
+ .map((r) => r.trim())
493
+ .filter((r) => r.length > 0)
494
+ : undefined,
495
+ bail: parsed.flags.bail === true,
496
+ listPinned: parsed.flags["list-pinned"] === true,
497
+ failed: parsed.flags.failed === true,
345
498
  useColors: parsed.flags.colors,
346
499
  discovery: {
347
500
  suffixes: parsed.flags.include,
348
501
  hardExcludes: parsed.flags.exclude,
349
502
  },
350
- nodeArgs:
351
- parsed.flags.tsx === false || !tsxLoader
352
- ? undefined
353
- : ["--import", tsxLoader],
503
+ // `japaPlugins` points `@japa/runner/core` at helix's shim in every
504
+ // worker, so an official Japa plugin instruments helix. Appended to
505
+ // the loader list rather than replacing it: the test files still need
506
+ // their TS loader.
507
+ nodeArgs: [
508
+ ...(parsed.flags.tsx === false || !tsxLoader
509
+ ? []
510
+ : ["--import", tsxLoader]),
511
+ ...(helixConfig.japaPlugins === true
512
+ ? [
513
+ "--import",
514
+ pathToFileURL(
515
+ useDist
516
+ ? path.resolve(here, "../dist/japa/japa-alias.mjs")
517
+ : path.resolve(here, "../src/japa/japa-alias.mjs"),
518
+ ).href,
519
+ ]
520
+ : []),
521
+ ],
354
522
  coverage: parsed.flags.coverage
355
523
  ? {
356
524
  enabled: true,
@@ -373,8 +541,140 @@ async function main() {
373
541
  }
374
542
  : undefined,
375
543
  };
376
- const outcome = await run(cfg);
377
- return outcome.exitCode;
544
+ // Per-test runtime filters travel to workers via env so they survive
545
+ // BOTH the TS pool and the Rust native orchestrator (child processes
546
+ // inherit this process's env; see runtime/worker.ts).
547
+ if (parsed.flags.grep !== undefined) {
548
+ process.env.HELIX_GREP = String(parsed.flags.grep);
549
+ }
550
+ if (parsed.flags.retries !== undefined) {
551
+ process.env.HELIX_RETRIES = String(parsed.flags.retries);
552
+ }
553
+ // Not used to resolve the timeout (the orchestrator carries it in the
554
+ // run instruction) — exported so a plugin can read it off `api.cliArgs`.
555
+ if (parsed.flags.timeout !== undefined) {
556
+ process.env.HELIX_TIMEOUT = String(parsed.flags.timeout);
557
+ }
558
+ if (parsed.flags.tags !== undefined) {
559
+ process.env.HELIX_TAGS = String(parsed.flags.tags);
560
+ }
561
+ if (parsed.flags["match-all"] === true || parsed.flags.matchAll === true) {
562
+ process.env.HELIX_MATCH_ALL = "1";
563
+ }
564
+ if (parsed.flags.tests !== undefined) {
565
+ process.env.HELIX_TESTS = String(parsed.flags.tests);
566
+ }
567
+ if (parsed.flags.groups !== undefined) {
568
+ process.env.HELIX_GROUPS = String(parsed.flags.groups);
569
+ }
570
+ if (parsed.flags.suite !== undefined) {
571
+ process.env.HELIX_SUITE = String(parsed.flags.suite);
572
+ }
573
+ if (parsed.flags.files !== undefined) {
574
+ process.env.HELIX_FILES = String(parsed.flags.files);
575
+ }
576
+ if (parsed.flags.bail === true) {
577
+ process.env.HELIX_BAIL = "1";
578
+ }
579
+ // Flags a plugin reads off `api.cliArgs` but the runtime itself doesn't
580
+ // act on — forwarded for the same reason as the filters: Japa hands its
581
+ // plugins the whole flag set.
582
+ if (cfg.reporters !== undefined) {
583
+ process.env.HELIX_REPORTERS = cfg.reporters.join(",");
584
+ } else if (parsed.flags.reporter !== undefined) {
585
+ process.env.HELIX_REPORTERS = String(parsed.flags.reporter);
586
+ }
587
+ if (parsed.flags.failed === true) {
588
+ process.env.HELIX_FAILED = "1";
589
+ }
590
+ // `--force-exit`, or `forceExit` in the config (AdonisJS `tests.forceExit`).
591
+ // Read back by `finish()` below, and by a plugin off `api.cliArgs`.
592
+ if (parsed.flags["list-pinned"] === true) {
593
+ process.env.HELIX_LIST_PINNED = "1";
594
+ }
595
+ // Positionals reach a plugin as `api.cliArgs._`, like Japa's.
596
+ if (parsed.positional.length > 0) {
597
+ process.env.HELIX_POSITIONALS = parsed.positional.join(",");
598
+ }
599
+ if (parsed.flags["force-exit"] === true || helixConfig.forceExit === true) {
600
+ process.env.HELIX_FORCE_EXIT = "1";
601
+ }
602
+ if (parsed.flags["bail-layer"] !== undefined) {
603
+ process.env.HELIX_BAIL_LAYER = String(parsed.flags["bail-layer"]);
604
+ }
605
+ // `--failed` replays the previous run's failures as a `--tests` filter,
606
+ // exactly like Japa's retry plugin.
607
+ if (parsed.flags.failed === true) {
608
+ const failedModule = pathToFileURL(
609
+ useDist
610
+ ? path.resolve(here, "../dist/cli/failed-cache.js")
611
+ : path.resolve(here, "../src/cli/failed-cache.ts"),
612
+ ).href;
613
+ const { readFailedCache } = await import(failedModule);
614
+ const failedTests = await readFailedCache(process.cwd());
615
+ if (failedTests.length === 0) {
616
+ process.stdout.write(
617
+ "helix: no failing tests cached — running all of them\n",
618
+ );
619
+ } else {
620
+ process.env.HELIX_TESTS = failedTests.join(",");
621
+ }
622
+ }
623
+
624
+ // `runnerHooks` run ONCE around the whole run, here, and the workers are
625
+ // told to skip them — Japa's semantics, and the difference between
626
+ // migrating once and migrating once per test file.
627
+ const dropGlobalHooks = await runGlobalHooks(process.env.HELIX_BOOTSTRAP, {
628
+ japaPlugins: helixConfig.japaPlugins === true,
629
+ });
630
+
631
+ if (!selectedSuites) {
632
+ try {
633
+ const outcome = await run(cfg);
634
+ return outcome.exitCode;
635
+ } finally {
636
+ await dropGlobalHooks();
637
+ }
638
+ }
639
+
640
+ // Suites run one after another (Japa runs them in sequence too), each
641
+ // with its own files, timeout, retries and `meta.suite` name. The
642
+ // sequence is handed to the orchestrator as a whole so watch mode
643
+ // wraps ALL of it in one watcher and the `--failed` cache holds every
644
+ // suite's failures.
645
+ const steps = [];
646
+ for (const suite of selectedSuites) {
647
+ const suiteFiles = filterByFileFilters(
648
+ await resolveSuiteFiles(suite, process.cwd(), cfg.discovery),
649
+ parsed.flags.files,
650
+ );
651
+ if (suiteFiles.length === 0) {
652
+ process.stderr.write(`helix: suite "${suite.name}": no test files\n`);
653
+ continue;
654
+ }
655
+ // A suite's `retries` overrides `--retries` for that suite only. Set
656
+ // on EVERY step (empty = unset, see `envCount`) so a suite that
657
+ // declares none doesn't inherit the previous suite's value.
658
+ const retries = suite.retries ?? parsed.flags.retries;
659
+ const env = {
660
+ HELIX_SUITE: suite.name,
661
+ HELIX_RETRIES: retries === undefined ? "" : String(retries),
662
+ };
663
+ steps.push({
664
+ env,
665
+ config: {
666
+ ...cfg,
667
+ files: suiteFiles,
668
+ timeoutMs: cfg.timeoutMs ?? suite.timeout,
669
+ },
670
+ });
671
+ }
672
+ try {
673
+ const outcome = await runSuites(steps, cfg);
674
+ return outcome.exitCode;
675
+ } finally {
676
+ await dropGlobalHooks();
677
+ }
378
678
  } catch (err) {
379
679
  // Re-exec under tsx when Node can't satisfy the TS-source imports
380
680
  // natively. Two failure shapes seen in the wild:
@@ -424,9 +724,25 @@ async function main() {
424
724
  }
425
725
  }
426
726
 
727
+ /**
728
+ * Japa semantics: the process exits on its own once the event loop drains, so a
729
+ * resource a test left open surfaces as a hang you can diagnose — rather than
730
+ * being swallowed by an unconditional `process.exit`, which also truncates
731
+ * pending stdout writes. `--force-exit` is the escape hatch, exactly as in Japa.
732
+ */
733
+ function finish(code) {
734
+ process.exitCode = code;
735
+ // The env var carries the config-declared `forceExit`; the argv check also
736
+ // covers a failure that happened before the config was ever read.
737
+ const forced =
738
+ process.env.HELIX_FORCE_EXIT === "1" ||
739
+ process.argv.includes("--force-exit");
740
+ if (forced) process.exit(code);
741
+ }
742
+
427
743
  main()
428
- .then((code) => process.exit(code))
744
+ .then(finish)
429
745
  .catch((err) => {
430
746
  process.stderr.write(`helix: ${err instanceof Error ? err.stack : err}\n`);
431
- process.exit(2);
747
+ finish(2);
432
748
  });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `--failed` cache — Japa's "run what failed last time".
3
+ *
4
+ * Japa writes the failed test TITLES to a cache file after every run, and
5
+ * `--failed` turns them into a `--tests` filter on the next one. Same contract
6
+ * here, same file shape (`{ tests: string[] }`), under
7
+ * `node_modules/.cache/helix/summary.json`.
8
+ *
9
+ * Named deviation: the cache is written only when the run produced per-test
10
+ * detail — i.e. the TypeScript pool. The native (Rust) engine reports totals
11
+ * only, so a run it owns leaves the previous cache untouched rather than
12
+ * blanking it. `--failed` therefore forces the TypeScript pool.
13
+ */
14
+ import type { Summary } from "./summary.js";
15
+ /** Titles of the tests that failed, in run order, without duplicates. */
16
+ export declare function failedTestTitles(summary: Summary): string[];
17
+ /**
18
+ * Persist the failed titles for a later `--failed`. Never throws: a run must
19
+ * not fail because its cache could not be written.
20
+ */
21
+ export declare function writeFailedCache(root: string, summary: Summary): Promise<void>;
22
+ /**
23
+ * The titles cached by the previous run, or an empty list when there is no
24
+ * usable cache (never run, unreadable, or malformed).
25
+ */
26
+ export declare function readFailedCache(root: string): Promise<string[]>;
27
+ //# sourceMappingURL=failed-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failed-cache.d.ts","sourceRoot":"","sources":["../../src/cli/failed-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAO5C,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAQ3D;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,GACd,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAWrE"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The `--failed` cache — Japa's "run what failed last time".
3
+ *
4
+ * Japa writes the failed test TITLES to a cache file after every run, and
5
+ * `--failed` turns them into a `--tests` filter on the next one. Same contract
6
+ * here, same file shape (`{ tests: string[] }`), under
7
+ * `node_modules/.cache/helix/summary.json`.
8
+ *
9
+ * Named deviation: the cache is written only when the run produced per-test
10
+ * detail — i.e. the TypeScript pool. The native (Rust) engine reports totals
11
+ * only, so a run it owns leaves the previous cache untouched rather than
12
+ * blanking it. `--failed` therefore forces the TypeScript pool.
13
+ */
14
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
15
+ import path from "node:path";
16
+ /** Where the cache lives, relative to the project root. */
17
+ function cacheFile(root) {
18
+ return path.join(root, "node_modules", ".cache", "helix", "summary.json");
19
+ }
20
+ /** Titles of the tests that failed, in run order, without duplicates. */
21
+ export function failedTestTitles(summary) {
22
+ const titles = new Set();
23
+ for (const file of summary.files) {
24
+ for (const test of file.tests) {
25
+ if (test.status === "fail")
26
+ titles.add(test.name);
27
+ }
28
+ }
29
+ return [...titles];
30
+ }
31
+ /**
32
+ * Persist the failed titles for a later `--failed`. Never throws: a run must
33
+ * not fail because its cache could not be written.
34
+ */
35
+ export async function writeFailedCache(root, summary) {
36
+ const file = cacheFile(root);
37
+ try {
38
+ await mkdir(path.dirname(file), { recursive: true });
39
+ await writeFile(file, JSON.stringify({ tests: failedTestTitles(summary) }), "utf8");
40
+ }
41
+ catch {
42
+ // Best-effort — a read-only or missing node_modules is not a run failure.
43
+ }
44
+ }
45
+ /**
46
+ * The titles cached by the previous run, or an empty list when there is no
47
+ * usable cache (never run, unreadable, or malformed).
48
+ */
49
+ export async function readFailedCache(root) {
50
+ try {
51
+ const raw = await readFile(cacheFile(root), "utf8");
52
+ const parsed = JSON.parse(raw);
53
+ if (parsed === null || typeof parsed !== "object")
54
+ return [];
55
+ const tests = Reflect.get(parsed, "tests");
56
+ if (!Array.isArray(tests))
57
+ return [];
58
+ return tests.filter((t) => typeof t === "string");
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ }
64
+ //# sourceMappingURL=failed-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failed-cache.js","sourceRoot":"","sources":["../../src/cli/failed-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,2DAA2D;AAC3D,SAAS,SAAS,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;AAC3E,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAChD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAClC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC/B,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM;gBAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnD,CAAC;IACF,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,IAAY,EACZ,OAAgB;IAEhB,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC;QACJ,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,SAAS,CACd,IAAI,EACJ,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC,EACpD,MAAM,CACN,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACR,0EAA0E;IAC3E,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY;IACjD,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QACpD,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QAC7D,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACrC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;AACF,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Glob patterns for suite `files` entries.
3
+ *
4
+ * AdonisJS declares suites with real globs — its own defaults are
5
+ * `tests/unit/**​/*.spec.(js|ts)` — so an `adonisrc.ts` suite list has to be
6
+ * portable verbatim. Compiles to a `RegExp` over root-relative POSIX paths:
7
+ * `*`, `**`, `?`, `{a,b}`, `(a|b)`, `[abc]`, and the extglob quantifiers with an
8
+ * exact regex equivalent — `@(a|b)`, `?(a|b)`, `*(a|b)`, `+(a|b)`.
9
+ *
10
+ * An ENTRY may also start with `!` to exclude what it matches from the suite,
11
+ * the way a file list is usually written.
12
+ *
13
+ * One form stays refused: `!(a|b)`, the negated extglob GROUP. Its semantics
14
+ * ("anything in this segment other than a or b", composing with whatever
15
+ * follows) have no faithful regex rendering, and a near-miss would silently
16
+ * select the wrong files — the one thing a file selector must not do.
17
+ */
18
+ /** Whether an entry is a pattern rather than a plain path. */
19
+ export declare function isGlob(entry: string): boolean;
20
+ /** Whether an entry excludes rather than selects (a leading `!`). */
21
+ export declare function isNegated(entry: string): boolean;
22
+ /** The pattern an entry carries, with any leading `!` stripped. */
23
+ export declare function withoutNegation(entry: string): string;
24
+ /** The reason an entry cannot be compiled, or `undefined` when it can. */
25
+ export declare function globRejection(entry: string): string | undefined;
26
+ /**
27
+ * The leading path that contains no pattern character — the directory a walk
28
+ * can start from. `tests/unit/**​/*.spec.ts` → `tests/unit`.
29
+ */
30
+ export declare function globBaseDir(entry: string): string;
31
+ /**
32
+ * Compile a glob to an anchored `RegExp`. Paths are matched with `/`
33
+ * separators, relative to the root the pattern was declared against.
34
+ */
35
+ export declare function globToRegExp(entry: string): RegExp;
36
+ //# sourceMappingURL=glob.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../../src/cli/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,8DAA8D;AAC9D,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7C;AAKD,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK/D;AAeD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAwElD"}