@c9up/helix 0.1.4 → 0.1.6

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 (65) hide show
  1. package/dist/cli/coverage/diff/overlay.d.ts.map +1 -1
  2. package/dist/cli/coverage/diff/overlay.js +19 -6
  3. package/dist/cli/coverage/diff/overlay.js.map +1 -1
  4. package/dist/cli/pool.js +10 -0
  5. package/dist/cli/pool.js.map +1 -1
  6. package/dist/runtime/equals.d.ts.map +1 -1
  7. package/dist/runtime/equals.js +6 -2
  8. package/dist/runtime/equals.js.map +1 -1
  9. package/dist/runtime/suite.d.ts +26 -0
  10. package/dist/runtime/suite.d.ts.map +1 -1
  11. package/dist/runtime/suite.js +21 -18
  12. package/dist/runtime/suite.js.map +1 -1
  13. package/index.darwin-arm64.node +0 -0
  14. package/index.darwin-x64.node +0 -0
  15. package/index.linux-arm64-gnu.node +0 -0
  16. package/index.linux-x64-gnu.node +0 -0
  17. package/index.win32-x64-msvc.node +0 -0
  18. package/package.json +2 -2
  19. package/src/cli/coverage/aggregate.ts +0 -231
  20. package/src/cli/coverage/collect.ts +0 -63
  21. package/src/cli/coverage/diff/base.ts +0 -46
  22. package/src/cli/coverage/diff/index.ts +0 -160
  23. package/src/cli/coverage/diff/overlay.ts +0 -62
  24. package/src/cli/coverage/diff/parse.ts +0 -121
  25. package/src/cli/coverage/diff/reporters.ts +0 -82
  26. package/src/cli/coverage/diff/types.ts +0 -46
  27. package/src/cli/coverage/filter.ts +0 -71
  28. package/src/cli/coverage/glob.ts +0 -0
  29. package/src/cli/coverage/index.ts +0 -126
  30. package/src/cli/coverage/reporters/json.ts +0 -40
  31. package/src/cli/coverage/reporters/lcov.ts +0 -54
  32. package/src/cli/coverage/reporters/text.ts +0 -48
  33. package/src/cli/coverage/thresholds.ts +0 -73
  34. package/src/cli/coverage/types.ts +0 -93
  35. package/src/cli/discover.ts +0 -174
  36. package/src/cli/native.ts +0 -104
  37. package/src/cli/pool.ts +0 -486
  38. package/src/cli/reporter.ts +0 -155
  39. package/src/cli/run.ts +0 -440
  40. package/src/cli/summary.ts +0 -42
  41. package/src/cli/watch/loop.ts +0 -159
  42. package/src/cli/watch/types.ts +0 -22
  43. package/src/cli/watch/watcher.ts +0 -145
  44. package/src/container/index.ts +0 -16
  45. package/src/container/override.ts +0 -86
  46. package/src/container/spy.ts +0 -25
  47. package/src/index.ts +0 -42
  48. package/src/runtime/assertion-error.ts +0 -38
  49. package/src/runtime/cli-worker.ts +0 -140
  50. package/src/runtime/equals.ts +0 -400
  51. package/src/runtime/expect.ts +0 -173
  52. package/src/runtime/index.ts +0 -50
  53. package/src/runtime/lifecycle.ts +0 -17
  54. package/src/runtime/matchers.ts +0 -452
  55. package/src/runtime/run.ts +0 -573
  56. package/src/runtime/suite.ts +0 -310
  57. package/src/runtime/test-context.ts +0 -59
  58. package/src/runtime/vi/fake-timers.ts +0 -410
  59. package/src/runtime/vi/index.ts +0 -254
  60. package/src/runtime/vi/spy.ts +0 -224
  61. package/src/runtime/vi/spyOn.ts +0 -155
  62. package/src/runtime/vi/system-time.ts +0 -121
  63. package/src/runtime/worker.ts +0 -239
  64. package/src/time/freeze.ts +0 -229
  65. package/src/time/index.ts +0 -16
@@ -1,155 +0,0 @@
1
- /**
2
- * Reporters — receive lifecycle callbacks as each file completes, write
3
- * incremental output to the given sink. Three built-ins matching the
4
- * Rust-side reporter trait:
5
- * - Dot: one char per test
6
- * - Spec: nested tree with colours + diff lines on failure
7
- * - Json: NDJSON to stdout, machine-readable
8
- */
9
-
10
- import type { FileResult } from "../runtime/run.js";
11
- import type { WorkerErrorMessage } from "./pool.js";
12
- import type { Summary } from "./summary.js";
13
-
14
- export interface Reporter {
15
- onFileStart(file: string): void;
16
- onFileResult(result: FileResult): void;
17
- onFileError(error: WorkerErrorMessage): void;
18
- onSummary(summary: Summary): void;
19
- }
20
-
21
- type Sink = {
22
- write(chunk: string): void;
23
- writeLine(chunk: string): void;
24
- };
25
-
26
- export function stdoutSink(): Sink {
27
- return {
28
- write(chunk: string): void {
29
- process.stdout.write(chunk);
30
- },
31
- writeLine(chunk: string): void {
32
- process.stdout.write(`${chunk}\n`);
33
- },
34
- };
35
- }
36
-
37
- const ANSI = {
38
- dim: (s: string, on: boolean): string => (on ? `\x1b[90m${s}\x1b[0m` : s),
39
- red: (s: string, on: boolean): string => (on ? `\x1b[31m${s}\x1b[0m` : s),
40
- green: (s: string, on: boolean): string => (on ? `\x1b[32m${s}\x1b[0m` : s),
41
- yellow: (s: string, on: boolean): string => (on ? `\x1b[33m${s}\x1b[0m` : s),
42
- };
43
-
44
- export class DotReporter implements Reporter {
45
- constructor(private readonly sink: Sink = stdoutSink()) {}
46
- onFileStart(_file: string): void {}
47
- onFileResult(result: FileResult): void {
48
- for (const t of result.tests) {
49
- const c =
50
- t.status === "pass"
51
- ? "."
52
- : t.status === "fail"
53
- ? "F"
54
- : t.status === "skip"
55
- ? "-"
56
- : "*";
57
- this.sink.write(c);
58
- }
59
- }
60
- onFileError(_error: WorkerErrorMessage): void {
61
- this.sink.write("E");
62
- }
63
- onSummary(summary: Summary): void {
64
- this.sink.writeLine("");
65
- printSummary(this.sink, summary);
66
- }
67
- }
68
-
69
- export class SpecReporter implements Reporter {
70
- constructor(
71
- private readonly sink: Sink = stdoutSink(),
72
- private readonly useColors = true,
73
- ) {}
74
- onFileStart(file: string): void {
75
- this.sink.writeLine(
76
- `${ANSI.dim("▶", this.useColors)} ${ANSI.dim(file, this.useColors)}`,
77
- );
78
- }
79
- onFileResult(result: FileResult): void {
80
- for (const t of result.tests) {
81
- const marker =
82
- t.status === "pass"
83
- ? ANSI.green("✔", this.useColors)
84
- : t.status === "fail"
85
- ? ANSI.red("✘", this.useColors)
86
- : t.status === "skip"
87
- ? ANSI.yellow("○", this.useColors)
88
- : ANSI.dim("☐", this.useColors);
89
- this.sink.writeLine(` ${marker} ${t.fullName}`);
90
- if (t.error) {
91
- this.sink.writeLine(
92
- ` ${ANSI.red(t.error.message, this.useColors)}`,
93
- );
94
- if (t.error.actual !== undefined && t.error.expected !== undefined) {
95
- this.sink.writeLine(
96
- ` ${ANSI.dim("actual: ", this.useColors)} ${JSON.stringify(t.error.actual)}`,
97
- );
98
- this.sink.writeLine(
99
- ` ${ANSI.dim("expected:", this.useColors)} ${JSON.stringify(t.error.expected)}`,
100
- );
101
- }
102
- }
103
- }
104
- }
105
- onFileError(error: WorkerErrorMessage): void {
106
- const file = error.file ?? "<unknown>";
107
- this.sink.writeLine(
108
- `${ANSI.red("✘", this.useColors)} ${file}: ${error.message}`,
109
- );
110
- }
111
- onSummary(summary: Summary): void {
112
- this.sink.writeLine("");
113
- printSummary(this.sink, summary);
114
- }
115
- }
116
-
117
- export class JsonReporter implements Reporter {
118
- constructor(private readonly sink: Sink = stdoutSink()) {}
119
- onFileStart(file: string): void {
120
- this.sink.writeLine(JSON.stringify({ event: "file:start", file }));
121
- }
122
- onFileResult(result: FileResult): void {
123
- this.sink.writeLine(JSON.stringify({ event: "file:end", result }));
124
- }
125
- onFileError(error: WorkerErrorMessage): void {
126
- this.sink.writeLine(JSON.stringify({ event: "file:error", error }));
127
- }
128
- onSummary(summary: Summary): void {
129
- this.sink.writeLine(JSON.stringify({ event: "summary", summary }));
130
- }
131
- }
132
-
133
- function printSummary(sink: Sink, summary: Summary): void {
134
- const t = summary.totals;
135
- sink.writeLine("──────────────────────────────────────");
136
- sink.writeLine(
137
- ` ${t.pass} passed | ${t.fail} failed | ${t.skip} skipped | ${t.todo} todo | ${t.fileErrors} file errors`,
138
- );
139
- sink.writeLine(` ${summary.durationMs} ms`);
140
- }
141
-
142
- /** Factory from a CLI-style name. */
143
- export function makeReporter(
144
- name: string | undefined,
145
- useColors: boolean,
146
- ): Reporter {
147
- switch ((name ?? "spec").toLowerCase()) {
148
- case "dot":
149
- return new DotReporter();
150
- case "json":
151
- return new JsonReporter();
152
- default:
153
- return new SpecReporter(stdoutSink(), useColors);
154
- }
155
- }
package/src/cli/run.ts DELETED
@@ -1,440 +0,0 @@
1
- /**
2
- * Orchestrator entry point — the TS equivalent of `ream-test-napi::run`.
3
- *
4
- * Discovers test files (or uses the explicit list), spawns the worker
5
- * pool, collects framed results, aggregates into a `Summary`, and invokes
6
- * the reporter at each milestone. When coverage is enabled, also threads
7
- * the V8 coverage lifecycle (open session → forward `NODE_V8_COVERAGE`
8
- * env to workers → aggregate after pool drain → write reports → enforce
9
- * thresholds).
10
- *
11
- * Designed so the future NAPI binding can be plugged in as a drop-in
12
- * replacement: both sides accept the same `RunConfig` shape and return
13
- * the same `Summary`.
14
- */
15
-
16
- import { existsSync } from "node:fs";
17
- import { rm } from "node:fs/promises";
18
- import os from "node:os";
19
- import path from "node:path";
20
- import { fileURLToPath } from "node:url";
21
- import {
22
- type DiffOptions,
23
- type DiffSummary,
24
- diffViolationSummary,
25
- finaliseDiff,
26
- } from "./coverage/diff/index.js";
27
- import {
28
- type CoverageOptions,
29
- type CoverageSession,
30
- type CoverageSummary,
31
- finalise as finaliseCoverage,
32
- openSession,
33
- type ThresholdViolation,
34
- violationSummary,
35
- } from "./coverage/index.js";
36
- import { type DiscoveryOptions, discover } from "./discover.js";
37
- import { getNative } from "./native.js";
38
- import { runPool } from "./pool.js";
39
- import { makeReporter, type Reporter } from "./reporter.js";
40
- import {
41
- buildSummary,
42
- exitCode,
43
- type Summary,
44
- type Totals,
45
- } from "./summary.js";
46
- import { runWatch } from "./watch/loop.js";
47
- import type { WatchOptions } from "./watch/types.js";
48
-
49
- export type { WatchOptions } from "./watch/types.js";
50
-
51
- const DEFAULT_WATCH_DEBOUNCE_MS = 200;
52
- const DEFAULT_WATCH_INCLUDE = [
53
- "src/**/*.{ts,tsx,js,mjs,cjs}",
54
- "tests/**/*.{ts,tsx,js,mjs,cjs}",
55
- "test/**/*.{ts,tsx,js,mjs,cjs}",
56
- ];
57
- const DEFAULT_WATCH_EXCLUDE = [
58
- "node_modules/**",
59
- "dist/**",
60
- "build/**",
61
- "coverage/**",
62
- ".helix-coverage/**",
63
- ".git/**",
64
- ".wolf/**",
65
- "target/**",
66
- ".next/**",
67
- ];
68
-
69
- export interface RunConfig {
70
- /** Absolute root directory to discover from. */
71
- root: string;
72
- /** Explicit files to run. When non-empty, discovery is skipped. */
73
- files?: string[];
74
- /** Discovery options (suffixes, excludes). */
75
- discovery?: DiscoveryOptions;
76
- /** Number of concurrent workers. Default: os.cpus().length. */
77
- threads?: number;
78
- /** Per-file timeout (ms). Default 60 000. */
79
- timeoutMs?: number;
80
- /** Reporter name: `"dot" | "spec" | "json"`. Default `"spec"`. */
81
- reporter?: string;
82
- /** Enable ANSI colours. Default: stdout is TTY. */
83
- useColors?: boolean;
84
- /** Pluggable reporter instance (overrides `reporter` when provided). */
85
- reporterInstance?: Reporter;
86
- /** Override the node binary. Default: `process.execPath`. */
87
- nodeBin?: string;
88
- /** Extra args for node (before the worker entry). */
89
- nodeArgs?: string[];
90
- /** Override the worker entry path (defaults to bundled `cli-worker.ts`). */
91
- workerEntry?: string;
92
- /** Coverage collection + reporting. Defaults to `{ enabled: false }`. */
93
- coverage?: CoverageOptions;
94
- /** Diff coverage vs a base ref. Requires `coverage.enabled === true`. */
95
- diffCoverage?: DiffOptions;
96
- /** Watch mode: re-run on file changes. */
97
- watch?: WatchOptions;
98
- }
99
-
100
- export interface RunOutcome {
101
- summary: Summary;
102
- coverage?: CoverageSummary;
103
- coverageViolations?: ThresholdViolation[];
104
- diffCoverage?: DiffSummary;
105
- diffCoverageViolations?: ThresholdViolation[];
106
- exitCode: number;
107
- }
108
-
109
- function defaultWorkerEntry(): string {
110
- // Resolve relative to THIS module's directory so it works whether we run
111
- // from `src/cli/run.ts` (dev: src sibling is `.ts`) or from the compiled
112
- // `dist/cli/run.js` (publish: sibling is `.js`). Hardcoding `.ts` made
113
- // the spawned worker open a non-existent `dist/runtime/cli-worker.ts`
114
- // when consumed via the dist tarball, dying silently before the pool's
115
- // frame handler could observe anything.
116
- const here = path.dirname(fileURLToPath(import.meta.url));
117
- const compiled = path.resolve(here, "../runtime/cli-worker.js");
118
- if (existsSync(compiled)) return compiled;
119
- return path.resolve(here, "../runtime/cli-worker.ts");
120
- }
121
-
122
- /**
123
- * Plain-path execution via the native `ream-test-napi` engine: it discovers
124
- * (skipped here — we pass the resolved `files`), spawns the worker pool, drives
125
- * the reporter, and returns aggregated totals. Per-file detail is carried in
126
- * `payload.json` (same shape as `Summary`) for callers that need it; the CLI
127
- * only consumes `exitCode`, and the Rust reporter already streamed per-file
128
- * output, so the reconstructed `Summary` keeps the detail arrays empty.
129
- */
130
- async function runNative(
131
- config: RunConfig,
132
- root: string,
133
- files: string[],
134
- ): Promise<RunOutcome> {
135
- const { run: nativeRun } = getNative();
136
- const payload = await nativeRun({
137
- root,
138
- files,
139
- threads: config.threads,
140
- timeoutMs: config.timeoutMs,
141
- reporter: config.reporter,
142
- workerEntry: config.workerEntry ?? defaultWorkerEntry(),
143
- nodeBin: config.nodeBin ?? process.execPath,
144
- nodeArgs: config.nodeArgs,
145
- useColors: config.useColors ?? process.stdout.isTTY === true,
146
- });
147
- const totals: Totals = {
148
- pass: payload.pass,
149
- fail: payload.fail,
150
- skip: payload.skip,
151
- todo: payload.todo,
152
- fileErrors: payload.fileErrors,
153
- };
154
- const summary: Summary = {
155
- totals,
156
- files: [],
157
- fileErrors: [],
158
- durationMs: payload.durationMs,
159
- };
160
- return { summary, exitCode: payload.exitCode };
161
- }
162
-
163
- export async function run(config: RunConfig): Promise<RunOutcome> {
164
- if (!config.watch?.enabled) return runOnce(config);
165
- const root = path.isAbsolute(config.root)
166
- ? config.root
167
- : path.resolve(config.root);
168
- const debounceMs = config.watch.debounceMs ?? DEFAULT_WATCH_DEBOUNCE_MS;
169
- // When the user has configured custom coverage globs, mirror them in
170
- // the watcher so the two surfaces agree on "files I care about".
171
- // Otherwise fall back to the watch defaults (which intentionally
172
- // include `tests/**` even though coverage doesn't, so editing a test
173
- // triggers a re-run).
174
- const include =
175
- config.watch.include ?? config.coverage?.include ?? DEFAULT_WATCH_INCLUDE;
176
- const exclude = [
177
- ...(config.watch.exclude ??
178
- config.coverage?.exclude ??
179
- DEFAULT_WATCH_EXCLUDE),
180
- ];
181
- return runWatch(
182
- {
183
- root,
184
- include,
185
- exclude,
186
- debounceMs,
187
- signal: config.watch.signal,
188
- },
189
- () => runOnce(config),
190
- );
191
- }
192
-
193
- /** Aggregated coverage + diff-coverage results threaded back into `RunOutcome`. */
194
- interface CoverageOutcome {
195
- coverage?: CoverageSummary;
196
- coverageViolations?: ThresholdViolation[];
197
- diffCoverage?: DiffSummary;
198
- diffCoverageViolations?: ThresholdViolation[];
199
- // Tracks unrecoverable diff-cov failures so the run still exits non-zero
200
- // when diff-cov was supposed to gate the PR (otherwise a thrown error
201
- // in finaliseDiff would silently downgrade exit to the full-tree code).
202
- diffCoverageFailed: boolean;
203
- }
204
-
205
- /** Resolve the explicit file list, or discover, warning when nothing matches. */
206
- async function resolveRunFiles(
207
- config: RunConfig,
208
- root: string,
209
- ): Promise<string[]> {
210
- const files =
211
- config.files && config.files.length > 0
212
- ? config.files.map((f) => (path.isAbsolute(f) ? f : path.resolve(f)))
213
- : await discover(root, config.discovery);
214
-
215
- if (files.length === 0) {
216
- process.stderr.write(
217
- `helix: no test files found under ${root} — check your include/exclude patterns.\n`,
218
- );
219
- }
220
- return files;
221
- }
222
-
223
- /**
224
- * Refuse mis-configured diff coverage up front (before spawning workers) so the
225
- * user fixes the config rather than getting a silent no-op or 0% diff coverage.
226
- */
227
- function assertDiffCoverageConfig(config: RunConfig, root: string): void {
228
- if (!config.diffCoverage?.enabled) return;
229
-
230
- // Diff-cov requires the full-tree coverage to be enabled — it has nothing
231
- // to overlay otherwise.
232
- if (config.coverage?.enabled !== true) {
233
- throw new Error(
234
- "diffCoverage.enabled requires coverage.enabled — diff-cov has nothing to overlay otherwise.",
235
- );
236
- }
237
-
238
- // Diff-cov spawns git in `diffCoverage.cwd` (defaulting to `coverage.root`).
239
- // The two paths must share a common ancestry — either coverage.root is
240
- // under diffCoverage.cwd (common monorepo case: `cwd` = git root,
241
- // `root` = a sub-package), or diffCoverage.cwd is under coverage.root
242
- // (legacy single-repo case). When they're fully disjoint, every diff
243
- // entry resolves to a path the coverage summary never indexes →
244
- // silent 0% diff coverage. Refuse only that case.
245
- if (!config.diffCoverage.cwd) return;
246
- const covRoot = path.resolve(config.coverage?.root ?? root);
247
- const diffCwd = path.resolve(config.diffCoverage.cwd);
248
- const covUnderDiff = path.relative(diffCwd, covRoot);
249
- const diffUnderCov = path.relative(covRoot, diffCwd);
250
- const covDescendantOfDiff =
251
- covUnderDiff !== "" &&
252
- !covUnderDiff.startsWith("..") &&
253
- !path.isAbsolute(covUnderDiff);
254
- const diffDescendantOfCov =
255
- diffUnderCov !== "" &&
256
- !diffUnderCov.startsWith("..") &&
257
- !path.isAbsolute(diffUnderCov);
258
- const equal = covUnderDiff === "";
259
- if (!equal && !covDescendantOfDiff && !diffDescendantOfCov) {
260
- throw new Error(
261
- `diffCoverage.cwd (${diffCwd}) and coverage.root (${covRoot}) are disjoint paths; diff entries would never overlay coverage.`,
262
- );
263
- }
264
- }
265
-
266
- /**
267
- * Finalise full-tree coverage, then overlay diff coverage on top. Each stage is
268
- * isolated: a failed coverage finalise (disk full, permissions) or a thrown
269
- * diff-cov is surfaced to stderr without crashing the run, but a diff-cov
270
- * failure still flips `diffCoverageFailed` so the CI gate stays red.
271
- */
272
- async function finaliseCoverageOutcome(
273
- config: RunConfig & { coverage: CoverageOptions },
274
- root: string,
275
- session: CoverageSession,
276
- ): Promise<CoverageOutcome> {
277
- const out: CoverageOutcome = { diffCoverageFailed: false };
278
- try {
279
- const finalised = await finaliseCoverage({
280
- root: config.coverage.root ?? root,
281
- session,
282
- enabled: true,
283
- include: config.coverage.include,
284
- exclude: config.coverage.exclude,
285
- reporters: config.coverage.reporters,
286
- outputDir: config.coverage.outputDir,
287
- thresholds: config.coverage.thresholds,
288
- });
289
- out.coverage = finalised.summary;
290
- out.coverageViolations = finalised.violations;
291
- if (finalised.textReport) process.stdout.write(finalised.textReport);
292
- if (finalised.violations.length > 0) {
293
- process.stderr.write(`${violationSummary(finalised.violations)}\n`);
294
- }
295
-
296
- // Diff coverage runs AFTER full-tree finalise, using its summary.
297
- if (config.diffCoverage?.enabled && out.coverage) {
298
- await overlayDiffCoverage(config, root, out.coverage, out);
299
- }
300
- } catch (err) {
301
- // Coverage finalisation failed (disk full, permission, etc.) — the test
302
- // summary already printed; surface the failure but don't crash the whole
303
- // run with a stack trace.
304
- process.stderr.write(
305
- `helix-coverage: ${err instanceof Error ? err.message : String(err)}\n`,
306
- );
307
- out.coverageViolations = [];
308
- }
309
- return out;
310
- }
311
-
312
- /** Overlay diff coverage onto an already-finalised full-tree summary. */
313
- async function overlayDiffCoverage(
314
- config: RunConfig & { coverage: CoverageOptions },
315
- root: string,
316
- coverage: CoverageSummary,
317
- out: CoverageOutcome,
318
- ): Promise<void> {
319
- if (!config.diffCoverage?.enabled) return;
320
- try {
321
- const diff = await finaliseDiff({
322
- enabled: true,
323
- root: config.coverage.root ?? root,
324
- coverage,
325
- base: config.diffCoverage.base,
326
- thresholds: config.diffCoverage.thresholds,
327
- outputDir: config.diffCoverage.outputDir ?? config.coverage.outputDir,
328
- cwd: config.diffCoverage.cwd,
329
- });
330
- if (diff.warning) {
331
- process.stderr.write(`${diff.warning}\n`);
332
- return;
333
- }
334
- out.diffCoverage = diff.summary;
335
- out.diffCoverageViolations = diff.violations;
336
- if (diff.textReport) process.stdout.write(diff.textReport);
337
- if (diff.violations.length > 0) {
338
- process.stderr.write(`${diffViolationSummary(diff.violations)}\n`);
339
- }
340
- } catch (err) {
341
- process.stderr.write(
342
- `helix-diff-cov: ${err instanceof Error ? err.message : String(err)}\n`,
343
- );
344
- // Don't let a thrown finaliseDiff masquerade as a clean run — the user
345
- // explicitly opted into diff-cov and a CI gate around it must still fail.
346
- out.diffCoverageFailed = true;
347
- }
348
- }
349
-
350
- async function runOnce(config: RunConfig): Promise<RunOutcome> {
351
- const started = Date.now();
352
- const root = path.isAbsolute(config.root)
353
- ? config.root
354
- : path.resolve(config.root);
355
-
356
- const files = await resolveRunFiles(config, root);
357
-
358
- // Cutover: the Rust NAPI engine owns the plain discovery + worker-pool +
359
- // reporter + summary path (42-N-orchestrator). Delegate unless a TS-only
360
- // layer is active — a pluggable reporter instance, coverage, or diff-cov,
361
- // none of which the Rust `run` exposes yet. Watch is already unwrapped by
362
- // `run()` above, so it never reaches here.
363
- if (
364
- !config.reporterInstance &&
365
- config.coverage?.enabled !== true &&
366
- config.diffCoverage?.enabled !== true
367
- ) {
368
- return runNative(config, root, files);
369
- }
370
-
371
- const reporter =
372
- config.reporterInstance ??
373
- makeReporter(
374
- config.reporter,
375
- config.useColors ?? process.stdout.isTTY === true,
376
- );
377
-
378
- const threads = config.threads ?? os.cpus().length;
379
-
380
- assertDiffCoverageConfig(config, root);
381
-
382
- // Open a coverage session BEFORE spawning workers so `NODE_V8_COVERAGE`
383
- // is set in their env. The pool forwards `extraEnv` to every spawn.
384
- const coverageEnabled = config.coverage?.enabled === true;
385
- const session = coverageEnabled ? await openSession() : undefined;
386
-
387
- try {
388
- const { results, errors } = await runPool(
389
- files,
390
- {
391
- workerEntry: config.workerEntry ?? defaultWorkerEntry(),
392
- nodeBin: config.nodeBin,
393
- nodeArgs: config.nodeArgs,
394
- threads,
395
- timeoutMs: config.timeoutMs,
396
- extraEnv: session?.env,
397
- },
398
- reporter,
399
- );
400
-
401
- const summary = buildSummary(results, errors, Date.now() - started);
402
- reporter.onSummary(summary);
403
-
404
- const cov: CoverageOutcome =
405
- session && config.coverage
406
- ? await finaliseCoverageOutcome(
407
- { ...config, coverage: config.coverage },
408
- root,
409
- session,
410
- )
411
- : { diffCoverageFailed: false };
412
-
413
- const baseExit = exitCode(summary);
414
- const coverageExit =
415
- cov.coverageViolations && cov.coverageViolations.length > 0 ? 1 : 0;
416
- const diffCoverageExit =
417
- (cov.diffCoverageViolations && cov.diffCoverageViolations.length > 0) ||
418
- cov.diffCoverageFailed
419
- ? 1
420
- : 0;
421
- return {
422
- summary,
423
- coverage: cov.coverage,
424
- coverageViolations: cov.coverageViolations,
425
- diffCoverage: cov.diffCoverage,
426
- diffCoverageViolations: cov.diffCoverageViolations,
427
- exitCode: Math.max(baseExit, coverageExit, diffCoverageExit),
428
- };
429
- } finally {
430
- // Belt-and-braces: if `runPool` itself rejected before
431
- // `finaliseCoverage` ran (which normally cleans the temp dir), we
432
- // still remove the session dir here so failed runs don't leak under
433
- // `.helix-coverage/`.
434
- if (session) {
435
- await rm(session.envDir, { recursive: true, force: true }).catch(
436
- () => {},
437
- );
438
- }
439
- }
440
- }
@@ -1,42 +0,0 @@
1
- import type { FileResult } from "../runtime/run.js";
2
- import type { WorkerErrorMessage } from "./pool.js";
3
-
4
- export interface Totals {
5
- pass: number;
6
- fail: number;
7
- skip: number;
8
- todo: number;
9
- fileErrors: number;
10
- }
11
-
12
- export interface Summary {
13
- totals: Totals;
14
- files: FileResult[];
15
- fileErrors: WorkerErrorMessage[];
16
- durationMs: number;
17
- }
18
-
19
- export function buildSummary(
20
- files: FileResult[],
21
- fileErrors: WorkerErrorMessage[],
22
- durationMs: number,
23
- ): Summary {
24
- const totals: Totals = {
25
- pass: 0,
26
- fail: 0,
27
- skip: 0,
28
- todo: 0,
29
- fileErrors: fileErrors.length,
30
- };
31
- for (const f of files) {
32
- totals.pass += f.totals.pass;
33
- totals.fail += f.totals.fail;
34
- totals.skip += f.totals.skip;
35
- totals.todo += f.totals.todo;
36
- }
37
- return { totals, files, fileErrors, durationMs };
38
- }
39
-
40
- export function exitCode(summary: Summary): number {
41
- return summary.totals.fail > 0 || summary.totals.fileErrors > 0 ? 1 : 0;
42
- }